Compare commits
57 commits
atomic-del
...
master
Author | SHA1 | Date | |
---|---|---|---|
ef46005e84 | |||
2ecd0dd1de | |||
2775328777 | |||
96bba9d47d | |||
6671562ab7 | |||
373eca7a19 | |||
70d5c713ca | |||
f3eeb711a0 | |||
80f788c38b | |||
1192cffa29 | |||
a3a6d2b5ba | |||
756c8b3ef4 | |||
c2029ed271 | |||
82c80a53a6 | |||
4b96e03f54 | |||
d591282f5e | |||
|
873ef89e42 | ||
d7a8660952 | |||
eec5ce1a99 | |||
f7cdb9df70 | |||
2b1c4b7245 | |||
e885beaab9 | |||
d25fa27c24 | |||
7a0067460b | |||
63a98f3b41 | |||
b29f49fefc | |||
|
8e508b921c | ||
9052a02598 | |||
57c4fef275 | |||
748685e53e | |||
8212c62663 | |||
8d48909e4f | |||
5587de26c7 | |||
|
9bb310e0d1 | ||
d621657f16 | |||
b33a595b67 | |||
|
ce08d2c827 | ||
|
a3ad95c058 | ||
|
3b804799c3 | ||
|
323de25075 | ||
|
c427e10f17 | ||
|
329995f4a3 | ||
|
f0a754eaa8 | ||
|
504ead526a | ||
15e669d943 | |||
e01e82aa65 | |||
a72ca7dcf4 | |||
599c265530 | |||
a97b5921c9 | |||
5b6e8cc110 | |||
a2517d3d03 | |||
9b8d02d1c3 | |||
7a7aef71cd | |||
|
4603ab67bb | ||
95d3fb08f4 | |||
4ba0357d49 | |||
|
763c708253 |
91 changed files with 2293 additions and 1493 deletions
|
@ -59,40 +59,40 @@ jobs:
|
||||||
- name: Test
|
- name: Test
|
||||||
run: pnpm --filter backend test-and-coverage
|
run: pnpm --filter backend test-and-coverage
|
||||||
|
|
||||||
e2e:
|
# e2e:
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
|
#
|
||||||
strategy:
|
# strategy:
|
||||||
matrix:
|
# matrix:
|
||||||
node-version: [22.11.0]
|
# node-version: [22.11.0]
|
||||||
|
#
|
||||||
services:
|
# services:
|
||||||
postgres:
|
# postgres:
|
||||||
image: l1drm/postgres-pgroonga:alpine-15-znver4
|
# image: l1drm/postgres-pgroonga:alpine-15-znver4
|
||||||
env:
|
# env:
|
||||||
POSTGRES_DB: test-misskey
|
# POSTGRES_DB: test-misskey
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
# POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
redis:
|
# redis:
|
||||||
image: redis:7
|
# image: redis:7
|
||||||
|
#
|
||||||
steps:
|
# steps:
|
||||||
- uses: actions/checkout@v4.1.1
|
# - uses: actions/checkout@v4.1.1
|
||||||
with:
|
# with:
|
||||||
submodules: true
|
# submodules: true
|
||||||
- name: Install pnpm
|
# - name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
# uses: pnpm/action-setup@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
# - name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4.0.4
|
# uses: actions/setup-node@v4.0.4
|
||||||
with:
|
# with:
|
||||||
node-version: ${{ matrix.node-version }}
|
# node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
# cache: 'pnpm'
|
||||||
- run: corepack enable
|
# - run: corepack enable
|
||||||
- run: pnpm i --frozen-lockfile
|
# - run: pnpm i --frozen-lockfile
|
||||||
- name: Check pnpm-lock.yaml
|
# - name: Check pnpm-lock.yaml
|
||||||
run: git diff --exit-code pnpm-lock.yaml
|
# run: git diff --exit-code pnpm-lock.yaml
|
||||||
- name: Copy Configure
|
# - name: Copy Configure
|
||||||
run: cp .forgejo/misskey/test.yml .config
|
# run: cp .forgejo/misskey/test.yml .config
|
||||||
- name: Build
|
# - name: Build
|
||||||
run: pnpm build
|
# run: pnpm build
|
||||||
- name: Test
|
# - name: Test
|
||||||
run: pnpm --filter backend test-and-coverage:e2e
|
# run: pnpm --filter backend test-and-coverage:e2e
|
97
.github/ISSUE_TEMPLATE/01_bug-report.yml
vendored
97
.github/ISSUE_TEMPLATE/01_bug-report.yml
vendored
|
@ -1,97 +0,0 @@
|
||||||
name: 🐛 Bug Report
|
|
||||||
description: Create a report to help us improve
|
|
||||||
labels: ["⚠️bug?"]
|
|
||||||
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thanks for reporting!
|
|
||||||
First, in order to avoid duplicate Issues, please search to see if the problem you found has already been reported.
|
|
||||||
Also, If you are NOT owner/admin of server, PLEASE DONT REPORT SERVER SPECIFIC ISSUES TO HERE! (e.g. feature XXX is not working in misskey.example) Please try with another misskey servers, and if your issue is only reproducible with specific server, contact your server's owner/admin first.
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: 💡 Summary
|
|
||||||
description: Tell us what the bug is
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: 🥰 Expected Behavior
|
|
||||||
description: Tell us what should happen
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: 🤬 Actual Behavior
|
|
||||||
description: |
|
|
||||||
Tell us what happens instead of the expected behavior.
|
|
||||||
Please include errors from the developer console and/or server log files if you have access to them.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: 📝 Steps to Reproduce
|
|
||||||
placeholder: |
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: 💻 Frontend Environment
|
|
||||||
description: |
|
|
||||||
Tell us where on the platform it happens
|
|
||||||
DO NOT WRITE "latest". Please provide the specific version.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
* Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4
|
|
||||||
* Browser: Chrome 113.0.5672.126
|
|
||||||
* Server URL: misskey.example.com
|
|
||||||
* Misskey: 2024.x.x
|
|
||||||
value: |
|
|
||||||
* Model and OS of the device(s):
|
|
||||||
* Browser:
|
|
||||||
* Server URL:
|
|
||||||
* Misskey:
|
|
||||||
render: markdown
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: 🛰 Backend Environment (for server admin)
|
|
||||||
description: |
|
|
||||||
Tell us where on the platform it happens
|
|
||||||
DO NOT WRITE "latest". Please provide the specific version.
|
|
||||||
If you are using a managed service, put that after the version.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
* Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment
|
|
||||||
* Misskey: 2024.x.x
|
|
||||||
* Node: 20.x.x
|
|
||||||
* PostgreSQL: 15.x.x
|
|
||||||
* Redis: 7.x.x
|
|
||||||
* OS and Architecture: Ubuntu 24.04.2 LTS aarch64
|
|
||||||
value: |
|
|
||||||
* Installation Method or Hosting Service:
|
|
||||||
* Misskey:
|
|
||||||
* Node:
|
|
||||||
* PostgreSQL:
|
|
||||||
* Redis:
|
|
||||||
* OS and Architecture:
|
|
||||||
render: markdown
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: Do you want to address this bug yourself?
|
|
||||||
options:
|
|
||||||
- label: Yes, I will patch the bug myself and send a pull request
|
|
22
.github/ISSUE_TEMPLATE/02_feature-request.yml
vendored
22
.github/ISSUE_TEMPLATE/02_feature-request.yml
vendored
|
@ -1,22 +0,0 @@
|
||||||
name: ✨ Feature Request
|
|
||||||
description: Suggest an idea for this project
|
|
||||||
labels: ["✨Feature"]
|
|
||||||
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Summary
|
|
||||||
description: Tell us what the suggestion is
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Purpose
|
|
||||||
description: Describe the specific problem or need you think this feature will solve, and who it will help.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: Do you want to implement this feature yourself?
|
|
||||||
options:
|
|
||||||
- label: Yes, I will implement this by myself and send a pull request
|
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1,8 +0,0 @@
|
||||||
contact_links:
|
|
||||||
- name: 💬 Misskey official Discord
|
|
||||||
url: https://discord.gg/Wp8gVStHW3
|
|
||||||
about: Chat freely about Misskey
|
|
||||||
# 仮
|
|
||||||
- name: 💬 Start discussion
|
|
||||||
url: https://github.com/misskey-dev/misskey/discussions
|
|
||||||
about: The official forum to join conversation and ask question
|
|
23
.github/PULL_REQUEST_TEMPLATE/01_bug.md
vendored
23
.github/PULL_REQUEST_TEMPLATE/01_bug.md
vendored
|
@ -1,23 +0,0 @@
|
||||||
<!-- ℹ お読みください / README
|
|
||||||
PRありがとうございます! PRを作成する前に、コントリビューションガイドをご確認ください:
|
|
||||||
Thank you for your PR! Before creating a PR, please check the contribution guide:
|
|
||||||
https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md
|
|
||||||
-->
|
|
||||||
|
|
||||||
## What
|
|
||||||
<!-- このPRで何をしたのか? どう変わるのか? -->
|
|
||||||
<!-- What did you do with this PR? How will it change things? -->
|
|
||||||
|
|
||||||
## Why
|
|
||||||
<!-- なぜそうするのか? どういう意図なのか? 何が困っているのか? -->
|
|
||||||
<!-- Why do you do it? What are your intentions? What is the problem? -->
|
|
||||||
|
|
||||||
## Additional info (optional)
|
|
||||||
<!-- テスト観点など -->
|
|
||||||
<!-- Test perspective, etc -->
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md)
|
|
||||||
- [ ] Test working in a local environment
|
|
||||||
- [ ] (If needed) Update CHANGELOG.md
|
|
||||||
- [ ] (If possible) Add tests
|
|
23
.github/PULL_REQUEST_TEMPLATE/02_enhance.md
vendored
23
.github/PULL_REQUEST_TEMPLATE/02_enhance.md
vendored
|
@ -1,23 +0,0 @@
|
||||||
<!-- ℹ お読みください / README
|
|
||||||
PRありがとうございます! PRを作成する前に、コントリビューションガイドをご確認ください:
|
|
||||||
Thank you for your PR! Before creating a PR, please check the contribution guide:
|
|
||||||
https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md
|
|
||||||
-->
|
|
||||||
|
|
||||||
## What
|
|
||||||
<!-- このPRで何をしたのか? どう変わるのか? -->
|
|
||||||
<!-- What did you do with this PR? How will it change things? -->
|
|
||||||
|
|
||||||
## Why
|
|
||||||
<!-- なぜそうするのか? どういう意図なのか? 何が困っているのか? -->
|
|
||||||
<!-- Why do you do it? What are your intentions? What is the problem? -->
|
|
||||||
|
|
||||||
## Additional info (optional)
|
|
||||||
<!-- テスト観点など -->
|
|
||||||
<!-- Test perspective, etc -->
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md)
|
|
||||||
- [ ] Test working in a local environment
|
|
||||||
- [ ] (If needed) Update CHANGELOG.md
|
|
||||||
- [ ] (If possible) Add tests
|
|
20
.github/PULL_REQUEST_TEMPLATE/03_release.md
vendored
20
.github/PULL_REQUEST_TEMPLATE/03_release.md
vendored
|
@ -1,20 +0,0 @@
|
||||||
## Summary
|
|
||||||
This is a release PR.
|
|
||||||
|
|
||||||
For more information on the release instructions, please see:
|
|
||||||
https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md#release
|
|
||||||
|
|
||||||
## For reviewers
|
|
||||||
- CHANGELOGに抜け漏れは無いか
|
|
||||||
- バージョンの上げ方は適切か
|
|
||||||
- 他にこのリリースに含めなければならない変更は無いか
|
|
||||||
- 全体的な変更内容を俯瞰し問題は無いか
|
|
||||||
- レビューされていないコミットがある場合は、それが問題ないか
|
|
||||||
- 最終的な動作確認を行い問題は無いか
|
|
||||||
|
|
||||||
などを確認し、リリースする準備が整っていると思われる場合は approve してください。
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
- [ ] package.jsonのバージョンが正しく更新されている
|
|
||||||
- [ ] CHANGELOGが過不足無く更新されている
|
|
||||||
- [ ] CIが全て通っている
|
|
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
|
@ -1,7 +1,7 @@
|
||||||
<!-- ℹ お読みください / README
|
<!-- ℹ お読みください / README
|
||||||
PRありがとうございます! PRを作成する前に、コントリビューションガイドをご確認ください:
|
PRありがとうございます! PRを作成する前に、コントリビューションガイドをご確認ください:
|
||||||
Thank you for your PR! Before creating a PR, please check the contribution guide:
|
Thank you for your PR! Before creating a PR, please check the contribution guide:
|
||||||
https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md
|
https://forge.yumechi.jp/yume/yumechi-no-kuni/src/branch/master/CONTRIBUTING.md
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## What
|
## What
|
||||||
|
@ -17,7 +17,7 @@ https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md
|
||||||
<!-- Test perspective, etc -->
|
<!-- Test perspective, etc -->
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md)
|
- [ ] Read the [contribution guide](https://forge.yumechi.jp/yume/yumechi-no-kuni/src/branch/master/CONTRIBUTING.md)
|
||||||
- [ ] Test working in a local environment
|
- [ ] Test working in a local environment
|
||||||
- [ ] (If needed) Add story of storybook
|
- [ ] (If needed) Add story of storybook
|
||||||
- [ ] (If needed) Update CHANGELOG.md
|
- [ ] (If needed) Update CHANGELOG.md
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
build:
|
|
||||||
misskey:
|
|
||||||
args:
|
|
||||||
- NODE_ENV=development
|
|
||||||
deploy:
|
|
||||||
- helm upgrade --install misskey chart --set image=${OKTETO_BUILD_MISSKEY_IMAGE} --set url="https://misskey-$(kubectl config view --minify -o jsonpath='{..namespace}').cloud.okteto.net" --set environment=development
|
|
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -1,3 +1,23 @@
|
||||||
|
|
||||||
|
## 2024.11.0-yumechinokuni.8
|
||||||
|
|
||||||
|
- Frontend: SSRでユーザープロフィールが表示されない問題を修正
|
||||||
|
- Security: SSRプライバシー方面の改善
|
||||||
|
- Security: AP Payloadの検証を強化
|
||||||
|
|
||||||
|
## 2024.11.0-yumechinokuni.7
|
||||||
|
|
||||||
|
- Misskey Trademark内容をWebUIから削除
|
||||||
|
- Service Worker キャッシュが正しく動作しない問題を修正
|
||||||
|
|
||||||
|
## 2024.11.0-yumechinokuni.6
|
||||||
|
|
||||||
|
- Upstream: 2024.11.0-alpha.4 タッグをマージする
|
||||||
|
- Performance: EmojiのリクエストをProxyでキャッシュするように
|
||||||
|
- Performance: Service Workerのキャッシュを最適化
|
||||||
|
- Security: AP Payloadの検証を強化
|
||||||
|
- Security: Image/Video Processorはドライブ機能だけを使うように
|
||||||
|
|
||||||
## 2024.11.0-yumechinokuni.5
|
## 2024.11.0-yumechinokuni.5
|
||||||
|
|
||||||
- Upstream: 2024.11.0-alpha.2 タッグをマージする
|
- Upstream: 2024.11.0-alpha.2 タッグをマージする
|
||||||
|
@ -58,6 +78,7 @@ PgroongaのCWサーチ (github.com/paricafe/misskey#d30db97b59d264450901c1dd8680
|
||||||
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/663)
|
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/663)
|
||||||
- Enhance: サイドバーを簡単に展開・折りたたみできるように ( #14981 )
|
- Enhance: サイドバーを簡単に展開・折りたたみできるように ( #14981 )
|
||||||
- Enhance: リノートメニューに「リノートの詳細」を追加
|
- Enhance: リノートメニューに「リノートの詳細」を追加
|
||||||
|
- Enhance: 非ログイン状態でMisskeyを開いた際のパフォーマンスを向上
|
||||||
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
|
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
|
||||||
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
|
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
|
||||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
|
||||||
|
@ -92,6 +113,7 @@ PgroongaのCWサーチ (github.com/paricafe/misskey#d30db97b59d264450901c1dd8680
|
||||||
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
|
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
|
||||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
|
||||||
- Fix: User Webhookテスト機能のMock Payloadを修正
|
- Fix: User Webhookテスト機能のMock Payloadを修正
|
||||||
|
- Fix: アカウント削除のモデレーションログが動作していないのを修正 (#14996)
|
||||||
|
|
||||||
### Misskey.js
|
### Misskey.js
|
||||||
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正
|
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正
|
||||||
|
|
62
README.md
62
README.md
|
@ -1,49 +1,31 @@
|
||||||
<div align="center">
|
# ゆめちのくに
|
||||||
<a href="https://misskey-hub.net">
|
|
||||||
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="300"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
**🌎 **Misskey** is an open source, federated social media platform that's free forever! 🚀**
|
YumechiNoKuni is a fork of Misskey, with a focus on security, observability and reliability.
|
||||||
|
|
||||||
[Learn more](https://misskey-hub.net/)
|
[mi.yumechi.jp](https://mi.yumechi.jp) is running this version.
|
||||||
|
|
||||||
---
|
[Learn more about Misskey](https://misskey-hub.net/)
|
||||||
|
|
||||||
<a href="https://misskey-hub.net/servers/">
|
## Main differences
|
||||||
<img src="https://custom-icon-badges.herokuapp.com/badge/find_an-instance-acea31?logoColor=acea31&style=for-the-badge&logo=misskey&labelColor=363B40" alt="find an instance"/></a>
|
|
||||||
|
|
||||||
<a href="https://misskey-hub.net/docs/for-admin/install/guides/">
|
### Unique features
|
||||||
<img src="https://custom-icon-badges.herokuapp.com/badge/create_an-instance-FBD53C?logoColor=FBD53C&style=for-the-badge&logo=server&labelColor=363B40" alt="create an instance"/></a>
|
|
||||||
|
|
||||||
<a href="./CONTRIBUTING.md">
|
- Strict ActivityPub sanitization by whitelisting properties and normalizing all referential properties.
|
||||||
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-contributor-A371F7?logoColor=A371F7&style=for-the-badge&logo=git-merge&labelColor=363B40" alt="become a contributor"/></a>
|
- Strict Content Security Policy.
|
||||||
|
- Require TLSv1.2+ over port 443 for all ActivityPub requests.
|
||||||
|
- Strongly-typed inbox filtering in Rust.
|
||||||
|
- Reduce needless retries by marking more errors as permanent.
|
||||||
|
- Detailed prometheus metrics for slow requests, DB queries, AP processing, failed auths, etc.
|
||||||
|
- Disable unauthenticated media processing and use custom AppArmored media proxy.
|
||||||
|
- Enable active users in nodeinfo back.
|
||||||
|
- Advertise Git information over nodeinfo for better observability and easy tracking of the actual code running.
|
||||||
|
- Logical replication for the database over mTLS.
|
||||||
|
- More atomic operations in API handlers.
|
||||||
|
|
||||||
<a href="https://discord.gg/Wp8gVStHW3">
|
### Picked from github.com/paricafe/misskey
|
||||||
<img src="https://custom-icon-badges.herokuapp.com/badge/join_the-community-5865F2?logoColor=5865F2&style=for-the-badge&logo=discord&labelColor=363B40" alt="join the community"/></a>
|
|
||||||
|
|
||||||
<a href="https://www.patreon.com/syuilo">
|
- pgroonga full-text search (with modifications).
|
||||||
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a>
|
- Better Service Worker caching.
|
||||||
|
- Better hashtag statistics.
|
||||||
|
- Better handling of deep recursive AP objects.
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Thanks
|
|
||||||
|
|
||||||
<a href="https://sentry.io/"><img src="https://github.com/misskey-dev/misskey/assets/4439005/98576556-222f-467a-94be-e98dbda1d852" height="30" alt="Sentry" /></a>
|
|
||||||
|
|
||||||
Thanks to [Sentry](https://sentry.io/) for providing the error tracking platform that helps us catch unexpected errors.
|
|
||||||
|
|
||||||
<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" height="30" alt="Chromatic" /></a>
|
|
||||||
|
|
||||||
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.
|
|
||||||
|
|
||||||
<a href="https://about.codecov.io/for/open-source/"><img src="https://about.codecov.io/wp-content/themes/codecov/assets/brand/sentry-cobranding/logos/codecov-by-sentry-logo.svg" height="30" alt="Codecov" /></a>
|
|
||||||
|
|
||||||
Thanks to [Codecov](https://about.codecov.io/for/open-source/) for providing the code coverage platform that helps us improve our test coverage.
|
|
||||||
|
|
||||||
<a href="https://crowdin.com/"><img src="https://user-images.githubusercontent.com/20679825/230709597-1299a011-171a-4294-a91e-355a9b37c672.svg" height="30" alt="Crowdin" /></a>
|
|
||||||
|
|
||||||
Thanks to [Crowdin](https://crowdin.com/) for providing the localization platform that helps us translate Misskey into many languages.
|
|
||||||
|
|
||||||
<a href="https://hub.docker.com/"><img src="https://user-images.githubusercontent.com/20679825/230148221-f8e73a32-a49b-47c3-9029-9a15c3824f92.png" height="30" alt="Docker" /></a>
|
|
||||||
|
|
||||||
Thanks to [Docker](https://hub.docker.com/) for providing the container platform that helps us run Misskey in production.
|
|
||||||
|
|
13
SECURITY.md
13
SECURITY.md
|
@ -1,15 +1,12 @@
|
||||||
# Reporting Security Issues
|
# Reporting Security Issues
|
||||||
|
|
||||||
If you discover a security issue in Misskey, please report it by **[this form](https://github.com/misskey-dev/misskey/security/advisories/new)**.
|
If you discover a security issue in this project, please use the `git blame` command to identify the source of the issue,
|
||||||
|
if it was introduced by this fork please contact me at secity<at>yumechi.jp.
|
||||||
|
|
||||||
This will allow us to assess the risk, and make a fix available before we add a
|
For upstream issues please report by **[this form](https://github.com/misskey-dev/misskey/security/advisories/new)**.
|
||||||
bug report to the GitHub repository.
|
|
||||||
|
|
||||||
Thanks for helping make Misskey safe for everyone.
|
Thanks for helping make YumechiNoKuni safe for everyone.
|
||||||
|
|
||||||
## When create a patch
|
## When create a patch
|
||||||
|
|
||||||
If you can also create a patch to fix the vulnerability, please create a PR on the private fork.
|
If you can also create a patch to fix the vulnerability, please send a diff file with the report.
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> There is a GitHub bug that prevents merging if a PR not following the develop branch of upstream, so please keep follow the develop branch.
|
|
||||||
|
|
|
@ -586,6 +586,7 @@ masterVolume: "Volum principal"
|
||||||
notUseSound: "Sense so"
|
notUseSound: "Sense so"
|
||||||
useSoundOnlyWhenActive: "Reproduir sons només quan Misskey estigui actiu"
|
useSoundOnlyWhenActive: "Reproduir sons només quan Misskey estigui actiu"
|
||||||
details: "Detalls"
|
details: "Detalls"
|
||||||
|
renoteDetails: "Més informació sobre l'impuls "
|
||||||
chooseEmoji: "Tria un emoji"
|
chooseEmoji: "Tria un emoji"
|
||||||
unableToProcess: "L'operació no pot ser completada "
|
unableToProcess: "L'operació no pot ser completada "
|
||||||
recentUsed: "Utilitzat recentment"
|
recentUsed: "Utilitzat recentment"
|
||||||
|
|
|
@ -1242,6 +1242,7 @@ keepOriginalFilenameDescription: "Wenn diese Einstellung deaktiviert ist, wird d
|
||||||
noDescription: "Keine Beschreibung vorhanden"
|
noDescription: "Keine Beschreibung vorhanden"
|
||||||
tryAgain: "Bitte später erneut versuchen"
|
tryAgain: "Bitte später erneut versuchen"
|
||||||
confirmWhenRevealingSensitiveMedia: "Das Anzeigen von sensiblen Medien bestätigen"
|
confirmWhenRevealingSensitiveMedia: "Das Anzeigen von sensiblen Medien bestätigen"
|
||||||
|
sensitiveMediaRevealConfirm: "Es könnte sich um sensible Medien handeln. Möchtest du sie anzeigen?"
|
||||||
createdLists: "Erstellte Listen"
|
createdLists: "Erstellte Listen"
|
||||||
createdAntennas: "Erstellte Antennen"
|
createdAntennas: "Erstellte Antennen"
|
||||||
fromX: "Von {x}"
|
fromX: "Von {x}"
|
||||||
|
@ -1253,6 +1254,8 @@ thereAreNChanges: "Es gibt {n} Änderung(en)"
|
||||||
signinWithPasskey: "Mit Passkey anmelden"
|
signinWithPasskey: "Mit Passkey anmelden"
|
||||||
passkeyVerificationFailed: "Die Passkey-Verifizierung ist fehlgeschlagen."
|
passkeyVerificationFailed: "Die Passkey-Verifizierung ist fehlgeschlagen."
|
||||||
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Die Verifizierung des Passkeys war erfolgreich, aber die passwortlose Anmeldung ist deaktiviert."
|
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Die Verifizierung des Passkeys war erfolgreich, aber die passwortlose Anmeldung ist deaktiviert."
|
||||||
|
messageToFollower: "Nachricht an die Follower"
|
||||||
|
testCaptchaWarning: "Diese Funktion ist für CAPTCHA-Testzwecke gedacht.\n<strong>Nicht in einer Produktivumgebung verwenden.</strong>"
|
||||||
prohibitedWordsForNameOfUser: "Verbotene Begriffe für Benutzernamen"
|
prohibitedWordsForNameOfUser: "Verbotene Begriffe für Benutzernamen"
|
||||||
prohibitedWordsForNameOfUserDescription: "Wenn eine Zeichenfolge aus dieser Liste im Namen eines Benutzers enthalten ist, wird der Benutzername abgelehnt. Benutzer mit Moderatorenrechten sind von dieser Einschränkung nicht betroffen."
|
prohibitedWordsForNameOfUserDescription: "Wenn eine Zeichenfolge aus dieser Liste im Namen eines Benutzers enthalten ist, wird der Benutzername abgelehnt. Benutzer mit Moderatorenrechten sind von dieser Einschränkung nicht betroffen."
|
||||||
yourNameContainsProhibitedWords: "Dein Name enthält einen verbotenen Begriff"
|
yourNameContainsProhibitedWords: "Dein Name enthält einen verbotenen Begriff"
|
||||||
|
@ -1264,6 +1267,7 @@ _accountSettings:
|
||||||
requireSigninToViewContentsDescription1: "Erfordere eine Anmeldung, um alle Notizen und andere Inhalte anzuzeigen, die du erstellt hast. Dadurch wird verhindert, dass Crawler deine Informationen sammeln."
|
requireSigninToViewContentsDescription1: "Erfordere eine Anmeldung, um alle Notizen und andere Inhalte anzuzeigen, die du erstellt hast. Dadurch wird verhindert, dass Crawler deine Informationen sammeln."
|
||||||
requireSigninToViewContentsDescription3: "Diese Einschränkungen gelten möglicherweise nicht für föderierte Inhalte von anderen Servern."
|
requireSigninToViewContentsDescription3: "Diese Einschränkungen gelten möglicherweise nicht für föderierte Inhalte von anderen Servern."
|
||||||
makeNotesFollowersOnlyBefore: "Macht frühere Notizen nur für Follower sichtbar"
|
makeNotesFollowersOnlyBefore: "Macht frühere Notizen nur für Follower sichtbar"
|
||||||
|
makeNotesHiddenBefore: "Frühere Notizen privat machen"
|
||||||
mayNotEffectForFederatedNotes: "Dies hat möglicherweise keine Auswirkungen auf Notizen, die an andere Server föderiert werden."
|
mayNotEffectForFederatedNotes: "Dies hat möglicherweise keine Auswirkungen auf Notizen, die an andere Server föderiert werden."
|
||||||
_abuseUserReport:
|
_abuseUserReport:
|
||||||
forward: "Weiterleiten"
|
forward: "Weiterleiten"
|
||||||
|
@ -1274,6 +1278,7 @@ _delivery:
|
||||||
stop: "Gesperrt"
|
stop: "Gesperrt"
|
||||||
_type:
|
_type:
|
||||||
none: "Wird veröffentlicht"
|
none: "Wird veröffentlicht"
|
||||||
|
manuallySuspended: "Manuell gesperrt"
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "Wie man spielt"
|
howToPlay: "Wie man spielt"
|
||||||
hold: "Halten"
|
hold: "Halten"
|
||||||
|
|
|
@ -586,6 +586,7 @@ masterVolume: "Master volume"
|
||||||
notUseSound: "Disable sound"
|
notUseSound: "Disable sound"
|
||||||
useSoundOnlyWhenActive: "Output sounds only if Misskey is active."
|
useSoundOnlyWhenActive: "Output sounds only if Misskey is active."
|
||||||
details: "Details"
|
details: "Details"
|
||||||
|
renoteDetails: "Renote details"
|
||||||
chooseEmoji: "Select an emoji"
|
chooseEmoji: "Select an emoji"
|
||||||
unableToProcess: "The operation could not be completed"
|
unableToProcess: "The operation could not be completed"
|
||||||
recentUsed: "Recently used"
|
recentUsed: "Recently used"
|
||||||
|
|
|
@ -586,6 +586,7 @@ masterVolume: "마스터 볼륨"
|
||||||
notUseSound: "음소거 하기"
|
notUseSound: "음소거 하기"
|
||||||
useSoundOnlyWhenActive: "Misskey를 활성화한 때에만 소리를 출력하기"
|
useSoundOnlyWhenActive: "Misskey를 활성화한 때에만 소리를 출력하기"
|
||||||
details: "자세히"
|
details: "자세히"
|
||||||
|
renoteDetails: "리노트 상세 내용"
|
||||||
chooseEmoji: "이모지 선택"
|
chooseEmoji: "이모지 선택"
|
||||||
unableToProcess: "작업을 완료할 수 없습니다"
|
unableToProcess: "작업을 완료할 수 없습니다"
|
||||||
recentUsed: "최근 사용"
|
recentUsed: "최근 사용"
|
||||||
|
@ -1299,6 +1300,7 @@ thisContentsAreMarkedAsSigninRequiredByAuthor: "게시자에 의해 로그인해
|
||||||
lockdown: "잠금"
|
lockdown: "잠금"
|
||||||
pleaseSelectAccount: "계정을 선택해주세요."
|
pleaseSelectAccount: "계정을 선택해주세요."
|
||||||
availableRoles: "사용 가능한 역할"
|
availableRoles: "사용 가능한 역할"
|
||||||
|
acknowledgeNotesAndEnable: "활성화 하기 전에 주의 사항을 확인했습니다."
|
||||||
_accountSettings:
|
_accountSettings:
|
||||||
requireSigninToViewContents: "콘텐츠 열람을 위해 로그인으 필수로 설정하기"
|
requireSigninToViewContents: "콘텐츠 열람을 위해 로그인으 필수로 설정하기"
|
||||||
requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다."
|
requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다."
|
||||||
|
@ -1455,6 +1457,8 @@ _serverSettings:
|
||||||
reactionsBufferingDescription: "활성화 한 경우, 리액션 작성 퍼포먼스가 대폭 향상되어 DB의 부하를 줄일 수 있으나, Redis의 메모리 사용량이 많아집니다."
|
reactionsBufferingDescription: "활성화 한 경우, 리액션 작성 퍼포먼스가 대폭 향상되어 DB의 부하를 줄일 수 있으나, Redis의 메모리 사용량이 많아집니다."
|
||||||
inquiryUrl: "문의처 URL"
|
inquiryUrl: "문의처 URL"
|
||||||
inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다."
|
inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다."
|
||||||
|
openRegistration: "회원 가입을 활성화 하기"
|
||||||
|
openRegistrationWarning: "회원 가입을 개방하는 것은 리스크가 따릅니다. 서버를 항상 감시할 수 있고, 문제가 발생했을 때 바로 대응할 수 있는 상태에서만 활성화 하는 것을 권장합니다."
|
||||||
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "일정 기간동안 모더레이터의 활동이 감지되지 않는 경우, 스팸 방지를 위해 이 설정은 자동으로 꺼집니다."
|
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "일정 기간동안 모더레이터의 활동이 감지되지 않는 경우, 스팸 방지를 위해 이 설정은 자동으로 꺼집니다."
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "다른 계정에서 이 계정으로 이사"
|
moveFrom: "다른 계정에서 이 계정으로 이사"
|
||||||
|
@ -2737,3 +2741,6 @@ _selfXssPrevention:
|
||||||
description1: "여기에 무언가를 붙여넣으면 악의적인 사용자에게 계정을 탈취당하거나 개인정보를 도용당할 수 있습니다."
|
description1: "여기에 무언가를 붙여넣으면 악의적인 사용자에게 계정을 탈취당하거나 개인정보를 도용당할 수 있습니다."
|
||||||
description2: "붙여 넣으려는 항목이 무엇인지 정확히 이해하지 못하는 경우, %c지금 바로 작업을 중단하고 이 창을 닫으십시오."
|
description2: "붙여 넣으려는 항목이 무엇인지 정확히 이해하지 못하는 경우, %c지금 바로 작업을 중단하고 이 창을 닫으십시오."
|
||||||
description3: "자세한 내용은 여기를 확인해 주세요. {link}"
|
description3: "자세한 내용은 여기를 확인해 주세요. {link}"
|
||||||
|
_followRequest:
|
||||||
|
recieved: "받은 신청"
|
||||||
|
sent: "보낸 신청"
|
||||||
|
|
|
@ -1707,9 +1707,9 @@ _achievements:
|
||||||
description: "在元旦登入"
|
description: "在元旦登入"
|
||||||
flavor: "今年也请对本服务器多多指教!"
|
flavor: "今年也请对本服务器多多指教!"
|
||||||
_cookieClicked:
|
_cookieClicked:
|
||||||
title: "点击饼干小游戏"
|
title: "饼干点点乐"
|
||||||
description: "点击了饼干"
|
description: "点击了饼干"
|
||||||
flavor: "用错软件了?"
|
flavor: "穿越了?"
|
||||||
_brainDiver:
|
_brainDiver:
|
||||||
title: "Brain Diver"
|
title: "Brain Diver"
|
||||||
description: "发布了包含 Brain Diver 链接的帖子"
|
description: "发布了包含 Brain Diver 链接的帖子"
|
||||||
|
|
|
@ -586,6 +586,7 @@ masterVolume: "主音量"
|
||||||
notUseSound: "關閉音效"
|
notUseSound: "關閉音效"
|
||||||
useSoundOnlyWhenActive: "瀏覽器在前景運作時,Misskey 才會發出音效"
|
useSoundOnlyWhenActive: "瀏覽器在前景運作時,Misskey 才會發出音效"
|
||||||
details: "詳細資訊"
|
details: "詳細資訊"
|
||||||
|
renoteDetails: "轉發貼文的細節"
|
||||||
chooseEmoji: "選擇您的表情符號"
|
chooseEmoji: "選擇您的表情符號"
|
||||||
unableToProcess: "操作無法完成"
|
unableToProcess: "操作無法完成"
|
||||||
recentUsed: "最近使用"
|
recentUsed: "最近使用"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2024.11.0-yumechinokuni.5",
|
"version": "2024.11.0-yumechinokuni.8",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
16
packages/backend/migration/1732071810971-IndexUserDeleted.js
Normal file
16
packages/backend/migration/1732071810971-IndexUserDeleted.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project and yumechi
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class IndexUserDeleted1732071810971 {
|
||||||
|
name = 'IndexUserDeleted1732071810971'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_199b79e682bdc5ba946f491686" ON "user" ("isDeleted")`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_199b79e682bdc5ba946f491686"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,7 +36,7 @@ import { GlobalEventService } from './GlobalEventService.js';
|
||||||
import { HashtagService } from './HashtagService.js';
|
import { HashtagService } from './HashtagService.js';
|
||||||
import { HttpRequestService } from './HttpRequestService.js';
|
import { HttpRequestService } from './HttpRequestService.js';
|
||||||
import { IdService } from './IdService.js';
|
import { IdService } from './IdService.js';
|
||||||
import { ImageProcessingService } from './ImageProcessingService.js';
|
import { __YUME_PRIVATE_ImageProcessingService } from './ImageProcessingService.js';
|
||||||
import { InstanceActorService } from './InstanceActorService.js';
|
import { InstanceActorService } from './InstanceActorService.js';
|
||||||
import { InternalStorageService } from './InternalStorageService.js';
|
import { InternalStorageService } from './InternalStorageService.js';
|
||||||
import { MetaService } from './MetaService.js';
|
import { MetaService } from './MetaService.js';
|
||||||
|
@ -67,7 +67,7 @@ import { UserMutingService } from './UserMutingService.js';
|
||||||
import { UserRenoteMutingService } from './UserRenoteMutingService.js';
|
import { UserRenoteMutingService } from './UserRenoteMutingService.js';
|
||||||
import { UserSuspendService } from './UserSuspendService.js';
|
import { UserSuspendService } from './UserSuspendService.js';
|
||||||
import { UserAuthService } from './UserAuthService.js';
|
import { UserAuthService } from './UserAuthService.js';
|
||||||
import { VideoProcessingService } from './VideoProcessingService.js';
|
import { __YUME_PRIVATE_VideoProcessingService } from './VideoProcessingService.js';
|
||||||
import { UserWebhookService } from './UserWebhookService.js';
|
import { UserWebhookService } from './UserWebhookService.js';
|
||||||
import { ProxyAccountService } from './ProxyAccountService.js';
|
import { ProxyAccountService } from './ProxyAccountService.js';
|
||||||
import { UtilityService } from './UtilityService.js';
|
import { UtilityService } from './UtilityService.js';
|
||||||
|
@ -179,7 +179,7 @@ const $GlobalEventService: Provider = { provide: 'GlobalEventService', useExisti
|
||||||
const $HashtagService: Provider = { provide: 'HashtagService', useExisting: HashtagService };
|
const $HashtagService: Provider = { provide: 'HashtagService', useExisting: HashtagService };
|
||||||
const $HttpRequestService: Provider = { provide: 'HttpRequestService', useExisting: HttpRequestService };
|
const $HttpRequestService: Provider = { provide: 'HttpRequestService', useExisting: HttpRequestService };
|
||||||
const $IdService: Provider = { provide: 'IdService', useExisting: IdService };
|
const $IdService: Provider = { provide: 'IdService', useExisting: IdService };
|
||||||
const $ImageProcessingService: Provider = { provide: 'ImageProcessingService', useExisting: ImageProcessingService };
|
const $ImageProcessingService: Provider = { provide: '__YUME_PRIVATE_ImageProcessingService', useExisting: __YUME_PRIVATE_ImageProcessingService };
|
||||||
const $InstanceActorService: Provider = { provide: 'InstanceActorService', useExisting: InstanceActorService };
|
const $InstanceActorService: Provider = { provide: 'InstanceActorService', useExisting: InstanceActorService };
|
||||||
const $InternalStorageService: Provider = { provide: 'InternalStorageService', useExisting: InternalStorageService };
|
const $InternalStorageService: Provider = { provide: 'InternalStorageService', useExisting: InternalStorageService };
|
||||||
const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaService };
|
const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaService };
|
||||||
|
@ -212,7 +212,7 @@ const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService',
|
||||||
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
|
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
|
||||||
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||||
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
||||||
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
const $VideoProcessingService: Provider = { provide: '__YUME_PRIVATE_VideoProcessingService', useExisting: __YUME_PRIVATE_VideoProcessingService };
|
||||||
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
|
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
|
||||||
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
|
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
|
||||||
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
|
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
|
||||||
|
@ -330,7 +330,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
HashtagService,
|
HashtagService,
|
||||||
HttpRequestService,
|
HttpRequestService,
|
||||||
IdService,
|
IdService,
|
||||||
ImageProcessingService,
|
__YUME_PRIVATE_ImageProcessingService,
|
||||||
InstanceActorService,
|
InstanceActorService,
|
||||||
InternalStorageService,
|
InternalStorageService,
|
||||||
MetaService,
|
MetaService,
|
||||||
|
@ -363,7 +363,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
UserSearchService,
|
UserSearchService,
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
UserAuthService,
|
UserAuthService,
|
||||||
VideoProcessingService,
|
__YUME_PRIVATE_VideoProcessingService,
|
||||||
UserWebhookService,
|
UserWebhookService,
|
||||||
SystemWebhookService,
|
SystemWebhookService,
|
||||||
WebhookTestService,
|
WebhookTestService,
|
||||||
|
@ -625,7 +625,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
HashtagService,
|
HashtagService,
|
||||||
HttpRequestService,
|
HttpRequestService,
|
||||||
IdService,
|
IdService,
|
||||||
ImageProcessingService,
|
__YUME_PRIVATE_ImageProcessingService,
|
||||||
InstanceActorService,
|
InstanceActorService,
|
||||||
InternalStorageService,
|
InternalStorageService,
|
||||||
MetaService,
|
MetaService,
|
||||||
|
@ -658,7 +658,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
UserSearchService,
|
UserSearchService,
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
UserAuthService,
|
UserAuthService,
|
||||||
VideoProcessingService,
|
__YUME_PRIVATE_VideoProcessingService,
|
||||||
UserWebhookService,
|
UserWebhookService,
|
||||||
SystemWebhookService,
|
SystemWebhookService,
|
||||||
WebhookTestService,
|
WebhookTestService,
|
||||||
|
|
|
@ -47,6 +47,10 @@ export class DeleteAccountService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await this.usersRepository.update({ id: user.id, isDeleted: false }, { isDeleted: true })).affected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 物理削除する前にDelete activityを送信する
|
// 物理削除する前にDelete activityを送信する
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
// 知り得る全SharedInboxにDelete配信
|
// 知り得る全SharedInboxにDelete配信
|
||||||
|
|
|
@ -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';
|
||||||
|
@ -45,6 +44,14 @@ export class DownloadService {
|
||||||
const maxSize = this.config.maxFileSize;
|
const maxSize = this.config.maxFileSize;
|
||||||
|
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
|
if (urlObj.protocol && urlObj.protocol !== 'https:') {
|
||||||
|
throw new Error(`Unsupported protocol: ${urlObj.protocol}, only HTTPS is supported`);
|
||||||
|
}
|
||||||
|
urlObj.protocol = 'https:';
|
||||||
|
if (urlObj.port && urlObj.port !== '443') {
|
||||||
|
throw new Error(`Unsupported port: ${urlObj.port}, only 443 is supported`);
|
||||||
|
}
|
||||||
|
|
||||||
let filename = urlObj.pathname.split('/').pop() ?? 'untitled';
|
let filename = urlObj.pathname.split('/').pop() ?? 'untitled';
|
||||||
|
|
||||||
const req = got.stream(url, {
|
const req = got.stream(url, {
|
||||||
|
@ -61,7 +68,6 @@ export class DownloadService {
|
||||||
request: operationTimeout, // whole operation timeout
|
request: operationTimeout, // whole operation timeout
|
||||||
},
|
},
|
||||||
agent: {
|
agent: {
|
||||||
http: this.httpRequestService.httpAgent,
|
|
||||||
https: this.httpRequestService.httpsAgent,
|
https: this.httpRequestService.httpsAgent,
|
||||||
},
|
},
|
||||||
http2: false, // default
|
http2: false, // default
|
||||||
|
@ -70,13 +76,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 +138,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';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,8 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { contentDisposition } from '@/misc/content-disposition.js';
|
import { contentDisposition } from '@/misc/content-disposition.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { VideoProcessingService } from '@/core/VideoProcessingService.js';
|
import { __YUME_PRIVATE_VideoProcessingService } from '@/core/VideoProcessingService.js';
|
||||||
import { ImageProcessingService } from '@/core/ImageProcessingService.js';
|
import { __YUME_PRIVATE_ImageProcessingService } from '@/core/ImageProcessingService.js';
|
||||||
import type { IImage } from '@/core/ImageProcessingService.js';
|
import type { IImage } from '@/core/ImageProcessingService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import type { MiDriveFolder } from '@/models/DriveFolder.js';
|
import type { MiDriveFolder } from '@/models/DriveFolder.js';
|
||||||
|
@ -120,8 +120,8 @@ export class DriveService {
|
||||||
private downloadService: DownloadService,
|
private downloadService: DownloadService,
|
||||||
private internalStorageService: InternalStorageService,
|
private internalStorageService: InternalStorageService,
|
||||||
private s3Service: S3Service,
|
private s3Service: S3Service,
|
||||||
private imageProcessingService: ImageProcessingService,
|
private privateImageProcessingService: __YUME_PRIVATE_ImageProcessingService,
|
||||||
private videoProcessingService: VideoProcessingService,
|
private privateVideoProcessingService: __YUME_PRIVATE_VideoProcessingService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
|
@ -277,7 +277,7 @@ export class DriveService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const thumbnail = await this.videoProcessingService.generateVideoThumbnail(path);
|
const thumbnail = await this.privateVideoProcessingService.generateVideoThumbnail(path);
|
||||||
return {
|
return {
|
||||||
webpublic: null,
|
webpublic: null,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
|
@ -331,9 +331,9 @@ export class DriveService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) {
|
if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) {
|
||||||
webpublic = await this.imageProcessingService.convertSharpToWebp(img, 2048, 2048);
|
webpublic = await this.privateImageProcessingService.convertSharpToWebp(img, 2048, 2048);
|
||||||
} else if (['image/png', 'image/bmp', 'image/svg+xml'].includes(type)) {
|
} else if (['image/png', 'image/bmp', 'image/svg+xml'].includes(type)) {
|
||||||
webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
|
webpublic = await this.privateImageProcessingService.convertSharpToPng(img, 2048, 2048);
|
||||||
} else {
|
} else {
|
||||||
this.registerLogger.debug('web image not created (not an required image)');
|
this.registerLogger.debug('web image not created (not an required image)');
|
||||||
}
|
}
|
||||||
|
@ -352,9 +352,9 @@ export class DriveService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isAnimated) {
|
if (isAnimated) {
|
||||||
thumbnail = await this.imageProcessingService.convertSharpToWebp(sharp(path, { animated: true }), 374, 317, { alphaQuality: 70 });
|
thumbnail = await this.privateImageProcessingService.convertSharpToWebp(sharp(path, { animated: true }), 374, 317, { alphaQuality: 70 });
|
||||||
} else {
|
} else {
|
||||||
thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 422);
|
thumbnail = await this.privateImageProcessingService.convertSharpToWebp(img, 498, 422);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.registerLogger.warn('thumbnail not created (an error occurred)', err as Error);
|
this.registerLogger.warn('thumbnail not created (an error occurred)', err as Error);
|
||||||
|
|
|
@ -313,6 +313,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,9 +6,10 @@
|
||||||
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 { HttpsProxyAgent } from 'hpagent';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
@ -18,30 +19,109 @@ import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/val
|
||||||
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
|
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
|
||||||
import type { IObject } from '@/core/activitypub/type.js';
|
import type { IObject } from '@/core/activitypub/type.js';
|
||||||
import type { Response } from 'node-fetch';
|
import type { Response } from 'node-fetch';
|
||||||
import type { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
|
|
||||||
export type HttpRequestSendOptions = {
|
export type HttpRequestSendOptions = {
|
||||||
throwErrorWhenResponseNotOk: boolean;
|
throwErrorWhenResponseNotOk: boolean;
|
||||||
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
|
* Get https non-proxy agent (without local address filtering)
|
||||||
*/
|
*/
|
||||||
private http: http.Agent;
|
private httpsNative: https.Agent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get https non-proxy agent
|
* Get https non-proxy agent
|
||||||
*/
|
*/
|
||||||
private https: https.Agent;
|
private https: https.Agent;
|
||||||
|
|
||||||
/**
|
|
||||||
* Get http proxy or non-proxy agent
|
|
||||||
*/
|
|
||||||
public httpAgent: http.Agent;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get https proxy or non-proxy agent
|
* Get https proxy or non-proxy agent
|
||||||
*/
|
*/
|
||||||
|
@ -57,34 +137,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,
|
||||||
});
|
minVersion: 'TLSv1.2' as const,
|
||||||
|
};
|
||||||
|
|
||||||
this.https = new https.Agent({
|
this.httpsNative = new https.Agent(agentOption);
|
||||||
keepAlive: true,
|
|
||||||
keepAliveMsecs: 30 * 1000,
|
this.https = new HttpsRequestServiceAgent(config, agentOption);
|
||||||
lookup: cache.lookup as unknown as net.LookupFunction,
|
|
||||||
localAddress: config.outgoingAddress,
|
|
||||||
});
|
|
||||||
|
|
||||||
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
||||||
|
|
||||||
this.httpAgent = config.proxy
|
|
||||||
? new HttpProxyAgent({
|
|
||||||
keepAlive: true,
|
|
||||||
keepAliveMsecs: 30 * 1000,
|
|
||||||
maxSockets,
|
|
||||||
maxFreeSockets: 256,
|
|
||||||
scheduling: 'lifo',
|
|
||||||
proxy: config.proxy,
|
|
||||||
localAddress: config.outgoingAddress,
|
|
||||||
})
|
|
||||||
: this.http;
|
|
||||||
|
|
||||||
this.httpsAgent = config.proxy
|
this.httpsAgent = config.proxy
|
||||||
? new HttpsProxyAgent({
|
? new HttpsProxyAgent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
|
@ -104,16 +170,23 @@ 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): https.Agent {
|
||||||
|
if (url.protocol && url.protocol !== 'https:') {
|
||||||
|
throw new Error('Invalid protocol');
|
||||||
|
}
|
||||||
|
url.protocol = 'https:';
|
||||||
|
if (url.port && url.port !== '443') {
|
||||||
|
throw new Error('Invalid port');
|
||||||
|
}
|
||||||
if (bypassProxy || (this.config.proxyBypassHosts ?? []).includes(url.hostname)) {
|
if (bypassProxy || (this.config.proxyBypassHosts ?? []).includes(url.hostname)) {
|
||||||
return url.protocol === 'http:' ? this.http : this.https;
|
return this.https;
|
||||||
} else {
|
} else {
|
||||||
return url.protocol === 'http:' ? this.httpAgent : this.httpsAgent;
|
return 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 +194,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 +203,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 +217,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 +246,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 +260,16 @@ export class HttpRequestService {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
|
const urlParsed = new URL(url);
|
||||||
|
|
||||||
|
if (urlParsed.protocol !== 'https:') {
|
||||||
|
throw new Error('Invalid protocol');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlParsed.port && urlParsed.port !== '443') {
|
||||||
|
throw new Error('Invalid port');
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: args.method ?? 'GET',
|
method: args.method ?? 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -191,7 +278,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),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,9 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImageProcessingService {
|
// Prevent accidental import by upstream merge
|
||||||
|
// eslint-disable-next-line
|
||||||
|
export class __YUME_PRIVATE_ImageProcessingService {
|
||||||
constructor(
|
constructor(
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { setImmediate } from 'node:timers/promises';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import { In, DataSource, IsNull, LessThan } from 'typeorm';
|
import { In, DataSource, IsNull, LessThan } from 'typeorm';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
|
import * as Bull from 'bullmq';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { extractMentions } from '@/misc/extract-mentions.js';
|
import { extractMentions } from '@/misc/extract-mentions.js';
|
||||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||||
|
@ -56,6 +57,7 @@ import { isReply } from '@/misc/is-reply.js';
|
||||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
|
@ -217,6 +219,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private userBlockingService: UserBlockingService,
|
private userBlockingService: UserBlockingService,
|
||||||
|
private cacheService: CacheService,
|
||||||
) {
|
) {
|
||||||
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
|
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
|
||||||
}
|
}
|
||||||
|
@ -291,7 +294,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
case 'followers':
|
case 'followers':
|
||||||
// 他人のfollowers noteはreject
|
// 他人のfollowers noteはreject
|
||||||
if (data.renote.userId !== user.id) {
|
if (data.renote.userId !== user.id) {
|
||||||
throw new Error('Renote target is not public or home');
|
throw new Bull.UnrecoverableError('Renote target is not public or home');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renote対象がfollowersならfollowersにする
|
// Renote対象がfollowersならfollowersにする
|
||||||
|
@ -299,7 +302,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
break;
|
break;
|
||||||
case 'specified':
|
case 'specified':
|
||||||
// specified / direct noteはreject
|
// specified / direct noteはreject
|
||||||
throw new Error('Renote target is not public or home');
|
throw new Bull.UnrecoverableError('Renote target is not public or home');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -543,15 +546,23 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
this.followingsRepository.findBy({
|
this.followingsRepository.findBy({
|
||||||
followeeId: user.id,
|
followeeId: user.id,
|
||||||
notify: 'normal',
|
notify: 'normal',
|
||||||
}).then(followings => {
|
}).then(async followings => {
|
||||||
if (note.visibility !== 'specified') {
|
if (note.visibility !== 'specified') {
|
||||||
|
const isPureRenote = this.isRenote(data) && !this.isQuote(data) ? true : false;
|
||||||
for (const following of followings) {
|
for (const following of followings) {
|
||||||
// TODO: ワードミュート考慮
|
// TODO: ワードミュート考慮
|
||||||
|
let isRenoteMuted = false;
|
||||||
|
if (isPureRenote) {
|
||||||
|
const userIdsWhoMeMutingRenotes = await this.cacheService.renoteMutingsCache.fetch(following.followerId);
|
||||||
|
isRenoteMuted = userIdsWhoMeMutingRenotes.has(user.id);
|
||||||
|
}
|
||||||
|
if (!isRenoteMuted) {
|
||||||
this.notificationService.createNotification(following.followerId, 'note', {
|
this.notificationService.createNotification(following.followerId, 'note', {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
}, user.id);
|
}, user.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { RemoteLoggerService } from '@/core/RemoteLoggerService.js';
|
||||||
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
||||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { ApResolverService } from './activitypub/ApResolverService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RemoteUserResolveService {
|
export class RemoteUserResolveService {
|
||||||
|
@ -35,6 +36,7 @@ export class RemoteUserResolveService {
|
||||||
private remoteLoggerService: RemoteLoggerService,
|
private remoteLoggerService: RemoteLoggerService,
|
||||||
private apDbResolverService: ApDbResolverService,
|
private apDbResolverService: ApDbResolverService,
|
||||||
private apPersonService: ApPersonService,
|
private apPersonService: ApPersonService,
|
||||||
|
private apResolverService: ApResolverService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.remoteLoggerService.logger.createSubLogger('resolve-user');
|
this.logger = this.remoteLoggerService.logger.createSubLogger('resolve-user');
|
||||||
}
|
}
|
||||||
|
@ -91,7 +93,7 @@ export class RemoteUserResolveService {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
|
this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
|
||||||
return await this.apPersonService.createPerson(self.href);
|
return await this.apPersonService.createPerson(self.href, this.apResolverService.createResolver());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ユーザー情報が古い場合は、WebFingerからやりなおして返す
|
// ユーザー情報が古い場合は、WebFingerからやりなおして返す
|
||||||
|
|
|
@ -488,6 +488,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
return ids.length > 0
|
return ids.length > 0
|
||||||
? await this.usersRepository.findBy({
|
? await this.usersRepository.findBy({
|
||||||
id: In(ids),
|
id: In(ids),
|
||||||
|
isDeleted: false,
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,19 +7,21 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import FFmpeg from 'fluent-ffmpeg';
|
import FFmpeg from 'fluent-ffmpeg';
|
||||||
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 { ImageProcessingService } from '@/core/ImageProcessingService.js';
|
import { __YUME_PRIVATE_ImageProcessingService } from '@/core/ImageProcessingService.js';
|
||||||
import type { IImage } from '@/core/ImageProcessingService.js';
|
import type { IImage } from '@/core/ImageProcessingService.js';
|
||||||
import { createTempDir } from '@/misc/create-temp.js';
|
import { createTempDir } from '@/misc/create-temp.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { appendQuery, query } from '@/misc/prelude/url.js';
|
import { appendQuery, query } from '@/misc/prelude/url.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VideoProcessingService {
|
// Prevent accidental import by upstream merge
|
||||||
|
// eslint-disable-next-line
|
||||||
|
export class __YUME_PRIVATE_VideoProcessingService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
private imageProcessingService: ImageProcessingService,
|
private imageProcessingService: __YUME_PRIVATE_ImageProcessingService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,11 @@ export class WebAuthnService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async verifyRegistration(userId: MiUser['id'], response: RegistrationResponseJSON): Promise<{
|
public async verifyRegistration(
|
||||||
|
userId: MiUser['id'],
|
||||||
|
response: RegistrationResponseJSON,
|
||||||
|
twoFactorOnly: boolean = false,
|
||||||
|
): Promise<{
|
||||||
credentialID: string;
|
credentialID: string;
|
||||||
credentialPublicKey: Uint8Array;
|
credentialPublicKey: Uint8Array;
|
||||||
attestationObject: Uint8Array;
|
attestationObject: Uint8Array;
|
||||||
|
@ -111,7 +115,7 @@ export class WebAuthnService {
|
||||||
expectedChallenge: challenge,
|
expectedChallenge: challenge,
|
||||||
expectedOrigin: relyingParty.origin,
|
expectedOrigin: relyingParty.origin,
|
||||||
expectedRPID: relyingParty.rpId,
|
expectedRPID: relyingParty.rpId,
|
||||||
requireUserVerification: true,
|
requireUserVerification: !twoFactorOnly,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -245,7 +249,11 @@ export class WebAuthnService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise<boolean> {
|
public async verifyAuthentication(
|
||||||
|
userId: MiUser['id'],
|
||||||
|
response: AuthenticationResponseJSON,
|
||||||
|
twoFactorOnly: boolean = false,
|
||||||
|
): Promise<boolean> {
|
||||||
const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`);
|
const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`);
|
||||||
|
|
||||||
if (!challenge) {
|
if (!challenge) {
|
||||||
|
@ -302,7 +310,7 @@ export class WebAuthnService {
|
||||||
counter: key.counter,
|
counter: key.counter,
|
||||||
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
|
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
|
||||||
},
|
},
|
||||||
requireUserVerification: true,
|
requireUserVerification: !twoFactorOnly,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||||
import { getApId } from './type.js';
|
import { getApId } from './type.js';
|
||||||
import { ApPersonService } from './models/ApPersonService.js';
|
import { ApPersonService } from './models/ApPersonService.js';
|
||||||
import type { IObject } from './type.js';
|
import type { IObject } from './type.js';
|
||||||
|
import { toASCII } from 'node:punycode';
|
||||||
|
|
||||||
export type UriParseResult = {
|
export type UriParseResult = {
|
||||||
/** wether the URI was generated by us */
|
/** wether the URI was generated by us */
|
||||||
|
@ -63,7 +64,9 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||||
const separator = '/';
|
const separator = '/';
|
||||||
|
|
||||||
const uri = new URL(getApId(value));
|
const uri = new URL(getApId(value));
|
||||||
if (uri.origin !== this.config.url) return { local: false, uri: uri.href };
|
if (toASCII(uri.host) !== toASCII(this.config.host)) {
|
||||||
|
return { local: false, uri: uri.href };
|
||||||
|
}
|
||||||
|
|
||||||
const [, type, id, ...rest] = uri.pathname.split(separator);
|
const [, type, id, ...rest] = uri.pathname.split(separator);
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -29,7 +29,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
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 { 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 { getApHrefNullable, getApId, getApIds, getApType, isActor, isCollection, isCollectionOrOrderedCollection, isPost, isTombstone, validActor, validPost, yumeDowncastAccept, yumeDowncastAdd, yumeDowncastAnnounce, yumeDowncastBlock, yumeDowncastCreate, yumeDowncastDelete, yumeDowncastFlag, yumeDowncastFollow, yumeDowncastLike, yumeDowncastMove, yumeDowncastReject, yumeDowncastRemove, yumeDowncastUndo, yumeDowncastUpdate } 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';
|
||||||
|
@ -138,53 +138,93 @@ export class ApInboxService {
|
||||||
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
|
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
|
||||||
if (actor.isSuspended) return;
|
if (actor.isSuspended) return;
|
||||||
|
|
||||||
if (isCreate(activity)) {
|
const create = yumeDowncastCreate(activity);
|
||||||
|
if (create) {
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'create' });
|
mInboxReceived?.inc({ host: actor.host, type: 'create' });
|
||||||
return await this.create(actor, activity);
|
return await this.create(actor, create);
|
||||||
} else if (isDelete(activity)) {
|
}
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'delete' });
|
|
||||||
return await this.delete(actor, activity);
|
const update = yumeDowncastUpdate(activity);
|
||||||
} else if (isUpdate(activity)) {
|
if (update) {
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'update' });
|
mInboxReceived?.inc({ host: actor.host, type: 'update' });
|
||||||
return await this.update(actor, activity);
|
return await this.update(actor, update);
|
||||||
} else if (isFollow(activity)) {
|
}
|
||||||
|
|
||||||
|
const del = yumeDowncastDelete(activity);
|
||||||
|
if (del) {
|
||||||
|
mInboxReceived?.inc({ host: actor.host, type: 'delete' });
|
||||||
|
return await this.delete(actor, del);
|
||||||
|
}
|
||||||
|
|
||||||
|
const follow = yumeDowncastFollow(activity);
|
||||||
|
if (follow) {
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'follow' });
|
mInboxReceived?.inc({ host: actor.host, type: 'follow' });
|
||||||
return await this.follow(actor, activity);
|
return await this.follow(actor, follow);
|
||||||
} else if (isAccept(activity)) {
|
}
|
||||||
|
|
||||||
|
const accept = yumeDowncastAccept(activity);
|
||||||
|
if (accept) {
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'accept' });
|
mInboxReceived?.inc({ host: actor.host, type: 'accept' });
|
||||||
return await this.accept(actor, activity);
|
return await this.accept(actor, accept);
|
||||||
} else if (isReject(activity)) {
|
}
|
||||||
|
|
||||||
|
const reject = yumeDowncastReject(activity);
|
||||||
|
if (reject) {
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'reject' });
|
mInboxReceived?.inc({ host: actor.host, type: 'reject' });
|
||||||
return await this.reject(actor, activity);
|
return await this.reject(actor, reject);
|
||||||
} else if (isAdd(activity)) {
|
}
|
||||||
|
|
||||||
|
const add = yumeDowncastAdd(activity);
|
||||||
|
if (add) {
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'add' });
|
mInboxReceived?.inc({ host: actor.host, type: 'add' });
|
||||||
return await this.add(actor, activity);
|
return await this.add(actor, add);
|
||||||
} else if (isRemove(activity)) {
|
}
|
||||||
|
|
||||||
|
const remove = yumeDowncastRemove(activity);
|
||||||
|
if (remove) {
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'remove' });
|
mInboxReceived?.inc({ host: actor.host, type: 'remove' });
|
||||||
return await this.remove(actor, activity);
|
return await this.remove(actor, remove);
|
||||||
} else if (isAnnounce(activity)) {
|
}
|
||||||
|
|
||||||
|
const announce = yumeDowncastAnnounce(activity);
|
||||||
|
if (announce) {
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'announce' });
|
mInboxReceived?.inc({ host: actor.host, type: 'announce' });
|
||||||
return await this.announce(actor, activity);
|
return await this.announce(actor, announce);
|
||||||
} else if (isLike(activity)) {
|
}
|
||||||
|
|
||||||
|
const like = yumeDowncastLike(activity);
|
||||||
|
if (like) {
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'like' });
|
mInboxReceived?.inc({ host: actor.host, type: 'like' });
|
||||||
return await this.like(actor, activity);
|
return await this.like(actor, like);
|
||||||
} else if (isUndo(activity)) {
|
}
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'undo' });
|
|
||||||
return await this.undo(actor, activity);
|
const move = yumeDowncastMove(activity);
|
||||||
} else if (isBlock(activity)) {
|
if (move) {
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'block' });
|
|
||||||
return await this.block(actor, activity);
|
|
||||||
} else if (isFlag(activity)) {
|
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'flag' });
|
|
||||||
return await this.flag(actor, activity);
|
|
||||||
} else if (isMove(activity)) {
|
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'move' });
|
mInboxReceived?.inc({ host: actor.host, type: 'move' });
|
||||||
return await this.move(actor, activity);
|
return await this.move(actor, move);
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
const undo = yumeDowncastUndo(activity);
|
||||||
|
if (undo) {
|
||||||
|
mInboxReceived?.inc({ host: actor.host, type: 'undo' });
|
||||||
|
return await this.undo(actor, undo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const block = yumeDowncastBlock(activity);
|
||||||
|
if (block) {
|
||||||
|
mInboxReceived?.inc({ host: actor.host, type: 'block' });
|
||||||
|
return await this.block(actor, block);
|
||||||
|
}
|
||||||
|
|
||||||
|
const flag = yumeDowncastFlag(activity);
|
||||||
|
if (flag) {
|
||||||
|
mInboxReceived?.inc({ host: actor.host, type: 'flag' });
|
||||||
|
return await this.flag(actor, flag);
|
||||||
|
}
|
||||||
|
|
||||||
mInboxReceived?.inc({ host: actor.host, type: 'unknown' });
|
mInboxReceived?.inc({ host: actor.host, type: 'unknown' });
|
||||||
return `unrecognized activity type: ${activity.type}`;
|
return `unrecognized activity type: ${activity.type}`;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async follow(actor: MiRemoteUser, activity: IFollow): Promise<string> {
|
private async follow(actor: MiRemoteUser, activity: IFollow): Promise<string> {
|
||||||
|
@ -234,7 +274,8 @@ export class ApInboxService {
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isFollow(object)) return await this.acceptFollow(actor, object);
|
const follow = yumeDowncastFollow(object);
|
||||||
|
if (follow) return await this.acceptFollow(actor, follow);
|
||||||
|
|
||||||
return `skip: Unknown Accept type: ${getApType(object)}`;
|
return `skip: Unknown Accept type: ${getApType(object)}`;
|
||||||
}
|
}
|
||||||
|
@ -446,7 +487,7 @@ 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) {
|
||||||
|
@ -509,19 +550,12 @@ export class ApInboxService {
|
||||||
return `skip: delete actor ${actor.uri} !== ${uri}`;
|
return `skip: delete actor ${actor.uri} !== ${uri}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: actor.id });
|
if (!(await this.usersRepository.update({ id: actor.id, isDeleted: false }, { isDeleted: true })).affected) {
|
||||||
if (user == null) {
|
|
||||||
return 'skip: actor not found';
|
|
||||||
} else if (user.isDeleted) {
|
|
||||||
return 'skip: already deleted';
|
return 'skip: already deleted';
|
||||||
}
|
}
|
||||||
|
|
||||||
const job = await this.queueService.createDeleteAccountJob(actor);
|
const job = await this.queueService.createDeleteAccountJob(actor);
|
||||||
|
|
||||||
await this.usersRepository.update(actor.id, {
|
|
||||||
isDeleted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id });
|
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id });
|
||||||
|
|
||||||
return `ok: queued ${job.name} ${job.id}`;
|
return `ok: queued ${job.name} ${job.id}`;
|
||||||
|
@ -590,7 +624,8 @@ export class ApInboxService {
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isFollow(object)) return await this.rejectFollow(actor, object);
|
const follow = yumeDowncastFollow(object);
|
||||||
|
if (follow) return await this.rejectFollow(actor, follow);
|
||||||
|
|
||||||
return `skip: Unknown Reject type: ${getApType(object)}`;
|
return `skip: Unknown Reject type: ${getApType(object)}`;
|
||||||
}
|
}
|
||||||
|
@ -657,11 +692,20 @@ export class ApInboxService {
|
||||||
});
|
});
|
||||||
|
|
||||||
// don't queue because the sender may attempt again when timeout
|
// don't queue because the sender may attempt again when timeout
|
||||||
if (isFollow(object)) return await this.undoFollow(actor, object);
|
const follow = yumeDowncastFollow(object);
|
||||||
if (isBlock(object)) return await this.undoBlock(actor, object);
|
if (follow) return await this.undoFollow(actor, follow);
|
||||||
if (isLike(object)) return await this.undoLike(actor, object);
|
|
||||||
if (isAnnounce(object)) return await this.undoAnnounce(actor, object);
|
const block = yumeDowncastBlock(object);
|
||||||
if (isAccept(object)) return await this.undoAccept(actor, object);
|
if (block) return await this.undoBlock(actor, block);
|
||||||
|
|
||||||
|
const like = yumeDowncastLike(object);
|
||||||
|
if (like) return await this.undoLike(actor, like);
|
||||||
|
|
||||||
|
const announce = yumeDowncastAnnounce(object);
|
||||||
|
if (announce) return await this.undoAnnounce(actor, announce);
|
||||||
|
|
||||||
|
const accept = yumeDowncastAccept(object);
|
||||||
|
if (accept) return await this.undoAccept(actor, accept);
|
||||||
|
|
||||||
return `skip: unknown object type ${getApType(object)}`;
|
return `skip: unknown object type ${getApType(object)}`;
|
||||||
}
|
}
|
||||||
|
@ -791,7 +835,7 @@ 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));
|
await this.apQuestionService.updateQuestion(object, actor, resolver).catch(err => console.error(err));
|
||||||
return 'ok: Question updated';
|
return 'ok: Question updated';
|
||||||
} else {
|
} else {
|
||||||
return `skip: Unknown type: ${getApType(object)}`;
|
return `skip: Unknown type: ${getApType(object)}`;
|
||||||
|
|
|
@ -30,7 +30,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 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 { markOutgoing, type IAccept, type IActivity, type IAdd, type IAnnounce, type IApDocument, type IApEmoji, type IApHashtag, type IApImage, type IApMention, type IBlock, type ICreate, type IDelete, type IFlag, type IFollow, type IKey, type ILike, type IMove, type IObject, type IPost, type IQuestion, type IReject, type IRemove, type ITombstone, type IUndo, type IUpdate } from './type.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApRendererService {
|
export class ApRendererService {
|
||||||
|
@ -66,21 +66,21 @@ export class ApRendererService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderAccept(object: string | IObject, user: { id: MiUser['id']; host: null }): IAccept {
|
public renderAccept(object: string | IObject, user: { id: MiUser['id']; host: null }): IAccept {
|
||||||
return {
|
return markOutgoing({
|
||||||
type: 'Accept',
|
type: 'Accept',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
object,
|
object,
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderAdd(user: MiLocalUser, target: string | IObject | undefined, object: string | IObject): IAdd {
|
public renderAdd(user: MiLocalUser, target: string | IObject | undefined, object: string | IObject): IAdd {
|
||||||
return {
|
return markOutgoing({
|
||||||
type: 'Add',
|
type: 'Add',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
target,
|
target,
|
||||||
object,
|
object,
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -103,7 +103,7 @@ export class ApRendererService {
|
||||||
throw new Error('renderAnnounce: cannot render non-public note');
|
throw new Error('renderAnnounce: cannot render non-public note');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return markOutgoing({
|
||||||
id: `${this.config.url}/notes/${note.id}/activity`,
|
id: `${this.config.url}/notes/${note.id}/activity`,
|
||||||
actor: this.userEntityService.genLocalUserUri(note.userId),
|
actor: this.userEntityService.genLocalUserUri(note.userId),
|
||||||
type: 'Announce',
|
type: 'Announce',
|
||||||
|
@ -111,7 +111,7 @@ export class ApRendererService {
|
||||||
to,
|
to,
|
||||||
cc,
|
cc,
|
||||||
object,
|
object,
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -125,23 +125,23 @@ export class ApRendererService {
|
||||||
throw new Error('renderBlock: missing blockee uri');
|
throw new Error('renderBlock: missing blockee uri');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return markOutgoing({
|
||||||
type: 'Block',
|
type: 'Block',
|
||||||
id: `${this.config.url}/blocks/${block.id}`,
|
id: `${this.config.url}/blocks/${block.id}`,
|
||||||
actor: this.userEntityService.genLocalUserUri(block.blockerId),
|
actor: this.userEntityService.genLocalUserUri(block.blockerId),
|
||||||
object: block.blockee.uri,
|
object: block.blockee.uri,
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderCreate(object: IObject, note: MiNote): ICreate {
|
public renderCreate(object: IObject, note: MiNote): ICreate {
|
||||||
const activity: ICreate = {
|
const activity: ICreate = markOutgoing({
|
||||||
id: `${this.config.url}/notes/${note.id}/activity`,
|
id: `${this.config.url}/notes/${note.id}/activity`,
|
||||||
actor: this.userEntityService.genLocalUserUri(note.userId),
|
actor: this.userEntityService.genLocalUserUri(note.userId),
|
||||||
type: 'Create',
|
type: 'Create',
|
||||||
published: this.idService.parse(note.id).date.toISOString(),
|
published: this.idService.parse(note.id).date.toISOString(),
|
||||||
object,
|
object,
|
||||||
};
|
}, undefined);
|
||||||
|
|
||||||
if (object.to) activity.to = object.to;
|
if (object.to) activity.to = object.to;
|
||||||
if (object.cc) activity.cc = object.cc;
|
if (object.cc) activity.cc = object.cc;
|
||||||
|
@ -151,28 +151,28 @@ export class ApRendererService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderDelete(object: IObject | string, user: { id: MiUser['id']; host: null }): IDelete {
|
public renderDelete(object: IObject | string, user: { id: MiUser['id']; host: null }): IDelete {
|
||||||
return {
|
return markOutgoing({
|
||||||
type: 'Delete',
|
type: 'Delete',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
object,
|
object,
|
||||||
published: new Date().toISOString(),
|
published: new Date().toISOString(),
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderDocument(file: MiDriveFile): IApDocument {
|
public renderDocument(file: MiDriveFile): IApDocument {
|
||||||
return {
|
return markOutgoing({
|
||||||
type: 'Document',
|
type: 'Document',
|
||||||
mediaType: file.webpublicType ?? file.type,
|
mediaType: file.webpublicType ?? file.type,
|
||||||
url: this.driveFileEntityService.getPublicUrl(file),
|
url: this.driveFileEntityService.getPublicUrl(file),
|
||||||
name: file.comment,
|
name: file.comment,
|
||||||
sensitive: file.isSensitive,
|
sensitive: file.isSensitive,
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderEmoji(emoji: MiEmoji): IApEmoji {
|
public renderEmoji(emoji: MiEmoji): IApEmoji {
|
||||||
return {
|
return markOutgoing( {
|
||||||
id: `${this.config.url}/emojis/${emoji.name}`,
|
id: `${this.config.url}/emojis/${emoji.name}`,
|
||||||
type: 'Emoji',
|
type: 'Emoji',
|
||||||
name: `:${emoji.name}:`,
|
name: `:${emoji.name}:`,
|
||||||
|
@ -183,28 +183,28 @@ export class ApRendererService {
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url: emoji.publicUrl || emoji.originalUrl,
|
url: emoji.publicUrl || emoji.originalUrl,
|
||||||
},
|
},
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
// to anonymise reporters, the reporting actor must be a system user
|
// to anonymise reporters, the reporting actor must be a system user
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderFlag(user: MiLocalUser, object: IObject | string, content: string): IFlag {
|
public renderFlag(user: MiLocalUser, object: IObject | string, content: string): IFlag {
|
||||||
return {
|
return markOutgoing({
|
||||||
type: 'Flag',
|
type: 'Flag',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
content,
|
content,
|
||||||
object,
|
object,
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderFollowRelay(relay: MiRelay, relayActor: MiLocalUser): IFollow {
|
public renderFollowRelay(relay: MiRelay, relayActor: MiLocalUser): IFollow {
|
||||||
return {
|
return markOutgoing({
|
||||||
id: `${this.config.url}/activities/follow-relay/${relay.id}`,
|
id: `${this.config.url}/activities/follow-relay/${relay.id}`,
|
||||||
type: 'Follow',
|
type: 'Follow',
|
||||||
actor: this.userEntityService.genLocalUserUri(relayActor.id),
|
actor: this.userEntityService.genLocalUserUri(relayActor.id),
|
||||||
object: 'https://www.w3.org/ns/activitystreams#Public',
|
object: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -223,36 +223,36 @@ export class ApRendererService {
|
||||||
followee: MiPartialLocalUser | MiPartialRemoteUser,
|
followee: MiPartialLocalUser | MiPartialRemoteUser,
|
||||||
requestId?: string,
|
requestId?: string,
|
||||||
): IFollow {
|
): IFollow {
|
||||||
return {
|
return markOutgoing({
|
||||||
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
|
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
|
||||||
type: 'Follow',
|
type: 'Follow',
|
||||||
actor: this.userEntityService.getUserUri(follower),
|
actor: this.userEntityService.getUserUri(follower),
|
||||||
object: this.userEntityService.getUserUri(followee),
|
object: this.userEntityService.getUserUri(followee),
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderHashtag(tag: string): IApHashtag {
|
public renderHashtag(tag: string): IApHashtag {
|
||||||
return {
|
return markOutgoing({
|
||||||
type: 'Hashtag',
|
type: 'Hashtag',
|
||||||
href: `${this.config.url}/tags/${encodeURIComponent(tag)}`,
|
href: `${this.config.url}/tags/${encodeURIComponent(tag)}`,
|
||||||
name: `#${tag}`,
|
name: `#${tag}`,
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderImage(file: MiDriveFile): IApImage {
|
public renderImage(file: MiDriveFile): IApImage {
|
||||||
return {
|
return markOutgoing({
|
||||||
type: 'Image',
|
type: 'Image',
|
||||||
url: this.driveFileEntityService.getPublicUrl(file),
|
url: this.driveFileEntityService.getPublicUrl(file),
|
||||||
sensitive: file.isSensitive,
|
sensitive: file.isSensitive,
|
||||||
name: file.comment,
|
name: file.comment,
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey {
|
public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey {
|
||||||
return {
|
return markOutgoing({
|
||||||
id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`,
|
id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`,
|
||||||
type: 'Key',
|
type: 'Key',
|
||||||
owner: this.userEntityService.genLocalUserUri(user.id),
|
owner: this.userEntityService.genLocalUserUri(user.id),
|
||||||
|
@ -260,21 +260,21 @@ export class ApRendererService {
|
||||||
type: 'spki',
|
type: 'spki',
|
||||||
format: 'pem',
|
format: 'pem',
|
||||||
}),
|
}),
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async renderLike(noteReaction: MiNoteReaction, note: { uri: string | null }): Promise<ILike> {
|
public async renderLike(noteReaction: MiNoteReaction, note: { uri: string | null }): Promise<ILike> {
|
||||||
const reaction = noteReaction.reaction;
|
const reaction = noteReaction.reaction;
|
||||||
|
|
||||||
const object: ILike = {
|
const object: ILike = markOutgoing({
|
||||||
type: 'Like',
|
type: 'Like',
|
||||||
id: `${this.config.url}/likes/${noteReaction.id}`,
|
id: `${this.config.url}/likes/${noteReaction.id}`,
|
||||||
actor: `${this.config.url}/users/${noteReaction.userId}`,
|
actor: `${this.config.url}/users/${noteReaction.userId}`,
|
||||||
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
|
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
|
||||||
content: reaction,
|
content: reaction,
|
||||||
_misskey_reaction: reaction,
|
_misskey_reaction: reaction,
|
||||||
};
|
}, undefined);
|
||||||
|
|
||||||
if (reaction.startsWith(':')) {
|
if (reaction.startsWith(':')) {
|
||||||
const name = reaction.replaceAll(':', '');
|
const name = reaction.replaceAll(':', '');
|
||||||
|
@ -288,11 +288,11 @@ export class ApRendererService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderMention(mention: MiPartialLocalUser | MiPartialRemoteUser): IApMention {
|
public renderMention(mention: MiPartialLocalUser | MiPartialRemoteUser): IApMention {
|
||||||
return {
|
return markOutgoing({
|
||||||
type: 'Mention',
|
type: 'Mention',
|
||||||
href: this.userEntityService.getUserUri(mention),
|
href: this.userEntityService.getUserUri(mention),
|
||||||
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as MiLocalUser).username}`,
|
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as MiLocalUser).username}`,
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -302,13 +302,13 @@ export class ApRendererService {
|
||||||
): IMove {
|
): IMove {
|
||||||
const actor = this.userEntityService.getUserUri(src);
|
const actor = this.userEntityService.getUserUri(src);
|
||||||
const target = this.userEntityService.getUserUri(dst);
|
const target = this.userEntityService.getUserUri(dst);
|
||||||
return {
|
return markOutgoing({
|
||||||
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
|
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
|
||||||
actor,
|
actor,
|
||||||
type: 'Move',
|
type: 'Move',
|
||||||
object: actor,
|
object: actor,
|
||||||
target,
|
target,
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -422,7 +422,7 @@ export class ApRendererService {
|
||||||
})),
|
})),
|
||||||
} as const : {};
|
} as const : {};
|
||||||
|
|
||||||
return {
|
return markOutgoing({
|
||||||
id: `${this.config.url}/notes/${note.id}`,
|
id: `${this.config.url}/notes/${note.id}`,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
attributedTo,
|
attributedTo,
|
||||||
|
@ -445,7 +445,7 @@ export class ApRendererService {
|
||||||
sensitive: note.cw != null || files.some(file => file.isSensitive),
|
sensitive: note.cw != null || files.some(file => file.isSensitive),
|
||||||
tag,
|
tag,
|
||||||
...asPoll,
|
...asPoll,
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -529,7 +529,7 @@ export class ApRendererService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderQuestion(user: { id: MiUser['id'] }, note: MiNote, poll: MiPoll): IQuestion {
|
public renderQuestion(user: { id: MiUser['id'] }, note: MiNote, poll: MiPoll): IQuestion {
|
||||||
return {
|
return markOutgoing({
|
||||||
type: 'Question',
|
type: 'Question',
|
||||||
id: `${this.config.url}/questions/${note.id}`,
|
id: `${this.config.url}/questions/${note.id}`,
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
|
@ -542,78 +542,78 @@ export class ApRendererService {
|
||||||
totalItems: poll.votes[i],
|
totalItems: poll.votes[i],
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
};
|
}, 'question');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderReject(object: string | IObject, user: { id: MiUser['id'] }): IReject {
|
public renderReject(object: string | IObject, user: { id: MiUser['id'] }): IReject {
|
||||||
return {
|
return markOutgoing({
|
||||||
type: 'Reject',
|
type: 'Reject',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
object,
|
object,
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderRemove(user: { id: MiUser['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove {
|
public renderRemove(user: { id: MiUser['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove {
|
||||||
return {
|
return markOutgoing({
|
||||||
type: 'Remove',
|
type: 'Remove',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
target,
|
target,
|
||||||
object,
|
object,
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderTombstone(id: string): ITombstone {
|
public renderTombstone(id: string): ITombstone {
|
||||||
return {
|
return markOutgoing({
|
||||||
id,
|
id,
|
||||||
type: 'Tombstone',
|
type: 'Tombstone',
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderUndo(object: string | IObject, user: { id: MiUser['id'] }): IUndo {
|
public renderUndo(object: string | IObject, user: { id: MiUser['id'] }): IUndo {
|
||||||
const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
|
const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
|
||||||
|
|
||||||
return {
|
return markOutgoing({
|
||||||
type: 'Undo',
|
type: 'Undo',
|
||||||
...(id ? { id } : {}),
|
...(id ? { id } : {}),
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
object,
|
object,
|
||||||
published: new Date().toISOString(),
|
published: new Date().toISOString(),
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderUpdate(object: string | IObject, user: { id: MiUser['id'] }): IUpdate {
|
public renderUpdate(object: string | IObject, user: { id: MiUser['id'] }): IUpdate {
|
||||||
return {
|
return markOutgoing( {
|
||||||
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
|
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
type: 'Update',
|
type: 'Update',
|
||||||
to: ['https://www.w3.org/ns/activitystreams#Public'],
|
to: ['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
object,
|
object,
|
||||||
published: new Date().toISOString(),
|
published: new Date().toISOString(),
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderVote(user: { id: MiUser['id'] }, vote: MiPollVote, note: MiNote, poll: MiPoll, pollOwner: MiRemoteUser): ICreate {
|
public renderVote(user: { id: MiUser['id'] }, vote: MiPollVote, note: MiNote, poll: MiPoll, pollOwner: MiRemoteUser): ICreate {
|
||||||
return {
|
return markOutgoing({
|
||||||
id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`,
|
id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`,
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
type: 'Create',
|
type: 'Create',
|
||||||
to: [pollOwner.uri],
|
to: [pollOwner.uri],
|
||||||
published: new Date().toISOString(),
|
published: new Date().toISOString(),
|
||||||
object: {
|
object: markOutgoing({
|
||||||
id: `${this.config.url}/users/${user.id}#votes/${vote.id}`,
|
id: `${this.config.url}/users/${user.id}#votes/${vote.id}`,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
attributedTo: this.userEntityService.genLocalUserUri(user.id),
|
attributedTo: this.userEntityService.genLocalUserUri(user.id),
|
||||||
to: [pollOwner.uri],
|
to: [pollOwner.uri],
|
||||||
inReplyTo: note.uri,
|
inReplyTo: note.uri,
|
||||||
name: poll.choices[vote.choice],
|
name: poll.choices[vote.choice],
|
||||||
},
|
}, undefined),
|
||||||
};
|
}, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -11,11 +11,14 @@ import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||||
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.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 type { IObject } from './type.js';
|
||||||
|
|
||||||
type Request = {
|
type Request = {
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -145,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') と言われる
|
||||||
|
@ -251,7 +255,11 @@ export class ApRequestService {
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
validateContentTypeSetAsActivityPub(res);
|
validateContentTypeSetAsActivityPub(res);
|
||||||
|
const finalUrl = res.url; // redirects may have been involved
|
||||||
|
const activity = await res.json() as IObject;
|
||||||
|
|
||||||
return await res.json();
|
assertActivityMatchesUrls(activity, [url, finalUrl]);
|
||||||
|
|
||||||
|
return activity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,11 +16,13 @@ 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 { isCollectionOrOrderedCollection } from './type.js';
|
import { isCollectionOrOrderedCollection, yumeNormalizeObject } 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, IUnsanitizedObject } from './type.js';
|
||||||
|
import { toASCII } from 'node:punycode';
|
||||||
|
import { yumeAssertAcceptableURL } from './misc/validator.js';
|
||||||
|
|
||||||
export class Resolver {
|
export class Resolver {
|
||||||
private history: Set<string>;
|
private history: Set<string>;
|
||||||
|
@ -53,11 +55,16 @@ 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'
|
||||||
? await this.resolve(value)
|
? await this.resolve(value)
|
||||||
: value;
|
: yumeNormalizeObject(value);
|
||||||
|
|
||||||
if (isCollectionOrOrderedCollection(collection)) {
|
if (isCollectionOrOrderedCollection(collection)) {
|
||||||
return collection;
|
return collection;
|
||||||
|
@ -67,7 +74,7 @@ export class Resolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolve(value: string | IObject): Promise<IObject> {
|
private async resolveNotNormalized(value: string | IObject): Promise<IUnsanitizedObject> {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
@ -103,8 +110,8 @@ export class Resolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
const object = (this.user
|
const object = (this.user
|
||||||
? await this.apRequestService.signedGet(value, this.user) as IObject
|
? await this.apRequestService.signedGet(value, this.user) as IUnsanitizedObject
|
||||||
: await this.httpRequestService.getActivityJson(value)) as IObject;
|
: await this.httpRequestService.getActivityJson(value)) as IUnsanitizedObject;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Array.isArray(object['@context']) ?
|
Array.isArray(object['@context']) ?
|
||||||
|
@ -114,9 +121,27 @@ export class Resolver {
|
||||||
throw new Error('invalid response');
|
throw new Error('invalid response');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HttpRequestService / ApRequestService have already checked that
|
||||||
|
// `object.id` or `object.url` matches the URL used to fetch the
|
||||||
|
// object after redirects; here we double-check that no redirects
|
||||||
|
// bounced between hosts
|
||||||
|
if (object.id == null) {
|
||||||
|
throw new Error('invalid AP object: missing id');
|
||||||
|
}
|
||||||
|
|
||||||
|
yumeAssertAcceptableURL(object.id);
|
||||||
|
yumeAssertAcceptableURL(value);
|
||||||
|
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async resolve(value: string | IObject): Promise<IObject> {
|
||||||
|
const object = await this.resolveNotNormalized(value);
|
||||||
|
|
||||||
|
return yumeNormalizeObject(object);
|
||||||
|
}
|
||||||
|
|
||||||
@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);
|
||||||
|
|
|
@ -4,6 +4,28 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Response } from 'node-fetch';
|
import type { Response } from 'node-fetch';
|
||||||
|
import * as Bull from 'bullmq';
|
||||||
|
import { toASCII } from 'node:punycode';
|
||||||
|
|
||||||
|
export function yumeAssertAcceptableURL(url: string | URL): URL {
|
||||||
|
const urlParsed = url instanceof URL ? url : new URL(url);
|
||||||
|
|
||||||
|
if (urlParsed.search.length + urlParsed.pathname.length > 1024) {
|
||||||
|
throw new Bull.UnrecoverableError('URL is too long');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlParsed.protocol !== 'https:') {
|
||||||
|
throw new Bull.UnrecoverableError('URL protocol is not https');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlParsed.port && urlParsed.port !== '443') {
|
||||||
|
throw new Bull.UnrecoverableError('URL port is not 443');
|
||||||
|
}
|
||||||
|
|
||||||
|
urlParsed.hostname = toASCII(urlParsed.hostname);
|
||||||
|
|
||||||
|
return urlParsed;
|
||||||
|
}
|
||||||
|
|
||||||
export function validateContentTypeSetAsActivityPub(response: Response): void {
|
export function validateContentTypeSetAsActivityPub(response: Response): void {
|
||||||
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as Bull from 'bullmq';
|
||||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
@ -36,6 +37,7 @@ import { ApQuestionService } from './ApQuestionService.js';
|
||||||
import { ApImageService } from './ApImageService.js';
|
import { ApImageService } from './ApImageService.js';
|
||||||
import type { Resolver } from '../ApResolverService.js';
|
import type { Resolver } from '../ApResolverService.js';
|
||||||
import type { IObject, IPost } from '../type.js';
|
import type { IObject, IPost } from '../type.js';
|
||||||
|
import { yumeAssertAcceptableURL } from '../misc/validator.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApNoteService {
|
export class ApNoteService {
|
||||||
|
@ -77,7 +79,7 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public validateNote(object: IObject, uri: string): Error | null {
|
public validateNote(object: IObject, uri: string, actor?: MiRemoteUser): Error | null {
|
||||||
const expectHost = this.utilityService.extractDbHost(uri);
|
const expectHost = this.utilityService.extractDbHost(uri);
|
||||||
const apType = getApType(object);
|
const apType = getApType(object);
|
||||||
|
|
||||||
|
@ -98,6 +100,14 @@ export class ApNoteService {
|
||||||
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 (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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,14 +125,14 @@ export class ApNoteService {
|
||||||
* 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() },
|
||||||
|
@ -136,16 +146,29 @@ 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('Refusing to create note without id');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkHttps(note.id)) {
|
||||||
throw new Error('unexpected schema of note.id: ' + note.id);
|
throw new Error('unexpected schema of note.id: ' + note.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = getOneApHrefNullable(note.url);
|
const url = getOneApHrefNullable(note.url);
|
||||||
|
|
||||||
if (url && !checkHttps(url)) {
|
if (url != null) {
|
||||||
|
if (!checkHttps(url)) {
|
||||||
throw new Error('unexpected schema of note url: ' + url);
|
throw new Error('unexpected schema of note url: ' + url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const actUrl = yumeAssertAcceptableURL(url);
|
||||||
|
const noteUrl = yumeAssertAcceptableURL(note.id);
|
||||||
|
|
||||||
|
if (noteUrl.host !== actUrl.host) {
|
||||||
|
throw new Bull.UnrecoverableError(`note url & uri host mismatch: note url: ${url}, note uri: ${note.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.info(`Creating the Note: ${note.id}`);
|
this.logger.info(`Creating the Note: ${note.id}`);
|
||||||
|
|
||||||
// 投稿者をフェッチ
|
// 投稿者をフェッチ
|
||||||
|
@ -156,8 +179,9 @@ export class ApNoteService {
|
||||||
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;
|
||||||
|
if (actor && 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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +213,8 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
//#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) {
|
||||||
|
@ -348,15 +373,11 @@ export class ApNoteService {
|
||||||
if (exist) return exist;
|
if (exist) return exist;
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
if (uri.startsWith(this.config.url)) {
|
|
||||||
throw new StatusError('cannot resolve local note', 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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -277,16 +277,13 @@ export class ApPersonService implements OnModuleInit {
|
||||||
* Personを作成します。
|
* Personを作成します。
|
||||||
*/
|
*/
|
||||||
@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 Error('uri is not string');
|
||||||
|
|
||||||
if (uri.startsWith(this.config.url)) {
|
if (uri.startsWith(this.config.url)) {
|
||||||
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
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 Error('invalid object.id: ' + object.id);
|
||||||
|
|
||||||
|
@ -557,7 +554,9 @@ export class ApPersonService implements OnModuleInit {
|
||||||
if (moving) updates.movedAt = new Date();
|
if (moving) updates.movedAt = new Date();
|
||||||
|
|
||||||
// Update user
|
// Update user
|
||||||
await this.usersRepository.update(exist.id, updates);
|
if (!(await this.usersRepository.update({ id: exist.id, isDeleted: false }, updates)).affected) {
|
||||||
|
return 'skip';
|
||||||
|
}
|
||||||
|
|
||||||
if (person.publicKey) {
|
if (person.publicKey) {
|
||||||
await this.userPublickeysRepository.update({ userId: exist.id }, {
|
await this.userPublickeysRepository.update({ userId: exist.id }, {
|
||||||
|
|
|
@ -5,16 +5,19 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
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 { 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';
|
||||||
|
import { yumeAssertAcceptableURL } from '../misc/validator.js';
|
||||||
|
import { toASCII } from 'punycode';
|
||||||
|
|
||||||
@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,
|
||||||
|
|
||||||
|
@ -65,28 +71,41 @@ 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 uriIn = typeof value === 'string' ? value : value.id;
|
||||||
if (uri == null) throw new Error('uri is null');
|
if (uriIn == null) throw new Error('uri is null');
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local');
|
const uri = yumeAssertAcceptableURL(uriIn);
|
||||||
|
|
||||||
|
if (toASCII(this.config.host) === uri.host) throw new Error('uri points local');
|
||||||
|
|
||||||
//#region このサーバーに既に登録されているか
|
//#region このサーバーに既に登録されているか
|
||||||
const note = await this.notesRepository.findOneBy({ uri });
|
const note = await this.notesRepository.findOneBy({ uri: uriIn });
|
||||||
if (note == null) throw new Error('Question is not registered');
|
if (note == null) throw new Error('Question is not registered');
|
||||||
|
|
||||||
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');
|
||||||
|
|
||||||
|
const user = await this.usersRepository.findOneBy({ id: poll.userId });
|
||||||
|
if (user == null) throw new Error('Question is not registered');
|
||||||
//#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 Error('object is not a Question');
|
||||||
|
|
||||||
|
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 Error('Refusing to ingest update for poll by different user');
|
||||||
|
}
|
||||||
|
|
||||||
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 Error('invalid apChoices: ' + apChoices);
|
||||||
|
@ -96,7 +115,7 @@ export class ApQuestionService {
|
||||||
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 Error('invalid newCount: ' + newCount);
|
||||||
|
|
||||||
if (oldCount !== newCount) {
|
if (oldCount !== newCount) {
|
||||||
changed = true;
|
changed = true;
|
||||||
|
|
|
@ -3,20 +3,45 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { target } from "happy-dom/lib/PropertySymbol.js";
|
||||||
|
import { toASCII } from "node:punycode";
|
||||||
|
import * as bull from "bullmq";
|
||||||
|
|
||||||
export type Obj = { [x: string]: any };
|
export type Obj = { [x: string]: any };
|
||||||
export type ApObject = IObject | string | (IObject | string)[];
|
export type ApObject = IObject | string | (IObject | string)[];
|
||||||
|
|
||||||
export interface IObject {
|
export interface MisskeyVendorKeys {
|
||||||
|
_misskey_summary: string;
|
||||||
|
_misskey_followedMessage: string | null;
|
||||||
|
_misskey_requireSigninToViewContents: boolean;
|
||||||
|
_misskey_makeNotesFollowersOnlyBefore: number | null;
|
||||||
|
_misskey_makeNotesHiddenBefore: number | null;
|
||||||
|
_misskey_quote: string;
|
||||||
|
_misskey_content: string;
|
||||||
|
_misskey_reaction: string;
|
||||||
|
_misskey_votes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractMisskeyVendorKeys(object: IObject): Partial<MisskeyVendorKeys> {
|
||||||
|
return {
|
||||||
|
_misskey_summary: object._misskey_summary,
|
||||||
|
_misskey_followedMessage: object._misskey_followedMessage,
|
||||||
|
_misskey_requireSigninToViewContents: object._misskey_requireSigninToViewContents,
|
||||||
|
_misskey_makeNotesFollowersOnlyBefore: object._misskey_makeNotesFollowersOnlyBefore,
|
||||||
|
_misskey_makeNotesHiddenBefore: object._misskey_makeNotesHiddenBefore,
|
||||||
|
_misskey_quote: object._misskey_quote,
|
||||||
|
_misskey_content: object._misskey_content,
|
||||||
|
_misskey_reaction: object._misskey_reaction,
|
||||||
|
_misskey_votes: object._misskey_votes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUnsanitizedObject extends Partial<MisskeyVendorKeys> {
|
||||||
'@context'?: string | string[] | Obj | Obj[];
|
'@context'?: string | string[] | Obj | Obj[];
|
||||||
type: string | string[];
|
type: string | string[];
|
||||||
id?: string;
|
id?: string;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
_misskey_summary?: string;
|
|
||||||
_misskey_followedMessage?: string | null;
|
|
||||||
_misskey_requireSigninToViewContents?: boolean;
|
|
||||||
_misskey_makeNotesFollowersOnlyBefore?: number | null;
|
|
||||||
_misskey_makeNotesHiddenBefore?: number | null;
|
|
||||||
published?: string;
|
published?: string;
|
||||||
cc?: ApObject;
|
cc?: ApObject;
|
||||||
to?: ApObject;
|
to?: ApObject;
|
||||||
|
@ -34,6 +59,80 @@ export interface IObject {
|
||||||
href?: string;
|
href?: string;
|
||||||
tag?: IObject | IObject[];
|
tag?: IObject | IObject[];
|
||||||
sensitive?: boolean;
|
sensitive?: boolean;
|
||||||
|
|
||||||
|
visibility?: string;
|
||||||
|
mentionedUsers?: any[];
|
||||||
|
visibleUsers?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IObject extends IUnsanitizedObject {
|
||||||
|
__yume_normalized_object: true | 'outgoing';
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface YumeDowncastSanitizedBadge<L extends 'question' | undefined> {
|
||||||
|
__yume_normalized_badge: L | 'outgoing';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function markOutgoing<T, L extends 'question' | undefined>(object: T, _badge: L): T & IObject & YumeDowncastSanitizedBadge<L> {
|
||||||
|
return object as T & IObject & YumeDowncastSanitizedBadge<L>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeNormalizeURL(url: string): string {
|
||||||
|
const u = new URL(url);
|
||||||
|
u.host = toASCII(u.host);
|
||||||
|
if (u.protocol && u.protocol !== 'https:') {
|
||||||
|
throw new bull.UnrecoverableError('protocol is not https');
|
||||||
|
}
|
||||||
|
u.protocol = 'https:';
|
||||||
|
if (u.port && u.port !== '443') {
|
||||||
|
throw new bull.UnrecoverableError('port is not 443');
|
||||||
|
}
|
||||||
|
return u.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeNormalizeRecursive<O extends IUnsanitizedObject | string | (IUnsanitizedObject | string)[]>(object: O, depth = 0):
|
||||||
|
IObject | string | (IObject | string)[] {
|
||||||
|
if (depth > 16) {
|
||||||
|
throw new bull.UnrecoverableError('recursion limit exceeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof object === 'string') {
|
||||||
|
return yumeNormalizeURL(object);
|
||||||
|
}
|
||||||
|
if (Array.isArray(object)) {
|
||||||
|
if (object.length > 64) {
|
||||||
|
throw new bull.UnrecoverableError('array length limit exceeded');
|
||||||
|
}
|
||||||
|
return object.flatMap((x) => yumeNormalizeRecursive(x, depth + (object.length + 3 / 4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return yumeNormalizeObject(object, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeNormalizeObject(object: IUnsanitizedObject, depth = 0): IObject {
|
||||||
|
if (object.cc) {
|
||||||
|
object.cc = yumeNormalizeRecursive(object.cc, depth + 1);
|
||||||
|
}
|
||||||
|
if (object.id) {
|
||||||
|
object.id = yumeNormalizeURL(object.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (object.url) {
|
||||||
|
object.url = yumeNormalizeRecursive(object.url, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (object.replies) {
|
||||||
|
object.replies.first = object.replies.first ?
|
||||||
|
typeof object.replies.first === 'string' ? yumeNormalizeURL(object.replies.first) : yumeNormalizeObject(object.replies.first, depth + 1) : undefined;
|
||||||
|
object.replies.items = object.replies.items ?
|
||||||
|
typeof object.replies.items === 'string' ? yumeNormalizeURL(object.replies.items) : yumeNormalizeRecursive(object.replies.items, depth + 1) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (object.inReplyTo) {
|
||||||
|
object.inReplyTo = yumeNormalizeRecursive(object.inReplyTo, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return object as IObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,7 +179,7 @@ export function getOneApHrefNullable(value: ApObject | undefined): string | unde
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getApHrefNullable(value: string | IObject | undefined): string | undefined {
|
export function getApHrefNullable(value: string | IObject | undefined): string | undefined {
|
||||||
if (typeof value === 'string') return value;
|
if (typeof value === 'string') return value;
|
||||||
if (typeof value?.href === 'string') return value.href;
|
if (typeof value?.href === 'string') return value.href;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -101,6 +200,28 @@ export interface IActivity extends IObject {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SafeList {
|
||||||
|
id: string;
|
||||||
|
content: string | null;
|
||||||
|
tag: IObject | IObject[];
|
||||||
|
published: string;
|
||||||
|
visibility: string;
|
||||||
|
mentionedUsers: any[];
|
||||||
|
visibleUsers: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractSafe(object: IObject): Partial<SafeList> {
|
||||||
|
return {
|
||||||
|
id: object.id,
|
||||||
|
content: object.content,
|
||||||
|
tag: object.tag,
|
||||||
|
published: object.published,
|
||||||
|
visibility: object.visibility,
|
||||||
|
mentionedUsers: object.mentionedUsers,
|
||||||
|
visibleUsers: object.visibleUsers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface ICollection extends IObject {
|
export interface ICollection extends IObject {
|
||||||
type: 'Collection';
|
type: 'Collection';
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
|
@ -122,7 +243,7 @@ export const isPost = (object: IObject): object is IPost => {
|
||||||
return type != null && validPost.includes(type);
|
return type != null && validPost.includes(type);
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IPost extends IObject {
|
export interface IPost extends IObject{
|
||||||
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
|
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
|
||||||
source?: {
|
source?: {
|
||||||
content: string;
|
content: string;
|
||||||
|
@ -133,7 +254,7 @@ export interface IPost extends IObject {
|
||||||
quoteUrl?: string;
|
quoteUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IQuestion extends IObject {
|
export interface IUnsanitizedQuestion extends IObject {
|
||||||
type: 'Note' | 'Question';
|
type: 'Note' | 'Question';
|
||||||
actor: string;
|
actor: string;
|
||||||
source?: {
|
source?: {
|
||||||
|
@ -148,7 +269,25 @@ export interface IQuestion extends IObject {
|
||||||
closed?: Date;
|
closed?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isQuestion = (object: IObject): object is IQuestion =>
|
export interface IQuestion extends IUnsanitizedQuestion, YumeDowncastSanitizedBadge<'question'> {}
|
||||||
|
|
||||||
|
export function yumeSanitizeQuestion(object: IUnsanitizedQuestion): IQuestion {
|
||||||
|
return {
|
||||||
|
type: object.type,
|
||||||
|
actor: yumeNormalizeURL(object.actor),
|
||||||
|
source: object.source,
|
||||||
|
_misskey_quote: object._misskey_quote,
|
||||||
|
quoteUrl: object.quoteUrl ? yumeNormalizeURL(object.quoteUrl) : '',
|
||||||
|
oneOf: object.oneOf,
|
||||||
|
anyOf: object.anyOf,
|
||||||
|
endTime: object.endTime,
|
||||||
|
closed: object.closed,
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
__yume_normalized_badge: 'question',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isQuestion = (object: IObject): object is IUnsanitizedQuestion =>
|
||||||
getApType(object) === 'Note' || getApType(object) === 'Question';
|
getApType(object) === 'Note' || getApType(object) === 'Question';
|
||||||
|
|
||||||
interface IQuestionChoice {
|
interface IQuestionChoice {
|
||||||
|
@ -264,88 +403,307 @@ export const isDocument = (object: IObject): object is IApDocument => {
|
||||||
return type != null && validDocumentTypes.includes(type);
|
return type != null && validDocumentTypes.includes(type);
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IApImage extends IApDocument {
|
export interface IApImage extends IApDocument, Partial<SafeList> {
|
||||||
type: 'Image';
|
type: 'Image';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreate extends IActivity {
|
export interface ICreate extends IActivity, Partial<SafeList> {
|
||||||
type: 'Create';
|
type: 'Create';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDelete extends IActivity {
|
export interface IDelete extends IActivity, Partial<SafeList> {
|
||||||
type: 'Delete';
|
type: 'Delete';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUpdate extends IActivity {
|
export interface IUpdate extends IActivity, Partial<SafeList> {
|
||||||
type: 'Update';
|
type: 'Update';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRead extends IActivity {
|
export interface IRead extends IActivity, Partial<SafeList> {
|
||||||
type: 'Read';
|
type: 'Read';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUndo extends IActivity {
|
export interface IUndo extends IActivity, Partial<SafeList> {
|
||||||
type: 'Undo';
|
type: 'Undo';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFollow extends IActivity {
|
export interface IFollow extends IActivity, Partial<SafeList> {
|
||||||
type: 'Follow';
|
type: 'Follow';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAccept extends IActivity {
|
export interface IAccept extends IActivity, Partial<SafeList> {
|
||||||
type: 'Accept';
|
type: 'Accept';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReject extends IActivity {
|
export interface IReject extends IActivity, Partial<SafeList> {
|
||||||
type: 'Reject';
|
type: 'Reject';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAdd extends IActivity {
|
export interface IAdd extends IActivity, Partial<SafeList> {
|
||||||
type: 'Add';
|
type: 'Add';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRemove extends IActivity {
|
export interface IRemove extends IActivity, Partial<SafeList> {
|
||||||
type: 'Remove';
|
type: 'Remove';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILike extends IActivity {
|
export interface ILike extends IActivity, Partial<SafeList> {
|
||||||
type: 'Like' | 'EmojiReaction' | 'EmojiReact';
|
type: 'Like' | 'EmojiReaction' | 'EmojiReact';
|
||||||
_misskey_reaction?: string;
|
_misskey_reaction?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAnnounce extends IActivity {
|
export interface IAnnounce extends IActivity, Partial<SafeList> {
|
||||||
type: 'Announce';
|
type: 'Announce';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBlock extends IActivity {
|
export interface IBlock extends IActivity, Partial<SafeList> {
|
||||||
type: 'Block';
|
type: 'Block';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFlag extends IActivity {
|
export interface IFlag extends IActivity, Partial<SafeList> {
|
||||||
type: 'Flag';
|
type: 'Flag';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMove extends IActivity {
|
export interface IMove extends IActivity, Partial<SafeList> {
|
||||||
type: 'Move';
|
type: 'Move';
|
||||||
target: IObject | string;
|
target: IObject | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
|
export function yumeDowncastCreate(object: IObject): ICreate | null {
|
||||||
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
|
if (getApType(object) !== 'Create') return null;
|
||||||
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
|
const obj = object as ICreate;
|
||||||
export const isRead = (object: IObject): object is IRead => getApType(object) === 'Read';
|
if (!obj.actor || !obj.object) return null;
|
||||||
export const isUndo = (object: IObject): object is IUndo => getApType(object) === 'Undo';
|
return {
|
||||||
export const isFollow = (object: IObject): object is IFollow => getApType(object) === 'Follow';
|
...extractMisskeyVendorKeys(object),
|
||||||
export const isAccept = (object: IObject): object is IAccept => getApType(object) === 'Accept';
|
...extractSafe(object),
|
||||||
export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject';
|
type: 'Create',
|
||||||
export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add';
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove';
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
export const isLike = (object: IObject): object is ILike => {
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
const type = getApType(object);
|
__yume_normalized_object: true,
|
||||||
return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type);
|
};
|
||||||
};
|
}
|
||||||
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
|
||||||
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
export function yumeDowncastDelete(object: IObject): IDelete | null {
|
||||||
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
if (getApType(object) !== 'Delete') return null;
|
||||||
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
|
const obj = object as IDelete;
|
||||||
export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note';
|
if (!obj.actor || !obj.object) return null;
|
||||||
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
|
type: 'Delete',
|
||||||
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeDowncastUpdate(object: IObject): IUpdate | null {
|
||||||
|
if (getApType(object) !== 'Update') return null;
|
||||||
|
const obj = object as IUpdate;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
|
type: 'Update',
|
||||||
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeDowncastRead(object: IObject): IRead | null {
|
||||||
|
if (getApType(object) !== 'Read') return null;
|
||||||
|
const obj = object as IRead;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
|
type: 'Read',
|
||||||
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeDowncastUndo(object: IObject): IUndo | null {
|
||||||
|
if (getApType(object) !== 'Undo') return null;
|
||||||
|
const obj = object as IUndo;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
|
type: 'Undo',
|
||||||
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeDowncastFollow(object: IObject): IFollow | null {
|
||||||
|
if (getApType(object) !== 'Follow') return null;
|
||||||
|
const obj = object as IFollow;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
|
type: 'Follow',
|
||||||
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeDowncastAccept(object: IObject): IAccept | null {
|
||||||
|
if (getApType(object) !== 'Accept') return null;
|
||||||
|
const obj = object as IAccept;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
|
type: 'Accept',
|
||||||
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeDowncastReject(object: IObject): IReject | null {
|
||||||
|
if (getApType(object) !== 'Reject') return null;
|
||||||
|
const obj = object as IReject;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
|
type: 'Reject',
|
||||||
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeDowncastAdd(object: IObject): IAdd | null {
|
||||||
|
if (getApType(object) !== 'Add') return null;
|
||||||
|
const obj = object as IAdd;
|
||||||
|
if (!obj.actor || !obj.object ) return null;
|
||||||
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
|
type: 'Add',
|
||||||
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeDowncastRemove(object: IObject): IRemove | null {
|
||||||
|
if (getApType(object) !== 'Remove') return null;
|
||||||
|
const obj = object as IRemove;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
|
type: 'Remove',
|
||||||
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeDowncastLike(object: IObject): ILike | null {
|
||||||
|
if (getApType(object) !== 'Like') return null;
|
||||||
|
const obj = object as ILike;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
|
type: 'Like',
|
||||||
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeDowncastAnnounce(object: IObject): IAnnounce | null {
|
||||||
|
if (getApType(object) !== 'Announce') return null;
|
||||||
|
const obj = object as IAnnounce;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
|
return {
|
||||||
|
// ...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
|
type: 'Announce',
|
||||||
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeDowncastBlock(object: IObject): IBlock | null {
|
||||||
|
if (getApType(object) !== 'Block') return null;
|
||||||
|
const obj = object as IBlock;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
|
type: 'Block',
|
||||||
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeDowncastFlag(object: IObject): IFlag | null {
|
||||||
|
if (getApType(object) !== 'Flag') return null;
|
||||||
|
const obj = object as IFlag;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
|
type: 'Flag',
|
||||||
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yumeDowncastMove(object: IObject): IMove | null {
|
||||||
|
if (getApType(object) !== 'Move') return null;
|
||||||
|
const obj = object as IMove;
|
||||||
|
if (!obj.actor || !obj.object || !obj.target) return null;
|
||||||
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
|
type: 'Move',
|
||||||
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target),
|
||||||
|
__yume_normalized_object: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function yumeDowncastMention(object: IObject): IApMention | null {
|
||||||
|
if (getApType(object) !== 'Mention') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const href = getApHrefNullable(object);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...object,
|
||||||
|
type: 'Mention',
|
||||||
|
href: href ? yumeNormalizeURL(href) : '',
|
||||||
|
name: object.name ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { UtilityService } from '../UtilityService.js';
|
import { UtilityService } from '../UtilityService.js';
|
||||||
import { VideoProcessingService } from '../VideoProcessingService.js';
|
|
||||||
import { UserEntityService } from './UserEntityService.js';
|
import { UserEntityService } from './UserEntityService.js';
|
||||||
import { DriveFolderEntityService } from './DriveFolderEntityService.js';
|
import { DriveFolderEntityService } from './DriveFolderEntityService.js';
|
||||||
|
|
||||||
|
@ -43,7 +42,6 @@ export class DriveFileEntityService {
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private driveFolderEntityService: DriveFolderEntityService,
|
private driveFolderEntityService: DriveFolderEntityService,
|
||||||
private videoProcessingService: VideoProcessingService,
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
@ -86,11 +84,7 @@ export class DriveFileEntityService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getThumbnailUrl(file: MiDriveFile): string | null {
|
public getThumbnailUrl(file: MiDriveFile): string | null {
|
||||||
if (file.type.startsWith('video')) {
|
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
|
||||||
if (file.thumbnailUrl) return file.thumbnailUrl;
|
|
||||||
|
|
||||||
return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url);
|
|
||||||
} else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
|
|
||||||
// 動画ではなくリモートかつメディアプロキシ
|
// 動画ではなくリモートかつメディアプロキシ
|
||||||
return this.getProxiedUrl(file.uri, 'static');
|
return this.getProxiedUrl(file.uri, 'static');
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,15 +11,39 @@ import type { } from '@/models/Blocking.js';
|
||||||
import type { MiEmoji } from '@/models/Emoji.js';
|
import type { MiEmoji } from '@/models/Emoji.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmojiEntityService {
|
export class EmojiEntityService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private stripProxyIfOrigin(url: string): string {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
let origin = u.origin;
|
||||||
|
if (u.origin === new URL(this.config.mediaProxy).origin) {
|
||||||
|
const innerUrl = u.searchParams.get('url');
|
||||||
|
if (innerUrl) {
|
||||||
|
origin = new URL(innerUrl).origin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (origin === u.origin) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packSimpleNoQuery(
|
public packSimpleNoQuery(
|
||||||
emoji: MiEmoji,
|
emoji: MiEmoji,
|
||||||
|
@ -29,7 +53,7 @@ export class EmojiEntityService {
|
||||||
name: emoji.name,
|
name: emoji.name,
|
||||||
category: emoji.category,
|
category: emoji.category,
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url: emoji.publicUrl || emoji.originalUrl,
|
url: this.stripProxyIfOrigin(emoji.publicUrl || emoji.originalUrl),
|
||||||
localOnly: emoji.localOnly ? true : undefined,
|
localOnly: emoji.localOnly ? true : undefined,
|
||||||
isSensitive: emoji.isSensitive ? true : undefined,
|
isSensitive: emoji.isSensitive ? true : undefined,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
|
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
|
||||||
|
@ -72,7 +96,7 @@ export class EmojiEntityService {
|
||||||
category: emoji.category,
|
category: emoji.category,
|
||||||
host: emoji.host,
|
host: emoji.host,
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url: emoji.publicUrl || emoji.originalUrl,
|
url: this.stripProxyIfOrigin(emoji.publicUrl || emoji.originalUrl),
|
||||||
license: emoji.license,
|
license: emoji.license,
|
||||||
isSensitive: emoji.isSensitive,
|
isSensitive: emoji.isSensitive,
|
||||||
localOnly: emoji.localOnly,
|
localOnly: emoji.localOnly,
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, ViewEntity } from 'typeorm';
|
||||||
import { id } from './util/id.js';
|
import { id } from './util/id.js';
|
||||||
import { MiUser } from './User.js';
|
import { MiUser } from './User.js';
|
||||||
|
|
||||||
|
@ -98,3 +98,4 @@ export class MiFollowing {
|
||||||
public followeeSharedInbox: string | null;
|
public followeeSharedInbox: string | null;
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { MoreThan } from 'typeorm';
|
import { DataSource, MoreThan, QueryFailedError, TypeORMError } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import { MiUser, type DriveFilesRepository, type NotesRepository, type UserProfilesRepository, type UsersRepository } from '@/models/_.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
|
@ -26,6 +26,9 @@ export class DeleteAccountProcessorService {
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.db)
|
||||||
|
private db: DataSource,
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
|
@ -52,6 +55,14 @@ export class DeleteAccountProcessorService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!user.isDeleted) {
|
||||||
|
this.logger.warn('User is not pre-marked as deleted, this is likely a bug');
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
throw new Error('User is not pre-marked as deleted'); // make some noise to make sure tests fail
|
||||||
|
}
|
||||||
|
await this.usersRepository.update({ id: user.id }, { isDeleted: true });
|
||||||
|
}
|
||||||
|
|
||||||
{ // Delete notes
|
{ // Delete notes
|
||||||
let cursor: MiNote['id'] | null = null;
|
let cursor: MiNote['id'] | null = null;
|
||||||
|
|
||||||
|
@ -121,13 +132,46 @@ export class DeleteAccountProcessorService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deadlockが発生した場合にリトライする
|
||||||
|
for (let remaining = 3; remaining > 0; remaining--) {
|
||||||
|
try {
|
||||||
// soft指定されている場合は物理削除しない
|
// soft指定されている場合は物理削除しない
|
||||||
|
await this.db.transaction(async txn => {
|
||||||
|
// soft指定してもデータをすべで削除する
|
||||||
|
await txn.delete(MiUser, user.id);
|
||||||
if (job.data.soft) {
|
if (job.data.soft) {
|
||||||
// nop
|
await txn.insert(MiUser, {
|
||||||
} else {
|
...user,
|
||||||
await this.usersRepository.delete(job.data.user.id);
|
isRoot: false,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
emojis: [],
|
||||||
|
hideOnlineStatus: true,
|
||||||
|
followersCount: 0,
|
||||||
|
followingCount: 0,
|
||||||
|
avatarUrl: null,
|
||||||
|
avatarId: null,
|
||||||
|
notesCount: 0,
|
||||||
|
inbox: null,
|
||||||
|
sharedInbox: null,
|
||||||
|
featured: null,
|
||||||
|
uri: null,
|
||||||
|
followersUri: null,
|
||||||
|
token: null,
|
||||||
|
isDeleted: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return 'Account deleted';
|
return 'Account deleted';
|
||||||
|
} catch (e) {
|
||||||
|
// 40P01 = deadlock_detected
|
||||||
|
// https://www.postgresql.org/docs/current/errcodes-appendix.html
|
||||||
|
if (remaining > 0 && e instanceof QueryFailedError && e.driverError.code === '40P01') {
|
||||||
|
this.logger.warn(`Deadlock occurred, retrying after 1s... [${remaining - 1} remaining]`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -255,6 +255,8 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
incCounter(mIncomingApReject, 'host_signature_mismatch');
|
incCounter(mIncomingApReject, 'host_signature_mismatch');
|
||||||
throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`);
|
throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw new Bull.UnrecoverableError('skip: activity id is not a string');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.apRequestChart.inbox();
|
this.apRequestChart.inbox();
|
||||||
|
@ -307,7 +309,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
const end = +new Date();
|
const end = +new Date();
|
||||||
observeHistogram(mIncomingApProcessingTime, { success: 'false' }, (end - begin) / 1000);
|
observeHistogram(mIncomingApProcessingTime, { success: 'false' }, (end - begin) / 1000);
|
||||||
incCounter(mincomingApProcessingError, { reason: 'unknown' });
|
incCounter(mincomingApProcessingError, {});
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
observeHistogram(mIncomingApProcessingTime, { success: 'true' }, (+new Date() - begin) / 1000);
|
observeHistogram(mIncomingApProcessingTime, { success: 'true' }, (+new Date() - begin) / 1000);
|
||||||
|
|
|
@ -105,7 +105,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) {
|
||||||
reply.code(401);
|
reply.code(401);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -8,27 +8,19 @@ import { fileURLToPath } from 'node:url';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import rename from 'rename';
|
import rename from 'rename';
|
||||||
import sharp from 'sharp';
|
|
||||||
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js';
|
import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
|
||||||
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { DownloadService } from '@/core/DownloadService.js';
|
|
||||||
import { IImageStreamable, ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
|
|
||||||
import { VideoProcessingService } from '@/core/VideoProcessingService.js';
|
|
||||||
import { InternalStorageService } from '@/core/InternalStorageService.js';
|
|
||||||
import { contentDisposition } from '@/misc/content-disposition.js';
|
import { contentDisposition } from '@/misc/content-disposition.js';
|
||||||
import { FileInfoService } from '@/core/FileInfoService.js';
|
import { FileInfoService } from '@/core/FileInfoService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
|
||||||
import { correctFilename } from '@/misc/correct-filename.js';
|
|
||||||
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
|
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||||
|
import { InternalStorageService } from '@/core/InternalStorageService.js';
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
const _dirname = dirname(_filename);
|
const _dirname = dirname(_filename);
|
||||||
|
@ -46,11 +38,8 @@ export class FileServerService {
|
||||||
@Inject(DI.driveFilesRepository)
|
@Inject(DI.driveFilesRepository)
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
private fileInfoService: FileInfoService,
|
|
||||||
private downloadService: DownloadService,
|
|
||||||
private imageProcessingService: ImageProcessingService,
|
|
||||||
private videoProcessingService: VideoProcessingService,
|
|
||||||
private internalStorageService: InternalStorageService,
|
private internalStorageService: InternalStorageService,
|
||||||
|
private fileInfoService: FileInfoService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('server', 'gray');
|
this.logger = this.loggerService.getLogger('server', 'gray');
|
||||||
|
@ -134,96 +123,13 @@ export class FileServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
if (file.state === 'remote') {
|
if (file.state === 'remote') {
|
||||||
let image: IImageStreamable | null = null;
|
const url = new URL(`${this.config.mediaProxy}/`);
|
||||||
|
|
||||||
if (file.fileRole === 'thumbnail') {
|
|
||||||
if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
|
|
||||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
|
||||||
|
|
||||||
const url = new URL(`${this.config.mediaProxy}/static.webp`);
|
|
||||||
url.searchParams.set('url', file.url);
|
|
||||||
url.searchParams.set('static', '1');
|
|
||||||
|
|
||||||
file.cleanup();
|
|
||||||
return await reply.redirect(url.toString(), 301);
|
|
||||||
} else if (file.mime.startsWith('video/')) {
|
|
||||||
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
|
|
||||||
if (externalThumbnail) {
|
|
||||||
file.cleanup();
|
|
||||||
return await reply.redirect(externalThumbnail, 301);
|
|
||||||
}
|
|
||||||
|
|
||||||
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.fileRole === 'webpublic') {
|
|
||||||
if (['image/svg+xml'].includes(file.mime)) {
|
|
||||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
|
||||||
|
|
||||||
const url = new URL(`${this.config.mediaProxy}/svg.webp`);
|
|
||||||
url.searchParams.set('url', file.url);
|
url.searchParams.set('url', file.url);
|
||||||
|
|
||||||
file.cleanup();
|
|
||||||
return await reply.redirect(url.toString(), 301);
|
return await reply.redirect(url.toString(), 301);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
if (request.headers.range && file.file.size > 0) {
|
|
||||||
const range = request.headers.range as string;
|
|
||||||
const parts = range.replace(/bytes=/, '').split('-');
|
|
||||||
const start = parseInt(parts[0], 10);
|
|
||||||
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
|
|
||||||
if (end > file.file.size) {
|
|
||||||
end = file.file.size - 1;
|
|
||||||
}
|
|
||||||
const chunksize = end - start + 1;
|
|
||||||
|
|
||||||
image = {
|
|
||||||
data: fs.createReadStream(file.path, {
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
}),
|
|
||||||
ext: file.ext,
|
|
||||||
type: file.mime,
|
|
||||||
};
|
|
||||||
|
|
||||||
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
|
|
||||||
reply.header('Accept-Ranges', 'bytes');
|
|
||||||
reply.header('Content-Length', chunksize);
|
|
||||||
reply.code(206);
|
|
||||||
} else {
|
|
||||||
image = {
|
|
||||||
data: fs.createReadStream(file.path),
|
|
||||||
ext: file.ext,
|
|
||||||
type: file.mime,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
|
|
||||||
// image.dataがstreamなら、stream終了後にcleanup
|
|
||||||
image.data.on('end', file.cleanup);
|
|
||||||
image.data.on('close', file.cleanup);
|
|
||||||
} else {
|
|
||||||
// image.dataがstreamでないなら直ちにcleanup
|
|
||||||
file.cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
|
|
||||||
reply.header('Content-Length', file.file.size);
|
|
||||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
|
||||||
reply.header('Content-Disposition',
|
|
||||||
contentDisposition(
|
|
||||||
'inline',
|
|
||||||
correctFilename(file.filename, image.ext),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return image.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.fileRole !== 'original') {
|
if (file.fileRole !== 'original') {
|
||||||
const filename = rename(file.filename, {
|
const filename = rename(file.filename, {
|
||||||
|
@ -284,15 +190,11 @@ export class FileServerService {
|
||||||
|
|
||||||
return fs.createReadStream(file.path);
|
return fs.createReadStream(file.path);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
if ('cleanup' in file) file.cleanup();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
|
private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
|
||||||
let url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
|
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
|
||||||
|
|
||||||
if (typeof url !== 'string') {
|
if (typeof url !== 'string') {
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
|
@ -302,26 +204,25 @@ export class FileServerService {
|
||||||
// アバタークロップなど、どうしてもオリジンである必要がある場合
|
// アバタークロップなど、どうしてもオリジンである必要がある場合
|
||||||
const mustOrigin = 'origin' in request.query;
|
const mustOrigin = 'origin' in request.query;
|
||||||
|
|
||||||
if (this.config.externalMediaProxyEnabled) {
|
if (!this.config.mediaProxy) {
|
||||||
// 外部のメディアプロキシが有効なら、そちらにリダイレクト
|
reply.code(501);
|
||||||
|
|
||||||
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
|
|
||||||
|
|
||||||
const externalURL = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(request.query)) {
|
|
||||||
externalURL.searchParams.append(key, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mustOrigin) {
|
const proxiedURL = new URL(`${this.config.mediaProxy}/?url=${encodeURIComponent(url)}`);
|
||||||
url = `${this.config.mediaProxy}?url=${encodeURIComponent(url)}`;
|
|
||||||
} else {
|
for (const [key, value] of Object.entries(request.query)) {
|
||||||
|
if (key.toLowerCase() === 'url') continue;
|
||||||
|
proxiedURL.searchParams.append(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mustOrigin) {
|
||||||
return await reply.redirect(
|
return await reply.redirect(
|
||||||
externalURL.toString(),
|
proxiedURL.toString(),
|
||||||
301,
|
301,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
|
||||||
|
|
||||||
if (!request.headers['user-agent']) {
|
if (!request.headers['user-agent']) {
|
||||||
throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
|
throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
|
||||||
|
@ -335,201 +236,24 @@ export class FileServerService {
|
||||||
throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
|
throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create temp file
|
// directly proxy request through
|
||||||
const file = await this.getStreamAndTypeFromUrl(url);
|
const res = await fetch(proxiedURL, {
|
||||||
if (file === '404') {
|
headers: {
|
||||||
reply.code(404);
|
'X-Forwarded-For': request.headers['x-forwarded-for']?.at(0) ?? request.ip,
|
||||||
reply.header('Cache-Control', 'max-age=86400');
|
'User-Agent': request.headers['user-agent'],
|
||||||
return reply.sendFile('/dummy.png', assets);
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
if (file === '204') {
|
reply.code(res.status);
|
||||||
reply.code(204);
|
for (const [key, value] of res.headers.entries()) {
|
||||||
reply.header('Cache-Control', 'max-age=86400');
|
reply.header(key, value);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image-with-bmp');
|
|
||||||
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
|
|
||||||
|
|
||||||
if (
|
|
||||||
'emoji' in request.query ||
|
|
||||||
'avatar' in request.query ||
|
|
||||||
'static' in request.query ||
|
|
||||||
'preview' in request.query ||
|
|
||||||
'badge' in request.query
|
|
||||||
) {
|
|
||||||
if (!isConvertibleImage) {
|
|
||||||
// 画像でないなら404でお茶を濁す
|
|
||||||
throw new StatusError('Unexpected mime', 404);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let image: IImageStreamable | null = null;
|
|
||||||
if ('emoji' in request.query || 'avatar' in request.query) {
|
|
||||||
if (!isAnimationConvertibleImage && !('static' in request.query)) {
|
|
||||||
image = {
|
|
||||||
data: fs.createReadStream(file.path),
|
|
||||||
ext: file.ext,
|
|
||||||
type: file.mime,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
|
|
||||||
.resize({
|
|
||||||
height: 'emoji' in request.query ? 128 : 320,
|
|
||||||
withoutEnlargement: true,
|
|
||||||
})
|
|
||||||
.webp(webpDefault);
|
|
||||||
|
|
||||||
image = {
|
|
||||||
data,
|
|
||||||
ext: 'webp',
|
|
||||||
type: 'image/webp',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if ('static' in request.query) {
|
|
||||||
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
|
|
||||||
} else if ('preview' in request.query) {
|
|
||||||
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
|
|
||||||
} else if ('badge' in request.query) {
|
|
||||||
const mask = (await sharpBmp(file.path, file.mime))
|
|
||||||
.resize(96, 96, {
|
|
||||||
fit: 'contain',
|
|
||||||
position: 'centre',
|
|
||||||
withoutEnlargement: false,
|
|
||||||
})
|
|
||||||
.greyscale()
|
|
||||||
.normalise()
|
|
||||||
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
|
|
||||||
.flatten({ background: '#000' })
|
|
||||||
.toColorspace('b-w');
|
|
||||||
|
|
||||||
const stats = await mask.clone().stats();
|
|
||||||
|
|
||||||
if (stats.entropy < 0.1) {
|
|
||||||
// エントロピーがあまりない場合は404にする
|
|
||||||
throw new StatusError('Skip to provide badge', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = sharp({
|
|
||||||
create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
|
|
||||||
})
|
|
||||||
.pipelineColorspace('b-w')
|
|
||||||
.boolean(await mask.png().toBuffer(), 'eor');
|
|
||||||
|
|
||||||
image = {
|
|
||||||
data: await data.png().toBuffer(),
|
|
||||||
ext: 'png',
|
|
||||||
type: 'image/png',
|
|
||||||
};
|
|
||||||
} else if (file.mime === 'image/svg+xml') {
|
|
||||||
image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
|
|
||||||
} else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
|
|
||||||
throw new StatusError('Rejected type', 403, 'Rejected type');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
if (request.headers.range && file.file && file.file.size > 0) {
|
|
||||||
const range = request.headers.range as string;
|
|
||||||
const parts = range.replace(/bytes=/, '').split('-');
|
|
||||||
const start = parseInt(parts[0], 10);
|
|
||||||
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
|
|
||||||
if (end > file.file.size) {
|
|
||||||
end = file.file.size - 1;
|
|
||||||
}
|
|
||||||
const chunksize = end - start + 1;
|
|
||||||
|
|
||||||
image = {
|
|
||||||
data: fs.createReadStream(file.path, {
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
}),
|
|
||||||
ext: file.ext,
|
|
||||||
type: file.mime,
|
|
||||||
};
|
|
||||||
|
|
||||||
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
|
|
||||||
reply.header('Accept-Ranges', 'bytes');
|
|
||||||
reply.header('Content-Length', chunksize);
|
|
||||||
reply.code(206);
|
|
||||||
} else {
|
|
||||||
image = {
|
|
||||||
data: fs.createReadStream(file.path),
|
|
||||||
ext: file.ext,
|
|
||||||
type: file.mime,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('cleanup' in file) {
|
|
||||||
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
|
|
||||||
// image.dataがstreamなら、stream終了後にcleanup
|
|
||||||
image.data.on('end', file.cleanup);
|
|
||||||
image.data.on('close', file.cleanup);
|
|
||||||
} else {
|
|
||||||
// image.dataがstreamでないなら直ちにcleanup
|
|
||||||
file.cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reply.header('Content-Type', image.type);
|
|
||||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
|
||||||
reply.header('Content-Disposition',
|
|
||||||
contentDisposition(
|
|
||||||
'inline',
|
|
||||||
correctFilename(file.filename, image.ext),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return image.data;
|
|
||||||
} catch (e) {
|
|
||||||
if ('cleanup' in file) file.cleanup();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async getStreamAndTypeFromUrl(url: string): Promise<
|
|
||||||
{ state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: MiDriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
|
|
||||||
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; }
|
|
||||||
| '404'
|
|
||||||
| '204'
|
|
||||||
> {
|
|
||||||
if (url.startsWith(`${this.config.url}/files/`)) {
|
|
||||||
const key = url.replace(`${this.config.url}/files/`, '').split('/').shift();
|
|
||||||
if (!key) throw new StatusError('Invalid File Key', 400, 'Invalid File Key');
|
|
||||||
|
|
||||||
return await this.getFileFromKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.downloadAndDetectTypeFromUrl(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async downloadAndDetectTypeFromUrl(url: string): Promise<
|
|
||||||
{ state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
|
|
||||||
> {
|
|
||||||
const [path, cleanup] = await createTemp();
|
|
||||||
try {
|
|
||||||
const { filename } = await this.downloadService.downloadUrl(url, path);
|
|
||||||
|
|
||||||
const { mime, ext } = await this.fileInfoService.detectType(path);
|
|
||||||
|
|
||||||
return {
|
|
||||||
state: 'remote',
|
|
||||||
mime, ext,
|
|
||||||
path, cleanup,
|
|
||||||
filename,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
cleanup();
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
reply.send(res.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async getFileFromKey(key: string): Promise<
|
private async getFileFromKey(key: string): Promise<
|
||||||
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; }
|
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; filename: string; url: string; }
|
||||||
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; }
|
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; }
|
||||||
| '404'
|
| '404'
|
||||||
| '204'
|
| '204'
|
||||||
|
@ -548,15 +272,10 @@ export class FileServerService {
|
||||||
|
|
||||||
if (!file.storedInternal) {
|
if (!file.storedInternal) {
|
||||||
if (!(file.isLink && file.uri)) return '204';
|
if (!(file.isLink && file.uri)) return '204';
|
||||||
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
|
return { state: 'remote',
|
||||||
file.size = (await fs.promises.stat(result.path)).size; // DB file.sizeは正確とは限らないので
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
url: file.uri,
|
|
||||||
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
|
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
|
||||||
file,
|
filename: file.name
|
||||||
filename: file.name,
|
, url: file.uri };
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = this.internalStorageService.resolvePath(key);
|
const path = this.internalStorageService.resolvePath(key);
|
||||||
|
|
|
@ -33,7 +33,6 @@ import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||||
import { makeHstsHook } from './hsts.js';
|
import { makeHstsHook } from './hsts.js';
|
||||||
import { generateCSP } from './csp.js';
|
import { generateCSP } from './csp.js';
|
||||||
import * as prom from 'prom-client';
|
|
||||||
import { sanitizeRequestURI } from '@/misc/log-sanitization.js';
|
import { sanitizeRequestURI } from '@/misc/log-sanitization.js';
|
||||||
import { metricCounter, metricGauge, metricHistogram, MetricsService } from './api/MetricsService.js';
|
import { metricCounter, metricGauge, metricHistogram, MetricsService } from './api/MetricsService.js';
|
||||||
|
|
||||||
|
@ -110,6 +109,11 @@ const mLastSuccessfulRequest = metricGauge({
|
||||||
labelNames: [],
|
labelNames: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// This function is used to determine if a path is safe to redirect to.
|
||||||
|
function redirectSafePath(path: string): boolean {
|
||||||
|
return ['/files/', '/identicon/', '/proxy/', '/static-assets/', '/vite/', '/embed_vite/'].some(prefix => path.startsWith(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerService implements OnApplicationShutdown {
|
export class ServerService implements OnApplicationShutdown {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
@ -274,7 +278,17 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
// Other Security/Privacy Headers
|
// Other Security/Privacy Headers
|
||||||
fastify.addHook('onRequest', (_, reply, done) => {
|
fastify.addHook('onRequest', (_, reply, done) => {
|
||||||
reply.header('x-content-type-options', 'nosniff');
|
reply.header('x-content-type-options', 'nosniff');
|
||||||
reply.header('permissions-policy', 'interest-cohort=()'); // Disable FLoC
|
reply.header('permissions-policy',
|
||||||
|
[
|
||||||
|
'interest-cohort',
|
||||||
|
'encrypted-media',
|
||||||
|
'attribution-reporting',
|
||||||
|
'geolocation', 'microphone', 'camera',
|
||||||
|
'midi', 'payment', 'usb', 'serial',
|
||||||
|
'xr-spatial-tracking'
|
||||||
|
]
|
||||||
|
.map(feature => `${feature}=()`).join(', '));
|
||||||
|
|
||||||
if (this.config.browserSandboxing.strictOriginReferrer) {
|
if (this.config.browserSandboxing.strictOriginReferrer) {
|
||||||
reply.header('referrer-policy', 'strict-origin');
|
reply.header('referrer-policy', 'strict-origin');
|
||||||
}
|
}
|
||||||
|
@ -348,7 +362,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
name: name,
|
name: name,
|
||||||
});
|
});
|
||||||
|
|
||||||
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
|
reply.header('Content-Security-Policy', 'default-src \'none\'');
|
||||||
|
|
||||||
if (emoji == null) {
|
if (emoji == null) {
|
||||||
if ('fallback' in request.query) {
|
if ('fallback' in request.query) {
|
||||||
|
@ -359,16 +373,26 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dbUrl = emoji?.publicUrl || emoji?.originalUrl;
|
||||||
|
const dbUrlParsed = new URL(dbUrl);
|
||||||
|
const instanceUrl = new URL(this.config.url);
|
||||||
|
if (dbUrlParsed.origin === instanceUrl.origin) {
|
||||||
|
if (!redirectSafePath(dbUrlParsed.pathname)) {
|
||||||
|
return await reply.status(508);
|
||||||
|
}
|
||||||
|
return await reply.redirect(dbUrl, 301);
|
||||||
|
}
|
||||||
|
|
||||||
let url: URL;
|
let url: URL;
|
||||||
if ('badge' in request.query) {
|
if ('badge' in request.query) {
|
||||||
url = new URL(`${this.config.mediaProxy}/emoji.png`);
|
url = new URL(`${this.config.mediaProxy}/emoji.png`);
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
|
url.searchParams.set('url', dbUrl);
|
||||||
url.searchParams.set('badge', '1');
|
url.searchParams.set('badge', '1');
|
||||||
} else {
|
} else {
|
||||||
url = new URL(`${this.config.mediaProxy}/emoji.webp`);
|
url = new URL(`${this.config.mediaProxy}/emoji.webp`);
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
|
url.searchParams.set('url', dbUrl);
|
||||||
url.searchParams.set('emoji', '1');
|
url.searchParams.set('emoji', '1');
|
||||||
if ('static' in request.query) url.searchParams.set('static', '1');
|
if ('static' in request.query) url.searchParams.set('static', '1');
|
||||||
}
|
}
|
||||||
|
@ -392,6 +416,16 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
reply.header('Cache-Control', 'public, max-age=86400');
|
reply.header('Cache-Control', 'public, max-age=86400');
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
const dbUrl = user?.avatarUrl ?? this.userEntityService.getIdenticonUrl(user);
|
||||||
|
const dbUrlParsed = new URL(dbUrl);
|
||||||
|
const instanceUrl = new URL(this.config.url);
|
||||||
|
if (dbUrlParsed.origin === instanceUrl.origin) {
|
||||||
|
if (!redirectSafePath(dbUrlParsed.pathname)) {
|
||||||
|
return await reply.status(508);
|
||||||
|
}
|
||||||
|
return await reply.redirect(dbUrl, 301);
|
||||||
|
}
|
||||||
|
|
||||||
reply.redirect(user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user));
|
reply.redirect(user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user));
|
||||||
} else {
|
} else {
|
||||||
reply.redirect('/static-assets/user-unknown.png');
|
reply.redirect('/static-assets/user-unknown.png');
|
||||||
|
|
|
@ -255,7 +255,7 @@ export class SigninApiService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential);
|
const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential, !profile.usePasswordLessLogin);
|
||||||
|
|
||||||
if (authorized) {
|
if (authorized) {
|
||||||
return this.signinService.signin(request, reply, user);
|
return this.signinService.signin(request, reply, user);
|
||||||
|
|
|
@ -46,7 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new Error('cannot delete a root account');
|
throw new Error('cannot delete a root account');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.deleteAccoountService.deleteAccount(user);
|
await this.deleteAccoountService.deleteAccount(user, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,13 +33,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
private deleteAccountService: DeleteAccountService,
|
private deleteAccountService: DeleteAccountService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const user = await this.usersRepository.findOneByOrFail({ id: ps.userId });
|
const user = await this.usersRepository.findOneByOrFail({ id: ps.userId });
|
||||||
if (user.isDeleted) {
|
if (user.isDeleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.deleteAccountService.deleteAccount(user);
|
await this.deleteAccountService.deleteAccount(user, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
]));
|
]));
|
||||||
if (local != null) return local;
|
if (local != null) return local;
|
||||||
|
|
||||||
|
const host = this.utilityService.extractDbHost(uri);
|
||||||
|
|
||||||
|
// local object, not found in db? fail
|
||||||
|
if (this.utilityService.isSelfHost(host)) return null;
|
||||||
|
|
||||||
// リモートから一旦オブジェクトフェッチ
|
// リモートから一旦オブジェクトフェッチ
|
||||||
const resolver = this.apResolverService.createResolver();
|
const resolver = this.apResolverService.createResolver();
|
||||||
const object = await resolver.resolve(uri) as any;
|
const object = await resolver.resolve(uri) as any;
|
||||||
|
@ -134,8 +139,8 @@ 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), resolver) : null,
|
||||||
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, true) : null,
|
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, resolver, true) : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw new ApiError(meta.errors.twoFactorNotEnabled);
|
throw new ApiError(meta.errors.twoFactorNotEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyInfo = await this.webAuthnService.verifyRegistration(me.id, ps.credential);
|
const keyInfo = await this.webAuthnService.verifyRegistration(me.id, ps.credential, !profile.usePasswordLessLogin);
|
||||||
const keyId = keyInfo.credentialID;
|
const keyId = keyInfo.credentialID;
|
||||||
|
|
||||||
await this.userSecurityKeysRepository.insert({
|
await this.userSecurityKeysRepository.insert({
|
||||||
|
|
|
@ -106,6 +106,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
id: In(ps.userIds),
|
id: In(ps.userIds),
|
||||||
} : {
|
} : {
|
||||||
id: In(ps.userIds),
|
id: In(ps.userIds),
|
||||||
|
isDeleted: false,
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,6 @@ export const commonPugFilters = {
|
||||||
throw new Error('Invalid mimeType');
|
throw new Error('Invalid mimeType');
|
||||||
}
|
}
|
||||||
const dataURI = `data:${options.mimeType};base64,${Buffer.from(data).toString('base64')}`;
|
const dataURI = `data:${options.mimeType};base64,${Buffer.from(data).toString('base64')}`;
|
||||||
return `<${options.tagName} data="${dataURI}"></${options.tagName}>`;
|
return `<${options.tagName} src="${dataURI}"></${options.tagName}>`;
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -248,16 +248,6 @@ export class ClientServerService {
|
||||||
fastify.addHook('onRequest', makeHstsHook(host, preload));
|
fastify.addHook('onRequest', makeHstsHook(host, preload));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other Security/Privacy Headers
|
|
||||||
fastify.addHook('onRequest', (_, reply, done) => {
|
|
||||||
reply.header('x-content-type-options', 'nosniff');
|
|
||||||
reply.header('permissions-policy', 'interest-cohort=()'); // Disable FLoC
|
|
||||||
if (this.config.browserSandboxing.strictOriginReferrer ?? true) {
|
|
||||||
reply.header('referrer-policy', 'strict-origin');
|
|
||||||
}
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
// CSP
|
// CSP
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent);
|
console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent);
|
||||||
|
@ -503,6 +493,7 @@ export class ClientServerService {
|
||||||
|
|
||||||
// ServiceWorker
|
// ServiceWorker
|
||||||
fastify.get('/sw.js', async (request, reply) => {
|
fastify.get('/sw.js', async (request, reply) => {
|
||||||
|
reply.header('content-security-policy', `default-src \'self'; connect-src \'self\'${ this.config.mediaProxy ? ` ${new URL(this.config.mediaProxy).origin}` : '' }`);
|
||||||
return await reply.sendFile('/sw.js', swAssets, {
|
return await reply.sendFile('/sw.js', swAssets, {
|
||||||
maxAge: ms('10 minutes'),
|
maxAge: ms('10 minutes'),
|
||||||
});
|
});
|
||||||
|
@ -561,6 +552,7 @@ export class ClientServerService {
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: host ?? IsNull(),
|
host: host ?? IsNull(),
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
|
requireSigninToViewContents: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return user && await this.feedService.packFeed(user);
|
return user && await this.feedService.packFeed(user);
|
||||||
|
@ -611,16 +603,25 @@ export class ClientServerService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region SSR (for crawlers)
|
//#region SSR
|
||||||
// User
|
// User
|
||||||
fastify.get<{ Params: { user: string; sub?: string; } }>('/@:user/:sub?', async (request, reply) => {
|
fastify.get<{ Params: { user: string; sub?: string; } }>('/@:user/:sub?', async (request, reply) => {
|
||||||
const { username, host } = Acct.parse(request.params.user);
|
const { username, host } = Acct.parse(request.params.user);
|
||||||
|
|
||||||
|
if (host) {
|
||||||
|
return await renderBase(reply); // リモートユーザーのページはSSRしない (プライバシーの観点から)
|
||||||
|
}
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: host ?? IsNull(),
|
host: host ?? IsNull(),
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (user?.requireSigninToViewContents) {
|
||||||
|
return await renderBase(reply);
|
||||||
|
}
|
||||||
|
|
||||||
vary(reply.raw, 'Accept');
|
vary(reply.raw, 'Accept');
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
|
@ -636,11 +637,19 @@ export class ClientServerService {
|
||||||
reply.header('X-Robots-Tag', 'noimageai');
|
reply.header('X-Robots-Tag', 'noimageai');
|
||||||
reply.header('X-Robots-Tag', 'noai');
|
reply.header('X-Robots-Tag', 'noai');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _user = await this.userEntityService.pack(user, null, {
|
||||||
|
schema: host ? 'UserLite' : 'UserDetailedNotMe' // リモートユーザーの場合は詳細情報を返さない
|
||||||
|
});
|
||||||
|
|
||||||
return await reply.view('user', {
|
return await reply.view('user', {
|
||||||
user, profile, me,
|
user, profile, me,
|
||||||
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
|
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
|
||||||
sub: request.params.sub,
|
sub: request.params.sub,
|
||||||
...await this.generateCommonPugData(this.meta),
|
...await this.generateCommonPugData(this.meta),
|
||||||
|
clientCtx: htmlSafeJsonStringify({
|
||||||
|
user: _user,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// リモートユーザーなので
|
// リモートユーザーなので
|
||||||
|
@ -654,6 +663,7 @@ export class ClientServerService {
|
||||||
id: request.params.user,
|
id: request.params.user,
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
|
requireSigninToViewContents: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
@ -693,6 +703,9 @@ export class ClientServerService {
|
||||||
// TODO: Let locale changeable by instance setting
|
// TODO: Let locale changeable by instance setting
|
||||||
summary: getNoteSummary(_note),
|
summary: getNoteSummary(_note),
|
||||||
...await this.generateCommonPugData(this.meta),
|
...await this.generateCommonPugData(this.meta),
|
||||||
|
clientCtx: htmlSafeJsonStringify({
|
||||||
|
note: _note,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return await renderBase(reply);
|
return await renderBase(reply);
|
||||||
|
@ -702,9 +715,14 @@ export class ClientServerService {
|
||||||
// Page
|
// Page
|
||||||
fastify.get<{ Params: { user: string; page: string; } }>('/@:user/pages/:page', async (request, reply) => {
|
fastify.get<{ Params: { user: string; page: string; } }>('/@:user/pages/:page', async (request, reply) => {
|
||||||
const { username, host } = Acct.parse(request.params.user);
|
const { username, host } = Acct.parse(request.params.user);
|
||||||
|
|
||||||
|
if (host) {
|
||||||
|
return await renderBase(reply); // リモートユーザーのページはSSRしない
|
||||||
|
}
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: host ?? IsNull(),
|
host: IsNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
|
@ -781,6 +799,9 @@ export class ClientServerService {
|
||||||
profile,
|
profile,
|
||||||
avatarUrl: _clip.user.avatarUrl,
|
avatarUrl: _clip.user.avatarUrl,
|
||||||
...await this.generateCommonPugData(this.meta),
|
...await this.generateCommonPugData(this.meta),
|
||||||
|
clientCtx: htmlSafeJsonStringify({
|
||||||
|
clip: _clip,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return await renderBase(reply);
|
return await renderBase(reply);
|
||||||
|
|
|
@ -118,7 +118,6 @@ export class UrlPreviewService {
|
||||||
private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise<SummalyResult> {
|
private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise<SummalyResult> {
|
||||||
const agent = this.config.proxy
|
const agent = this.config.proxy
|
||||||
? {
|
? {
|
||||||
http: this.httpRequestService.httpAgent,
|
|
||||||
https: this.httpRequestService.httpsAgent,
|
https: this.httpRequestService.httpsAgent,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
@ -145,6 +144,6 @@ export class UrlPreviewService {
|
||||||
contentLengthRequired: meta.urlPreviewRequireContentLength,
|
contentLengthRequired: meta.urlPreviewRequireContentLength,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.httpRequestService.getJson<SummalyResult>(`${proxy}?${queryStr}`);
|
return this.httpRequestService.getJson<SummalyResult>(`${proxy}?${queryStr}`, 'application/json, */*', undefined, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,136 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
class Systemd {
|
||||||
|
constructor(version, cmdline) {
|
||||||
|
this.tty_dom = document.querySelector('#tty');
|
||||||
|
const welcome = document.createElement('div');
|
||||||
|
welcome.className = 'tty-line';
|
||||||
|
welcome.innerText = `YumechiNoKuni ${version} running in Web mode. (+mproxy, +metrics, +csp) cmdline: ${cmdline}`;
|
||||||
|
this.tty_dom.appendChild(welcome);
|
||||||
|
}
|
||||||
|
async start(id, promise) {
|
||||||
|
let state = { state: 'running' };
|
||||||
|
let persistentDom = null;
|
||||||
|
const started = Date.now();
|
||||||
|
const formatRunning = () => {
|
||||||
|
const shiftArray = (arr, n) => {
|
||||||
|
return arr.slice(n).concat(arr.slice(0, n));
|
||||||
|
};
|
||||||
|
const elapsed_secs = Math.floor((Date.now() - started) / 1000);
|
||||||
|
const stars = shiftArray([' ', '*', '*', '*', ' ', ' '], elapsed_secs % 6);
|
||||||
|
const spanStatus = document.createElement('span');
|
||||||
|
spanStatus.innerText = stars.join('');
|
||||||
|
spanStatus.className = 'tty-status-running';
|
||||||
|
const spanMessage = document.createElement('span');
|
||||||
|
spanMessage.innerText = `A start job is running for ${id} (${elapsed_secs}s / no limit)`;
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'tty-line';
|
||||||
|
div.innerHTML = '[';
|
||||||
|
div.appendChild(spanStatus);
|
||||||
|
div.innerHTML += '] ';
|
||||||
|
div.appendChild(spanMessage);
|
||||||
|
return div;
|
||||||
|
};
|
||||||
|
const formatDone = () => {
|
||||||
|
const elapsed_secs = (Date.now() - started) / 1000;
|
||||||
|
const spanStatus = document.createElement('span');
|
||||||
|
spanStatus.innerText = ' OK ';
|
||||||
|
spanStatus.className = 'tty-status-ok';
|
||||||
|
const spanMessage = document.createElement('span');
|
||||||
|
spanMessage.innerText = `Finished ${id} in ${elapsed_secs.toFixed(3)}s`;
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'tty-line';
|
||||||
|
div.innerHTML = '[';
|
||||||
|
div.appendChild(spanStatus);
|
||||||
|
div.innerHTML += '] ';
|
||||||
|
div.appendChild(spanMessage);
|
||||||
|
return div;
|
||||||
|
};
|
||||||
|
const formatFailed = (message) => {
|
||||||
|
const elapsed_secs = (Date.now() - started) / 1000;
|
||||||
|
const spanStatus = document.createElement('span');
|
||||||
|
spanStatus.innerText = 'FAILED';
|
||||||
|
spanStatus.className = 'tty-status-failed';
|
||||||
|
const spanMessage = document.createElement('span');
|
||||||
|
spanMessage.innerText = `Failed ${id} in ${elapsed_secs.toFixed(3)}s: ${message}`;
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'tty-line';
|
||||||
|
div.innerHTML = '[';
|
||||||
|
div.appendChild(spanStatus);
|
||||||
|
div.innerHTML += '] ';
|
||||||
|
div.appendChild(spanMessage);
|
||||||
|
return div;
|
||||||
|
};
|
||||||
|
const render = () => {
|
||||||
|
switch (state.state) {
|
||||||
|
case 'running':
|
||||||
|
if (persistentDom === null) {
|
||||||
|
persistentDom = formatRunning();
|
||||||
|
this.tty_dom.appendChild(persistentDom);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
persistentDom.innerHTML = formatRunning().innerHTML;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'done':
|
||||||
|
if (persistentDom === null) {
|
||||||
|
persistentDom = formatDone();
|
||||||
|
this.tty_dom.appendChild(persistentDom);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
persistentDom.innerHTML = formatDone().innerHTML;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'failed':
|
||||||
|
if (persistentDom === null) {
|
||||||
|
persistentDom = formatFailed(state.message);
|
||||||
|
this.tty_dom.appendChild(persistentDom);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
persistentDom.innerHTML = formatFailed(state.message).innerHTML;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
render();
|
||||||
|
const interval = setInterval(render, 500);
|
||||||
|
try {
|
||||||
|
let res = await promise;
|
||||||
|
state = { state: 'done' };
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
state = { state: 'failed', message: e.message };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
state = { state: 'failed', message: 'Unknown error' };
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
clearInterval(interval);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async startSync(id, func) {
|
||||||
|
return this.start(id, (async () => {
|
||||||
|
return func();
|
||||||
|
})());
|
||||||
|
}
|
||||||
|
emergency_mode(code, details) {``
|
||||||
|
const divPrev = document.createElement('div');
|
||||||
|
divPrev.className = 'tty-line';
|
||||||
|
divPrev.innerText = 'Critical error occurred [' + code + '] : ' + details.message ? details.message : details;
|
||||||
|
this.tty_dom.appendChild(divPrev);
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'tty-line';
|
||||||
|
div.innerText = 'You are in emergency mode. Type Ctrl-Shift-I to view system logs. Clearing local storage by going to /flush and browser settings may help.';
|
||||||
|
this.tty_dom.appendChild(div);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
|
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
|
||||||
(async () => {
|
(async () => {
|
||||||
window.onerror = (e) => {
|
window.onerror = (e) => {
|
||||||
|
@ -16,10 +146,24 @@
|
||||||
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
|
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cmdline = new URLSearchParams(location.search).get('cmdline') || '';
|
||||||
|
const cmdlineArray = cmdline.split(',').map(x => x.trim());
|
||||||
|
if (cmdlineArray.includes('nosplash')) {
|
||||||
|
document.querySelector('#splashIcon').classList.add('hidden');
|
||||||
|
document.querySelector('#splashSpinner').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemd = new Systemd(VERSION, cmdline);
|
||||||
|
|
||||||
|
if (cmdlineArray.includes('leak')) {
|
||||||
|
await systemd.start('Promise Leak Service', new Promise(() => { }));
|
||||||
|
}
|
||||||
|
|
||||||
let forceError = localStorage.getItem('forceError');
|
let forceError = localStorage.getItem('forceError');
|
||||||
if (forceError != null) {
|
if (forceError != null) {
|
||||||
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
|
await systemd.startSync('Force Error Service', () => {
|
||||||
return;
|
throw new Error('This error is forced by having forceError in local storage.');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Detect language & fetch translations
|
//#region Detect language & fetch translations
|
||||||
|
@ -37,7 +181,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const metaRes = await window.fetch('/api/meta', {
|
const metaRes = await systemd.start('Fetch /api/meta',window.fetch('/api/meta', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({}),
|
body: JSON.stringify({}),
|
||||||
credentials: 'omit',
|
credentials: 'omit',
|
||||||
|
@ -45,12 +189,12 @@
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
if (metaRes.status !== 200) {
|
if (metaRes.status !== 200) {
|
||||||
renderError('META_FETCH');
|
renderError('META_FETCH');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const meta = await metaRes.json();
|
const meta = await systemd.start('Parse /api/meta', metaRes.json());
|
||||||
const v = meta.version;
|
const v = meta.version;
|
||||||
if (v == null) {
|
if (v == null) {
|
||||||
renderError('META_FETCH_V');
|
renderError('META_FETCH_V');
|
||||||
|
@ -63,7 +207,7 @@
|
||||||
lang = 'en-US';
|
lang = 'en-US';
|
||||||
}
|
}
|
||||||
|
|
||||||
const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
|
const localRes = await systemd.start('Fetch Locale files', window.fetch(`/assets/locales/${lang}.${v}.json`));
|
||||||
if (localRes.status === 200) {
|
if (localRes.status === 200) {
|
||||||
localStorage.setItem('lang', lang);
|
localStorage.setItem('lang', lang);
|
||||||
localStorage.setItem('locale', await localRes.text());
|
localStorage.setItem('locale', await localRes.text());
|
||||||
|
@ -77,19 +221,25 @@
|
||||||
|
|
||||||
//#region Script
|
//#region Script
|
||||||
async function importAppScript() {
|
async function importAppScript() {
|
||||||
await import(`/vite/${CLIENT_ENTRY}`)
|
await systemd.start('Load App Script', import(`/vite/${CLIENT_ENTRY}`))
|
||||||
.catch(async e => {
|
.catch(async e => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
renderError('APP_IMPORT', e);
|
renderError('APP_IMPORT', e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cmdlineArray.includes('fail')) {
|
||||||
|
await systemd.startSync('Force Error Service', () => {
|
||||||
|
throw new Error('This error is forced by having fail in command line.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
|
// タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
|
||||||
if (document.readyState !== 'loading') {
|
if (document.readyState !== 'loading') {
|
||||||
importAppScript();
|
systemd.start('import App Script', importAppScript());
|
||||||
} else {
|
} else {
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
importAppScript();
|
systemd.start('import App Script', importAppScript());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -97,6 +247,7 @@
|
||||||
//#region Theme
|
//#region Theme
|
||||||
const theme = localStorage.getItem('theme');
|
const theme = localStorage.getItem('theme');
|
||||||
if (theme) {
|
if (theme) {
|
||||||
|
await systemd.startSync('Apply theme', () => {
|
||||||
for (const [k, v] of Object.entries(JSON.parse(theme))) {
|
for (const [k, v] of Object.entries(JSON.parse(theme))) {
|
||||||
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
|
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
|
||||||
|
|
||||||
|
@ -110,6 +261,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const colorScheme = localStorage.getItem('colorScheme');
|
const colorScheme = localStorage.getItem('colorScheme');
|
||||||
if (colorScheme) {
|
if (colorScheme) {
|
||||||
|
@ -134,181 +286,22 @@
|
||||||
|
|
||||||
const customCss = localStorage.getItem('customCss');
|
const customCss = localStorage.getItem('customCss');
|
||||||
if (customCss && customCss.length > 0) {
|
if (customCss && customCss.length > 0) {
|
||||||
|
await systemd.startSync('Apply custom CSS', () => {
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.innerHTML = customCss;
|
style.innerHTML = customCss;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addStyle(styleText) {
|
async function addStyle(styleText) {
|
||||||
|
await systemd.startSync('Apply custom Style', () => {
|
||||||
let css = document.createElement('style');
|
let css = document.createElement('style');
|
||||||
css.appendChild(document.createTextNode(styleText));
|
css.appendChild(document.createTextNode(styleText));
|
||||||
document.head.appendChild(css);
|
document.head.appendChild(css);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderError(code, details) {
|
async function renderError(code, details) {
|
||||||
// Cannot set property 'innerHTML' of null を回避
|
systemd.emergency_mode(code, details);
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
|
|
||||||
}
|
|
||||||
|
|
||||||
let errorsElement = document.getElementById('errors');
|
|
||||||
|
|
||||||
if (!errorsElement) {
|
|
||||||
document.body.innerHTML = `
|
|
||||||
<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
|
||||||
<path d="M12 9v2m0 4v.01"></path>
|
|
||||||
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
|
||||||
</svg>
|
|
||||||
<h1>Failed to load<br>読み込みに失敗しました</h1>
|
|
||||||
<button class="button-big" onclick="location.reload(true);">
|
|
||||||
<span class="button-label-big">Reload / リロード</span>
|
|
||||||
</button>
|
|
||||||
<p><b>The following actions may solve the problem. / 以下を行うと解決する可能性があります。</b></p>
|
|
||||||
<p>Update your os and browser / ブラウザおよびOSを最新バージョンに更新する</p>
|
|
||||||
<p>Disable an adblocker / アドブロッカーを無効にする</p>
|
|
||||||
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
|
|
||||||
<p>(Tor Browser) Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
|
|
||||||
<details style="color: #86b300;">
|
|
||||||
<summary>Other options / その他のオプション</summary>
|
|
||||||
<a href="/flush">
|
|
||||||
<button class="button-small">
|
|
||||||
<span class="button-label-small">Clear preferences and cache</span>
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<a href="/cli">
|
|
||||||
<button class="button-small">
|
|
||||||
<span class="button-label-small">Start the simple client</span>
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<a href="/bios">
|
|
||||||
<button class="button-small">
|
|
||||||
<span class="button-label-small">Start the repair tool</span>
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
</details>
|
|
||||||
<br>
|
|
||||||
<div id="errors"></div>
|
|
||||||
`;
|
|
||||||
errorsElement = document.getElementById('errors');
|
|
||||||
}
|
|
||||||
const detailsElement = document.createElement('details');
|
|
||||||
detailsElement.id = 'errorInfo';
|
|
||||||
detailsElement.innerHTML = `
|
|
||||||
<br>
|
|
||||||
<summary>
|
|
||||||
<code>ERROR CODE: ${code}</code>
|
|
||||||
</summary>
|
|
||||||
<code>${details.toString()} ${JSON.stringify(details)}</code>`;
|
|
||||||
errorsElement.appendChild(detailsElement);
|
|
||||||
addStyle(`
|
|
||||||
* {
|
|
||||||
font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
#misskey_app,
|
|
||||||
#splash {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body,
|
|
||||||
html {
|
|
||||||
background-color: #222;
|
|
||||||
color: #dfddcc;
|
|
||||||
justify-content: center;
|
|
||||||
margin: auto;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0px 12px 0px 12px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-big {
|
|
||||||
background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0));
|
|
||||||
line-height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-big:hover {
|
|
||||||
background: rgb(153, 204, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-small {
|
|
||||||
background: #444;
|
|
||||||
line-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-small:hover {
|
|
||||||
background: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-label-big {
|
|
||||||
color: #222;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.2em;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-label-small {
|
|
||||||
color: rgb(153, 204, 0);
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: rgb(134, 179, 0);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
p,
|
|
||||||
li {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-warning {
|
|
||||||
color: #dec340;
|
|
||||||
height: 4rem;
|
|
||||||
padding-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.5em;
|
|
||||||
margin: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: Fira, FiraCode, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
#errorInfo {
|
|
||||||
background: #333;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
width: 40rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
justify-content: center;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#errorInfo summary {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#errorInfo summary > * {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
|
||||||
#errorInfo {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
}`);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -9,6 +9,32 @@ html {
|
||||||
color: var(--MI_THEME-fg);
|
color: var(--MI_THEME-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tty {
|
||||||
|
z-index: 10001;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tty > .tty-line {
|
||||||
|
font-family: 'Courier New', Courier, monospace !important;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tty > .tty-line .tty-status-ok {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tty > .tty-line .tty-status-failed {
|
||||||
|
color: darkred;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tty > .tty-line .tty-status-running {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
#splash {
|
#splash {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
|
@ -42,7 +68,7 @@ html {
|
||||||
left: 0;
|
left: 0;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 28px;
|
width: 60px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
transform: translateY(70px);
|
transform: translateY(70px);
|
||||||
color: var(--MI_THEME-accent);
|
color: var(--MI_THEME-accent);
|
||||||
|
@ -60,6 +86,16 @@ html {
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
stroke-miterlimit: 1.5;
|
stroke-miterlimit: 1.5;
|
||||||
}
|
}
|
||||||
|
#splashSpinner > .spinner.bg circle {
|
||||||
|
fill:none;
|
||||||
|
stroke:currentColor;
|
||||||
|
stroke-width:24px;
|
||||||
|
}
|
||||||
|
#splashSpinner > .spinner.fg path {
|
||||||
|
fill:none;
|
||||||
|
stroke:currentColor;
|
||||||
|
stroke-width:24px;
|
||||||
|
}
|
||||||
#splashSpinner > .spinner.bg {
|
#splashSpinner > .spinner.bg {
|
||||||
opacity: 0.275;
|
opacity: 0.275;
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ html.embed.noborder #splash {
|
||||||
left: 0;
|
left: 0;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 28px;
|
width: 60px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
transform: translateY(70px);
|
transform: translateY(70px);
|
||||||
color: var(--MI_THEME-accent);
|
color: var(--MI_THEME-accent);
|
||||||
|
@ -82,6 +82,16 @@ html.embed.noborder #splash {
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
stroke-miterlimit: 1.5;
|
stroke-miterlimit: 1.5;
|
||||||
}
|
}
|
||||||
|
#splashSpinner > .spinner.bg circle {
|
||||||
|
fill:none;
|
||||||
|
stroke:currentColor;
|
||||||
|
stroke-width:24px;
|
||||||
|
}
|
||||||
|
#splashSpinner > .spinner.fg path {
|
||||||
|
fill:none;
|
||||||
|
stroke:currentColor;
|
||||||
|
stroke-width:24px;
|
||||||
|
}
|
||||||
#splashSpinner > .spinner.bg {
|
#splashSpinner > .spinner.bg {
|
||||||
opacity: 0.275;
|
opacity: 0.275;
|
||||||
}
|
}
|
||||||
|
|
151
packages/backend/src/server/web/systemd.ts
Normal file
151
packages/backend/src/server/web/systemd.ts
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
export class Systemd {
|
||||||
|
private tty_dom: HTMLDivElement;
|
||||||
|
constructor() {
|
||||||
|
this.tty_dom = document.querySelector('#tty') as HTMLDivElement;
|
||||||
|
|
||||||
|
console.log('Systemd started');
|
||||||
|
}
|
||||||
|
|
||||||
|
async start<T>(id: string, promise: Promise<T>): Promise<T> {
|
||||||
|
|
||||||
|
let state: {
|
||||||
|
state: 'running'
|
||||||
|
} | {
|
||||||
|
state: 'done'
|
||||||
|
} | {
|
||||||
|
state: 'failed'
|
||||||
|
message: string
|
||||||
|
} = { state: 'running' };
|
||||||
|
|
||||||
|
let persistentDom : HTMLDivElement | null = null;
|
||||||
|
|
||||||
|
const started = Date.now();
|
||||||
|
|
||||||
|
const formatRunning = () => {
|
||||||
|
const shiftArray = <T>(arr: T[], n: number): T[] => {
|
||||||
|
return arr.slice(n).concat(arr.slice(0, n));
|
||||||
|
};
|
||||||
|
|
||||||
|
const elapsed_secs = Math.floor((Date.now() - started) / 1000);
|
||||||
|
const stars = shiftArray(['*', '*', '*', ' ', ' ', ' '], elapsed_secs % 6);
|
||||||
|
|
||||||
|
const spanStatus = document.createElement('span');
|
||||||
|
|
||||||
|
spanStatus.innerText = stars.join('');
|
||||||
|
spanStatus.className = 'tty-status-running';
|
||||||
|
|
||||||
|
const spanMessage = document.createElement('span');
|
||||||
|
spanMessage.innerText = `A start job is running for ${id} (${elapsed_secs}s / no limit)`;
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'tty-line';
|
||||||
|
div.innerHTML = '[';
|
||||||
|
div.appendChild(spanStatus);
|
||||||
|
div.innerHTML += '] ';
|
||||||
|
div.appendChild(spanMessage);
|
||||||
|
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDone = () => {
|
||||||
|
const elapsed_secs = Math.floor((Date.now() - started) / 1000);
|
||||||
|
|
||||||
|
const spanStatus = document.createElement('span');
|
||||||
|
spanStatus.innerText = ' OK ';
|
||||||
|
spanStatus.className = 'tty-status-ok';
|
||||||
|
|
||||||
|
const spanMessage = document.createElement('span');
|
||||||
|
spanMessage.innerText = `Finished ${id} in ${elapsed_secs}s`;
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'tty-line';
|
||||||
|
div.innerHTML = '[';
|
||||||
|
div.appendChild(spanStatus);
|
||||||
|
div.innerHTML += '] ';
|
||||||
|
div.appendChild(spanMessage);
|
||||||
|
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFailed = (message: string) => {
|
||||||
|
const elapsed_secs = Math.floor((Date.now() - started) / 1000);
|
||||||
|
|
||||||
|
const spanStatus = document.createElement('span');
|
||||||
|
spanStatus.innerText = 'FAILED';
|
||||||
|
spanStatus.className = 'tty-status-failed';
|
||||||
|
|
||||||
|
const spanMessage = document.createElement('span');
|
||||||
|
spanMessage.innerText = `Failed ${id} in ${elapsed_secs}s: ${message}`;
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'tty-line';
|
||||||
|
div.innerHTML = '[';
|
||||||
|
div.appendChild(spanStatus);
|
||||||
|
div.innerHTML += '] ';
|
||||||
|
div.appendChild(spanMessage);
|
||||||
|
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
switch (state.state) {
|
||||||
|
case 'running':
|
||||||
|
if (persistentDom === null) {
|
||||||
|
persistentDom = formatRunning();
|
||||||
|
this.tty_dom.appendChild(persistentDom);
|
||||||
|
} else {
|
||||||
|
persistentDom.innerHTML = formatRunning().innerHTML;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'done':
|
||||||
|
if (persistentDom === null) {
|
||||||
|
persistentDom = formatDone();
|
||||||
|
this.tty_dom.appendChild(persistentDom);
|
||||||
|
} else {
|
||||||
|
persistentDom.innerHTML = formatDone().innerHTML;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'failed':
|
||||||
|
if (persistentDom === null) {
|
||||||
|
persistentDom = formatFailed(state.message);
|
||||||
|
this.tty_dom.appendChild(persistentDom);
|
||||||
|
} else {
|
||||||
|
persistentDom.innerHTML = formatFailed(state.message).innerHTML;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render();
|
||||||
|
const interval = setInterval(render, 500);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let res = await promise;
|
||||||
|
state = { state: 'done' };
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
state = { state: 'failed', message: e.message };
|
||||||
|
} else {
|
||||||
|
state = { state: 'failed', message: 'Unknown error' };
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
clearInterval(interval);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startSync<T>(id: string, func: () => T): Promise<T> {
|
||||||
|
return this.start(id, (async () => {
|
||||||
|
return func();
|
||||||
|
})());
|
||||||
|
}
|
||||||
|
|
||||||
|
public emergency_mode() {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'tty-line';
|
||||||
|
div.innerText = 'You are in emergency mode. Type Ctrl-Shift-I to view logs.';
|
||||||
|
this.tty_dom.appendChild(div);
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,18 +56,8 @@ html(class='embed')
|
||||||
br
|
br
|
||||||
| Please turn on your JavaScript
|
| Please turn on your JavaScript
|
||||||
div#splash
|
div#splash
|
||||||
|
div#tty
|
||||||
img#splashIcon(src= icon || '/static-assets/splash.png')
|
img#splashIcon(src= icon || '/static-assets/splash.png')
|
||||||
div#splashSpinner
|
div#splashSpinner
|
||||||
:dataTag(tagName='img' mimeType='image/svg+xml')
|
<span>Loading...</span>
|
||||||
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g transform="matrix(1,0,0,1,12,12)">
|
|
||||||
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
:dataTag(tagName='img' mimeType='image/svg+xml')
|
|
||||||
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g transform="matrix(1,0,0,1,12,12)">
|
|
||||||
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
block content
|
block content
|
||||||
|
|
|
@ -9,17 +9,6 @@ block loadClientEntry
|
||||||
doctype html
|
doctype html
|
||||||
|
|
||||||
//
|
//
|
||||||
-
|
|
||||||
_____ _ _
|
|
||||||
| |_|___ ___| |_ ___ _ _
|
|
||||||
| | | | |_ -|_ -| '_| -_| | |
|
|
||||||
|_|_|_|_|___|___|_,_|___|_ |
|
|
||||||
|___|
|
|
||||||
Thank you for using Misskey!
|
|
||||||
If you are reading this message... how about joining the development?
|
|
||||||
https://github.com/misskey-dev/misskey
|
|
||||||
|
|
||||||
|
|
||||||
html
|
html
|
||||||
|
|
||||||
head
|
head
|
||||||
|
@ -73,7 +62,9 @@ html
|
||||||
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
||||||
!= metaJson
|
!= metaJson
|
||||||
|
|
||||||
script(integrity=bootJS.integrity) !{bootJS.content}
|
script(type='application/json' id='misskey_clientCtx' data-generated-at=now)
|
||||||
|
!= clientCtx
|
||||||
|
|
||||||
|
|
||||||
body
|
body
|
||||||
noscript: p
|
noscript: p
|
||||||
|
@ -81,18 +72,11 @@ html
|
||||||
br
|
br
|
||||||
| Please turn on your JavaScript
|
| Please turn on your JavaScript
|
||||||
div#splash
|
div#splash
|
||||||
|
div#tty
|
||||||
img#splashIcon(src= icon || '/static-assets/splash.png')
|
img#splashIcon(src= icon || '/static-assets/splash.png')
|
||||||
div#splashSpinner
|
div#splashSpinner
|
||||||
:dataTag(tagName='img' mimeType='image/svg+xml')
|
<span>Loading...</span>
|
||||||
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g transform="matrix(1,0,0,1,12,12)">
|
script(integrity=bootJS.integrity) !{bootJS.content}
|
||||||
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
:dataTag(tagName='img' mimeType='image/svg+xml')
|
|
||||||
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g transform="matrix(1,0,0,1,12,12)">
|
|
||||||
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
block content
|
block content
|
||||||
|
|
|
@ -1,17 +1,6 @@
|
||||||
doctype html
|
doctype html
|
||||||
|
|
||||||
//
|
//
|
||||||
-
|
|
||||||
_____ _ _
|
|
||||||
| |_|___ ___| |_ ___ _ _
|
|
||||||
| | | | |_ -|_ -| '_| -_| | |
|
|
||||||
|_|_|_|_|___|___|_,_|___|_ |
|
|
||||||
|___|
|
|
||||||
Thank you for using Misskey!
|
|
||||||
If you are reading this message... how about joining the development?
|
|
||||||
https://github.com/misskey-dev/misskey
|
|
||||||
|
|
||||||
|
|
||||||
html
|
html
|
||||||
|
|
||||||
head
|
head
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { CoreModule } from '@/core/CoreModule.js';
|
import { CoreModule } from '@/core/CoreModule.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
|
import { yumeNormalizeObject, type IActor, type IApDocument, type ICollection, type IObject, type IPost } from '@/core/activitypub/type.js';
|
||||||
import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js';
|
import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
|
@ -42,6 +42,7 @@ function createRandomActor({ actorHost = host } = {}): NonTransientIActor {
|
||||||
id: actorId,
|
id: actorId,
|
||||||
type: 'Person',
|
type: 'Person',
|
||||||
preferredUsername,
|
preferredUsername,
|
||||||
|
__yume_normalized_object: true,
|
||||||
inbox: `${actorId}/inbox`,
|
inbox: `${actorId}/inbox`,
|
||||||
outbox: `${actorId}/outbox`,
|
outbox: `${actorId}/outbox`,
|
||||||
};
|
};
|
||||||
|
@ -55,6 +56,7 @@ function createRandomNote(actor: NonTransientIActor): NonTransientIPost {
|
||||||
id: noteId,
|
id: noteId,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
attributedTo: actor.id,
|
attributedTo: actor.id,
|
||||||
|
__yume_normalized_object: true,
|
||||||
content: 'test test foo',
|
content: 'test test foo',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -71,6 +73,7 @@ function createRandomFeaturedCollection(actor: NonTransientIActor, length: numbe
|
||||||
type: 'Collection',
|
type: 'Collection',
|
||||||
id: actor.outbox as string,
|
id: actor.outbox as string,
|
||||||
totalItems: items.length,
|
totalItems: items.length,
|
||||||
|
__yume_normalized_object: true,
|
||||||
items,
|
items,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -162,6 +165,34 @@ describe('ActivityPub', () => {
|
||||||
content: 'あ',
|
content: 'あ',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const punnyPost = {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: `https://あ.com/users/あ`,
|
||||||
|
type: 'Note',
|
||||||
|
attributedTo: actor.id,
|
||||||
|
to: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
content: 'あ',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('punnyPost normalization', async () => {
|
||||||
|
const normalized = yumeNormalizeObject(punnyPost);
|
||||||
|
assert.strictEqual(normalized.id, 'https://xn--l8j.com/users/あ');
|
||||||
|
});
|
||||||
|
|
||||||
|
const portedHost = {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: `https://あ.com:12443/users/${secureRndstr(8)}`,
|
||||||
|
type: 'Note',
|
||||||
|
to: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
content: 'あ',
|
||||||
|
}
|
||||||
|
|
||||||
|
test('actor with port should be rejected', async () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
yumeNormalizeObject(portedHost);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('Minimum Actor', async () => {
|
test('Minimum Actor', async () => {
|
||||||
resolver.register(actor.id, actor);
|
resolver.register(actor.id, actor);
|
||||||
|
|
||||||
|
@ -176,7 +207,7 @@ describe('ActivityPub', () => {
|
||||||
resolver.register(actor.id, actor);
|
resolver.register(actor.id, actor);
|
||||||
resolver.register(post.id, post);
|
resolver.register(post.id, post);
|
||||||
|
|
||||||
const note = await noteService.createNote(post.id, resolver, true);
|
const note = await noteService.createNote(post.id, undefined, resolver, true);
|
||||||
|
|
||||||
assert.deepStrictEqual(note?.uri, post.id);
|
assert.deepStrictEqual(note?.uri, post.id);
|
||||||
assert.deepStrictEqual(note.visibility, 'public');
|
assert.deepStrictEqual(note.visibility, 'public');
|
||||||
|
@ -220,6 +251,7 @@ describe('ActivityPub', () => {
|
||||||
type: 'OrderedCollection',
|
type: 'OrderedCollection',
|
||||||
totalItems: 0,
|
totalItems: 0,
|
||||||
first: `${actor.id}/following?page=1`,
|
first: `${actor.id}/following?page=1`,
|
||||||
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
actor.followers = `${actor.id}/followers`;
|
actor.followers = `${actor.id}/followers`;
|
||||||
|
|
||||||
|
@ -229,6 +261,7 @@ describe('ActivityPub', () => {
|
||||||
type: 'OrderedCollection',
|
type: 'OrderedCollection',
|
||||||
totalItems: 0,
|
totalItems: 0,
|
||||||
first: `${actor.followers}?page=1`,
|
first: `${actor.followers}?page=1`,
|
||||||
|
__yume_normalized_object: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = await personService.createPerson(actor.id, resolver);
|
const user = await personService.createPerson(actor.id, resolver);
|
||||||
|
@ -244,6 +277,7 @@ describe('ActivityPub', () => {
|
||||||
id: `${actor.id}/following`,
|
id: `${actor.id}/following`,
|
||||||
type: 'OrderedCollection',
|
type: 'OrderedCollection',
|
||||||
totalItems: 0,
|
totalItems: 0,
|
||||||
|
__yume_normalized_object: true,
|
||||||
// first: …
|
// first: …
|
||||||
};
|
};
|
||||||
actor.followers = `${actor.id}/followers`;
|
actor.followers = `${actor.id}/followers`;
|
||||||
|
@ -336,7 +370,7 @@ describe('ActivityPub', () => {
|
||||||
resolver.register(actor.featured, featured);
|
resolver.register(actor.featured, featured);
|
||||||
resolver.register(firstNote.id, firstNote);
|
resolver.register(firstNote.id, firstNote);
|
||||||
|
|
||||||
const note = await noteService.createNote(firstNote.id as string, resolver);
|
const note = await noteService.createNote(firstNote.id as string, undefined, resolver);
|
||||||
assert.strictEqual(note?.uri, firstNote.id);
|
assert.strictEqual(note?.uri, firstNote.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -348,6 +382,7 @@ describe('ActivityPub', () => {
|
||||||
mediaType: 'image/png',
|
mediaType: 'image/png',
|
||||||
url: 'http://host1.test/foo.png',
|
url: 'http://host1.test/foo.png',
|
||||||
name: '',
|
name: '',
|
||||||
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
const driveFile = await imageService.createImage(
|
const driveFile = await imageService.createImage(
|
||||||
await createRandomRemoteUser(resolver, personService),
|
await createRandomRemoteUser(resolver, personService),
|
||||||
|
@ -361,6 +396,7 @@ describe('ActivityPub', () => {
|
||||||
url: 'http://host1.test/bar.png',
|
url: 'http://host1.test/bar.png',
|
||||||
name: '',
|
name: '',
|
||||||
sensitive: true,
|
sensitive: true,
|
||||||
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
const sensitiveDriveFile = await imageService.createImage(
|
const sensitiveDriveFile = await imageService.createImage(
|
||||||
await createRandomRemoteUser(resolver, personService),
|
await createRandomRemoteUser(resolver, personService),
|
||||||
|
@ -377,6 +413,7 @@ describe('ActivityPub', () => {
|
||||||
mediaType: 'image/png',
|
mediaType: 'image/png',
|
||||||
url: 'http://host1.test/foo.png',
|
url: 'http://host1.test/foo.png',
|
||||||
name: '',
|
name: '',
|
||||||
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
const driveFile = await imageService.createImage(
|
const driveFile = await imageService.createImage(
|
||||||
await createRandomRemoteUser(resolver, personService),
|
await createRandomRemoteUser(resolver, personService),
|
||||||
|
@ -390,6 +427,7 @@ describe('ActivityPub', () => {
|
||||||
url: 'http://host1.test/bar.png',
|
url: 'http://host1.test/bar.png',
|
||||||
name: '',
|
name: '',
|
||||||
sensitive: true,
|
sensitive: true,
|
||||||
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
const sensitiveDriveFile = await imageService.createImage(
|
const sensitiveDriveFile = await imageService.createImage(
|
||||||
await createRandomRemoteUser(resolver, personService),
|
await createRandomRemoteUser(resolver, personService),
|
||||||
|
@ -406,6 +444,7 @@ describe('ActivityPub', () => {
|
||||||
mediaType: 'image/png',
|
mediaType: 'image/png',
|
||||||
url: 'http://host1.test/foo.png',
|
url: 'http://host1.test/foo.png',
|
||||||
name: '',
|
name: '',
|
||||||
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
const driveFile = await imageService.createImage(
|
const driveFile = await imageService.createImage(
|
||||||
await createRandomRemoteUser(resolver, personService),
|
await createRandomRemoteUser(resolver, personService),
|
||||||
|
@ -419,6 +458,7 @@ describe('ActivityPub', () => {
|
||||||
url: 'http://host1.test/bar.png',
|
url: 'http://host1.test/bar.png',
|
||||||
name: '',
|
name: '',
|
||||||
sensitive: true,
|
sensitive: true,
|
||||||
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
const sensitiveDriveFile = await imageService.createImage(
|
const sensitiveDriveFile = await imageService.createImage(
|
||||||
await createRandomRemoteUser(resolver, personService),
|
await createRandomRemoteUser(resolver, personService),
|
||||||
|
@ -431,6 +471,7 @@ describe('ActivityPub', () => {
|
||||||
const linkObject: IObject = {
|
const linkObject: IObject = {
|
||||||
type: 'Link',
|
type: 'Link',
|
||||||
href: 'https://example.com/',
|
href: 'https://example.com/',
|
||||||
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
const driveFile = await imageService.createImage(
|
const driveFile = await imageService.createImage(
|
||||||
await createRandomRemoteUser(resolver, personService),
|
await createRandomRemoteUser(resolver, personService),
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>[DEV] Loading...</title>
|
<title>[DEV] Loading...</title>
|
||||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
<!--
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/;
|
content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/;
|
||||||
|
@ -25,6 +25,7 @@
|
||||||
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;
|
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;
|
||||||
frame-src *;"
|
frame-src *;"
|
||||||
/>
|
/>
|
||||||
|
-->
|
||||||
<meta property="og:site_name" content="[DEV BUILD] Misskey" />
|
<meta property="og:site_name" content="[DEV BUILD] Misskey" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -45,7 +45,8 @@ const queryingKey = ref(true);
|
||||||
async function queryKey() {
|
async function queryKey() {
|
||||||
queryingKey.value = true;
|
queryingKey.value = true;
|
||||||
await webAuthnRequest(props.credentialRequest)
|
await webAuthnRequest(props.credentialRequest)
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
return Promise.reject(null);
|
return Promise.reject(null);
|
||||||
})
|
})
|
||||||
.then((credential) => {
|
.then((credential) => {
|
||||||
|
|
|
@ -41,15 +41,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch>
|
<MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder :defaultOpen="true">
|
|
||||||
<template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template>
|
|
||||||
<template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
|
|
||||||
|
|
||||||
<a href="https://misskey-hub.net/docs/for-users/onboarding/warning/" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a>
|
|
||||||
|
|
||||||
<MkSwitch :modelValue="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree @update:modelValue="updateAgreeNote">{{ i18n.ts.agree }}</MkSwitch>
|
|
||||||
</MkFolder>
|
|
||||||
|
|
||||||
<div v-if="!agreed" style="text-align: center;">{{ i18n.ts.pleaseAgreeAllToContinue }}</div>
|
<div v-if="!agreed" style="text-align: center;">{{ i18n.ts.pleaseAgreeAllToContinue }}</div>
|
||||||
|
|
||||||
<div class="_buttonsCenter">
|
<div class="_buttonsCenter">
|
||||||
|
@ -77,10 +68,9 @@ const availablePrivacyPolicy = instance.privacyPolicyUrl != null && instance.pri
|
||||||
|
|
||||||
const agreeServerRules = ref(false);
|
const agreeServerRules = ref(false);
|
||||||
const agreeTosAndPrivacyPolicy = ref(false);
|
const agreeTosAndPrivacyPolicy = ref(false);
|
||||||
const agreeNote = ref(false);
|
|
||||||
|
|
||||||
const agreed = computed(() => {
|
const agreed = computed(() => {
|
||||||
return (!availableServerRules || agreeServerRules.value) && ((!availableTos && !availablePrivacyPolicy) || agreeTosAndPrivacyPolicy.value) && agreeNote.value;
|
return (!availableServerRules || agreeServerRules.value) && ((!availableTos && !availablePrivacyPolicy) || agreeTosAndPrivacyPolicy.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -129,20 +119,6 @@ async function updateAgreeTosAndPrivacyPolicy(v: boolean) {
|
||||||
agreeTosAndPrivacyPolicy.value = false;
|
agreeTosAndPrivacyPolicy.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateAgreeNote(v: boolean) {
|
|
||||||
if (v) {
|
|
||||||
const confirm = await os.confirm({
|
|
||||||
type: 'question',
|
|
||||||
title: i18n.ts.doYouAgree,
|
|
||||||
text: i18n.tsx.iHaveReadXCarefullyAndAgree({ x: i18n.ts.basicNotesBeforeCreateAccount }),
|
|
||||||
});
|
|
||||||
if (confirm.canceled) return;
|
|
||||||
agreeNote.value = true;
|
|
||||||
} else {
|
|
||||||
agreeNote.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -25,17 +25,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, ref } from 'vue';
|
import { computed, defineAsyncComponent, inject, ref } from 'vue';
|
||||||
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
|
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { customEmojisMap } from '@/custom-emojis.js';
|
import { customEmojisMap } from '@/custom-emojis.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import { $i } from '@/account.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -125,9 +126,31 @@ function onClick(ev: MouseEvent) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ($i?.isModerator ?? $i?.isAdmin) {
|
||||||
|
menuItems.push({
|
||||||
|
text: i18n.ts.edit,
|
||||||
|
icon: 'ti ti-pencil',
|
||||||
|
action: async () => {
|
||||||
|
await edit(props.name);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function edit(name: string) {
|
||||||
|
const emoji = await misskeyApi('emoji', {
|
||||||
|
name: name,
|
||||||
|
});
|
||||||
|
const { dispose } = os.popup(defineAsyncComponent(() => import('@/pages/emoji-edit-dialog.vue')), {
|
||||||
|
emoji: emoji,
|
||||||
|
}, {
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -8,12 +8,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.container">
|
<div :class="$style.container">
|
||||||
<svg :class="[$style.spinner, $style.bg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
|
<svg :class="[$style.spinner, $style.bg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g transform="matrix(1.125,0,0,1.125,12,12)">
|
<g transform="matrix(1.125,0,0,1.125,12,12)">
|
||||||
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
|
<circle cx="64" cy="64" r="64" :class="[$style.bgcircle]"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<svg :class="[$style.spinner, $style.fg, { [$style.static]: static }]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
|
<svg :class="[$style.spinner, $style.fg, { [$style.static]: static }]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g transform="matrix(1.125,0,0,1.125,12,12)">
|
<g transform="matrix(1.125,0,0,1.125,12,12)">
|
||||||
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
|
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" :class="[$style.fgpath]"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,4 +109,16 @@ const props = withDefaults(defineProps<{
|
||||||
animation-play-state: paused;
|
animation-play-state: paused;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bgcircle {
|
||||||
|
fill:none;
|
||||||
|
stroke:currentColor;
|
||||||
|
stroke-width:21.33px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fgpath {
|
||||||
|
fill:none;
|
||||||
|
stroke:currentColor;
|
||||||
|
stroke-width:21.33px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>[DEV] Loading...</title>
|
<title>[DEV] Loading...</title>
|
||||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||||
|
<!--
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/;
|
content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/;
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;
|
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;
|
||||||
frame-src *;"
|
frame-src *;"
|
||||||
/>
|
/>
|
||||||
|
-->
|
||||||
<meta property="og:site_name" content="[DEV BUILD] Misskey" />
|
<meta property="og:site_name" content="[DEV BUILD] Misskey" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<FormLink to="/about-misskey">
|
<FormLink to="/about-misskey">
|
||||||
<template #icon><i class="ti ti-info-circle"></i></template>
|
<template #icon><i class="ti ti-info-circle"></i></template>
|
||||||
{{ i18n.ts.aboutMisskey }}
|
{{ i18n.ts.aboutMisskey }} (Upstream)
|
||||||
</FormLink>
|
</FormLink>
|
||||||
<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
|
<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
|
||||||
<template #icon><i class="ti ti-code"></i></template>
|
<template #icon><i class="ti ti-code"></i></template>
|
||||||
|
|
|
@ -33,25 +33,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, watch, provide, ref } from 'vue';
|
import { computed, watch, provide, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { url } from '@@/js/config.js';
|
||||||
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import MkNotes from '@/components/MkNotes.vue';
|
import MkNotes from '@/components/MkNotes.vue';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { url } from '@@/js/config.js';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { clipsCache } from '@/cache.js';
|
import { clipsCache } from '@/cache.js';
|
||||||
import { isSupportShare } from '@/scripts/navigator.js';
|
import { isSupportShare } from '@/scripts/navigator.js';
|
||||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||||
import { genEmbedCode } from '@/scripts/get-embed-code.js';
|
import { genEmbedCode } from '@/scripts/get-embed-code.js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import { getServerContext } from '@/server-context.js';
|
||||||
|
|
||||||
|
const CTX_CLIP = getServerContext('clip');
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
clipId: string,
|
clipId: string,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const clip = ref<Misskey.entities.Clip | null>(null);
|
const clip = ref<Misskey.entities.Clip | null>(CTX_CLIP);
|
||||||
const favorited = ref(false);
|
const favorited = ref(false);
|
||||||
const pagination = {
|
const pagination = {
|
||||||
endpoint: 'clips/notes' as const,
|
endpoint: 'clips/notes' as const,
|
||||||
|
@ -64,6 +67,11 @@ const pagination = {
|
||||||
const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId));
|
const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId));
|
||||||
|
|
||||||
watch(() => props.clipId, async () => {
|
watch(() => props.clipId, async () => {
|
||||||
|
if (CTX_CLIP && CTX_CLIP.id === props.clipId) {
|
||||||
|
clip.value = CTX_CLIP;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
clip.value = await misskeyApi('clips/show', {
|
clip.value = await misskeyApi('clips/show', {
|
||||||
clipId: props.clipId,
|
clipId: props.clipId,
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,18 +15,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
emoji: Misskey.entities.EmojiSimple;
|
emoji: Misskey.entities.EmojiSimple;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function menu(ev) {
|
function menu(ev) {
|
||||||
os.popupMenu([{
|
const menuItems: MenuItem[] = [];
|
||||||
|
menuItems.push({
|
||||||
type: 'label',
|
type: 'label',
|
||||||
text: ':' + props.emoji.name + ':',
|
text: ':' + props.emoji.name + ':',
|
||||||
}, {
|
}, {
|
||||||
|
@ -48,8 +52,28 @@ function menu(ev) {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}], ev.currentTarget ?? ev.target);
|
});
|
||||||
|
|
||||||
|
if ($i?.isModerator ?? $i?.isAdmin) {
|
||||||
|
menuItems.push({
|
||||||
|
text: i18n.ts.edit,
|
||||||
|
icon: 'ti ti-pencil',
|
||||||
|
action: () => {
|
||||||
|
edit(props.emoji);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const edit = async (emoji) => {
|
||||||
|
const { dispose } = os.popup(defineAsyncComponent(() => import('@/pages/emoji-edit-dialog.vue')), {
|
||||||
|
emoji: emoji,
|
||||||
|
}, {
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -62,13 +62,16 @@ import { dateString } from '@/filters/date.js';
|
||||||
import MkClipPreview from '@/components/MkClipPreview.vue';
|
import MkClipPreview from '@/components/MkClipPreview.vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
|
import { getServerContext } from '@/server-context.js';
|
||||||
|
|
||||||
|
const CTX_NOTE = getServerContext('note');
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
noteId: string;
|
noteId: string;
|
||||||
initialTab?: string;
|
initialTab?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const note = ref<null | Misskey.entities.Note>();
|
const note = ref<null | Misskey.entities.Note>(CTX_NOTE);
|
||||||
const clips = ref<Misskey.entities.Clip[]>();
|
const clips = ref<Misskey.entities.Clip[]>();
|
||||||
const showPrev = ref<'user' | 'channel' | false>(false);
|
const showPrev = ref<'user' | 'channel' | false>(false);
|
||||||
const showNext = ref<'user' | 'channel' | false>(false);
|
const showNext = ref<'user' | 'channel' | false>(false);
|
||||||
|
@ -116,6 +119,12 @@ function fetchNote() {
|
||||||
showPrev.value = false;
|
showPrev.value = false;
|
||||||
showNext.value = false;
|
showNext.value = false;
|
||||||
note.value = null;
|
note.value = null;
|
||||||
|
|
||||||
|
if (CTX_NOTE && CTX_NOTE.id === props.noteId) {
|
||||||
|
note.value = CTX_NOTE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
misskeyApi('notes/show', {
|
misskeyApi('notes/show', {
|
||||||
noteId: props.noteId,
|
noteId: props.noteId,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
|
|
|
@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div><MkSparkle><Mfm :plain="true" :text="user.followedMessage" :author="user"/></MkSparkle></div>
|
<div><MkSparkle><Mfm :plain="true" :text="user.followedMessage" :author="user"/></MkSparkle></div>
|
||||||
</MkFukidashi>
|
</MkFukidashi>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="user.roles.length > 0" class="roles">
|
<div v-if="user.roles && user.roles.length > 0" class="roles">
|
||||||
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">
|
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">
|
||||||
<MkA v-adaptive-bg :to="`/roles/${role.id}`">
|
<MkA v-adaptive-bg :to="`/roles/${role.id}`">
|
||||||
<img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/>
|
<img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/>
|
||||||
|
@ -96,7 +96,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<dt class="name"><i class="ti ti-cake ti-fw"></i> {{ i18n.ts.birthday }}</dt>
|
<dt class="name"><i class="ti ti-cake ti-fw"></i> {{ i18n.ts.birthday }}</dt>
|
||||||
<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.tsx.yearsOld({ age }) }})</dd>
|
<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.tsx.yearsOld({ age }) }})</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<dl class="field">
|
<dl v-if="user.createdAt" class="field">
|
||||||
<dt class="name"><i class="ti ti-calendar ti-fw"></i> {{ i18n.ts.registeredDate }}</dt>
|
<dt class="name"><i class="ti ti-calendar ti-fw"></i> {{ i18n.ts.registeredDate }}</dt>
|
||||||
<dd class="value">{{ dateString(user.createdAt) }} (<MkTime :time="user.createdAt"/>)</dd>
|
<dd class="value">{{ dateString(user.createdAt) }} (<MkTime :time="user.createdAt"/>)</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
|
@ -39,6 +39,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||||
|
import { getServerContext } from '@/server-context.js';
|
||||||
|
|
||||||
const XHome = defineAsyncComponent(() => import('./home.vue'));
|
const XHome = defineAsyncComponent(() => import('./home.vue'));
|
||||||
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
|
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
|
||||||
|
@ -52,6 +53,8 @@ const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
|
||||||
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
|
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
|
||||||
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
|
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
|
||||||
|
|
||||||
|
const CTX_USER = getServerContext('user');
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
acct: string;
|
acct: string;
|
||||||
page?: string;
|
page?: string;
|
||||||
|
@ -61,13 +64,24 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
const tab = ref(props.page);
|
const tab = ref(props.page);
|
||||||
|
|
||||||
const user = ref<null | Misskey.entities.UserDetailed>(null);
|
const user = ref<null | Misskey.entities.UserDetailed>(CTX_USER);
|
||||||
const error = ref<any>(null);
|
const error = ref<any>(null);
|
||||||
|
|
||||||
function fetchUser(): void {
|
function fetchUser(): void {
|
||||||
if (props.acct == null) return;
|
if (props.acct == null) return;
|
||||||
|
|
||||||
|
const { username, host } = Misskey.acct.parse(props.acct);
|
||||||
|
|
||||||
|
if (CTX_USER && CTX_USER.username === username && CTX_USER.host === host) {
|
||||||
|
user.value = CTX_USER;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
user.value = null;
|
user.value = null;
|
||||||
misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => {
|
misskeyApi('users/show', {
|
||||||
|
username,
|
||||||
|
host,
|
||||||
|
}).then(u => {
|
||||||
user.value = u;
|
user.value = u;
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
error.value = err;
|
error.value = err;
|
||||||
|
|
23
packages/frontend/src/server-context.ts
Normal file
23
packages/frontend/src/server-context.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
|
|
||||||
|
const providedContextEl = document.getElementById('misskey_clientCtx');
|
||||||
|
|
||||||
|
export type ServerContext = {
|
||||||
|
clip?: Misskey.entities.Clip;
|
||||||
|
note?: Misskey.entities.Note;
|
||||||
|
user?: Misskey.entities.UserLite;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
export const serverContext: ServerContext = (providedContextEl && providedContextEl.textContent) ? JSON.parse(providedContextEl.textContent) : null;
|
||||||
|
|
||||||
|
export function getServerContext<K extends keyof NonNullable<ServerContext>>(entity: K): Required<Pick<NonNullable<ServerContext>, K>> | null {
|
||||||
|
// contextは非ログイン状態の情報しかないためログイン時は利用できない
|
||||||
|
if ($i) return null;
|
||||||
|
|
||||||
|
return serverContext ? (serverContext[entity] ?? null) : null;
|
||||||
|
}
|
|
@ -5,8 +5,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mk-app">
|
<div class="mk-app">
|
||||||
<a v-if="isRoot" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--MI_THEME-panel); color:var(--MI_THEME-fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
|
|
||||||
|
|
||||||
<div v-if="!narrow && !isRoot" class="side">
|
<div v-if="!narrow && !isRoot" class="side">
|
||||||
<div class="banner" :style="{ backgroundImage: instance.backgroundImageUrl ? `url(${ instance.backgroundImageUrl })` : 'none' }"></div>
|
<div class="banner" :style="{ backgroundImage: instance.backgroundImageUrl ? `url(${ instance.backgroundImageUrl })` : 'none' }"></div>
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
|
|
|
@ -15,7 +15,7 @@ Issueを作成する前に、以下をご確認ください:
|
||||||
- 重複を防ぐため、既に同様の内容のIssueが作成されていないか検索してから新しいIssueを作ってください。
|
- 重複を防ぐため、既に同様の内容のIssueが作成されていないか検索してから新しいIssueを作ってください。
|
||||||
- Issueを質問に使わないでください。
|
- Issueを質問に使わないでください。
|
||||||
- Issueは、要望、提案、問題の報告にのみ使用してください。
|
- Issueは、要望、提案、問題の報告にのみ使用してください。
|
||||||
- 質問は、[GitHub Discussions](https://github.com/misskey-dev/misskey/discussions)や[Discord](https://discord.gg/Wp8gVStHW3)でお願いします。
|
- 質問は、@yume@mi.yumechi.jp / yume@mi.yumechi.jp でお願いします。
|
||||||
|
|
||||||
## PRの作成
|
## PRの作成
|
||||||
PRを作成する前に、以下をご確認ください:
|
PRを作成する前に、以下をご確認ください:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "misskey-js",
|
"name": "misskey-js",
|
||||||
"version": "2024.11.0-yumechinokuni.5",
|
"version": "2024.11.0-yumechinokuni.7",
|
||||||
"description": "Misskey SDK for JavaScript",
|
"description": "Misskey SDK for JavaScript",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/misskey-dev/misskey.git",
|
"url": "https://forge.yumechi.jp/yume/yumechi-no-kuni.git",
|
||||||
"directory": "packages/misskey-js"
|
"directory": "packages/misskey-js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -12,17 +12,34 @@ import { createEmptyNotification, createNotification } from '@/scripts/create-no
|
||||||
import { swLang } from '@/scripts/lang.js';
|
import { swLang } from '@/scripts/lang.js';
|
||||||
import * as swos from '@/scripts/operations.js';
|
import * as swos from '@/scripts/operations.js';
|
||||||
|
|
||||||
globalThis.addEventListener('install', () => {
|
const STATIC_CACHE_NAME = `misskey-static-${_VERSION_}`;
|
||||||
// ev.waitUntil(globalThis.skipWaiting());
|
const PATHS_TO_CACHE = ['/assets/', '/static-assets/', '/emoji/', '/twemoji/', '/fluent-emoji/', '/vite/'];
|
||||||
|
|
||||||
|
async function cacheWithFallback(cache, paths) {
|
||||||
|
for (const path of paths) {
|
||||||
|
try {
|
||||||
|
await cache.add(new Request(path, { credentials: 'same-origin' }));
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.addEventListener('install', (ev) => {
|
||||||
|
ev.waitUntil((async () => {
|
||||||
|
const cache = await caches.open(STATIC_CACHE_NAME);
|
||||||
|
await cacheWithFallback(cache, PATHS_TO_CACHE);
|
||||||
|
await globalThis.skipWaiting();
|
||||||
|
})());
|
||||||
});
|
});
|
||||||
|
|
||||||
globalThis.addEventListener('activate', ev => {
|
globalThis.addEventListener('activate', (ev) => {
|
||||||
ev.waitUntil(
|
ev.waitUntil(
|
||||||
caches.keys()
|
caches.keys()
|
||||||
.then(cacheNames => Promise.all(
|
.then((cacheNames) => Promise.all(
|
||||||
cacheNames
|
cacheNames
|
||||||
.filter((v) => v !== swLang.cacheName)
|
.filter((v) => v !== STATIC_CACHE_NAME && v !== swLang.cacheName)
|
||||||
.map(name => caches.delete(name)),
|
.map((name) => caches.delete(name)),
|
||||||
))
|
))
|
||||||
.then(() => globalThis.clients.claim()),
|
.then(() => globalThis.clients.claim()),
|
||||||
);
|
);
|
||||||
|
@ -36,10 +53,32 @@ async function offlineContentHTML() {
|
||||||
reload: i18n.ts?.reload ?? 'Reload',
|
reload: i18n.ts?.reload ?? 'Reload',
|
||||||
};
|
};
|
||||||
|
|
||||||
return `<!DOCTYPE html><html lang="ja"><head><meta charset="UTF-8"><meta content="width=device-width,initial-scale=1"name="viewport"><title>${messages.title}</title><style>body{background-color:#0c1210;color:#dee7e4;font-family:Hiragino Maru Gothic Pro,BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif;line-height:1.35;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:24px;box-sizing:border-box}.icon{max-width:120px;width:100%;height:auto;margin-bottom:20px;}.message{text-align:center;font-size:20px;font-weight:700;margin-bottom:20px}.version{text-align:center;font-size:90%;margin-bottom:20px}button{padding:7px 14px;min-width:100px;font-weight:700;font-family:Hiragino Maru Gothic Pro,BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif;line-height:1.35;border-radius:99rem;background-color:#b4e900;color:#192320;border:none;cursor:pointer;-webkit-tap-highlight-color:transparent}button:hover{background-color:#c6ff03}</style></head><body><svg class="icon"fill="none"height="24"stroke="currentColor"stroke-linecap="round"stroke-linejoin="round"stroke-width="2"viewBox="0 0 24 24"width="24"xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z"fill="none"stroke="none"/><path d="M9.58 5.548c.24 -.11 .492 -.207 .752 -.286c1.88 -.572 3.956 -.193 5.444 1c1.488 1.19 2.162 3.007 1.77 4.769h.99c1.913 0 3.464 1.56 3.464 3.486c0 .957 -.383 1.824 -1.003 2.454m-2.997 1.033h-11.343c-2.572 -.004 -4.657 -2.011 -4.657 -4.487c0 -2.475 2.085 -4.482 4.657 -4.482c.13 -.582 .37 -1.128 .7 -1.62"/><path d="M3 3l18 18"/></svg><div class="message">${messages.header}</div><div class="version">v${_VERSION_}</div><button onclick="reloadPage()">${messages.reload}</button><script>function reloadPage(){location.reload(!0)}</script></body></html>`;
|
return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta content="width=device-width,initial-scale=1" name="viewport"><title>${messages.title}</title><style>body{background-color:#0c1210;color:#dee7e4;font-family:Hiragino Maru Gothic Pro,BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif;line-height:1.35;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:24px;box-sizing:border-box}.icon{max-width:120px;width:100%;height:auto;margin-bottom:20px;}.message{text-align:center;font-size:20px;font-weight:700;margin-bottom:20px}.version{text-align:center;font-size:90%;margin-bottom:20px}button{padding:7px 14px;min-width:100px;font-weight:700;font-family:Hiragino Maru Gothic Pro,BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif;line-height:1.35;border-radius:99rem;background-color:#ff82ab;color:#192320;border:none;cursor:pointer;-webkit-tap-highlight-color:transparent}button:hover{background-color:#fac5eb}</style></head><body><svg class="icon" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none" stroke="none"/><path d="M9.58 5.548c.24 -.11 .492 -.207 .752 -.286c1.88 -.572 3.956 -.193 5.444 1c1.488 1.19 2.162 3.007 1.77 4.769h.99c1.913 0 3.464 1.56 3.464 3.486c0 .957 -.383 1.824 -1.003 2.454m-2.997 1.033h-11.343c-2.572 -.004 -4.657 -2.011 -4.657 -4.487c0 -2.475 2.085 -4.482 4.657 -4.482c.13 -.582 .37 -1.128 .7 -1.62"/><path d="M3 3l18 18"/></svg><div class="message">${messages.header}</div><div class="version">v${_VERSION_}</div><button onclick="reloadPage()">${messages.reload}</button><script>function reloadPage(){location.reload(true)}</script></body></html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
globalThis.addEventListener('fetch', ev => {
|
globalThis.addEventListener('fetch', (ev) => {
|
||||||
|
const shouldCache = PATHS_TO_CACHE.some((path) => ev.request.url.includes(path));
|
||||||
|
|
||||||
|
if (shouldCache) {
|
||||||
|
ev.respondWith(
|
||||||
|
caches.match(ev.request)
|
||||||
|
.then((response) => {
|
||||||
|
if (response) return response;
|
||||||
|
|
||||||
|
return fetch(ev.request).then((response) => {
|
||||||
|
if (!response || response.status !== 200 || response.type !== 'basic') return response;
|
||||||
|
const responseToCache = response.clone();
|
||||||
|
caches.open(STATIC_CACHE_NAME)
|
||||||
|
.then((cache) => {
|
||||||
|
cache.put(ev.request, responseToCache);
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let isHTMLRequest = false;
|
let isHTMLRequest = false;
|
||||||
if (ev.request.headers.get('sec-fetch-dest') === 'document') {
|
if (ev.request.headers.get('sec-fetch-dest') === 'document') {
|
||||||
isHTMLRequest = true;
|
isHTMLRequest = true;
|
||||||
|
@ -64,8 +103,7 @@ globalThis.addEventListener('fetch', ev => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
globalThis.addEventListener('push', ev => {
|
globalThis.addEventListener('push', (ev) => {
|
||||||
// クライアント取得
|
|
||||||
ev.waitUntil(globalThis.clients.matchAll({
|
ev.waitUntil(globalThis.clients.matchAll({
|
||||||
includeUncontrolled: true,
|
includeUncontrolled: true,
|
||||||
type: 'window',
|
type: 'window',
|
||||||
|
@ -73,16 +111,14 @@ globalThis.addEventListener('push', ev => {
|
||||||
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.data?.json();
|
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.data?.json();
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
// case 'driveFileCreated':
|
|
||||||
case 'notification':
|
case 'notification':
|
||||||
case 'unreadAntennaNote':
|
case 'unreadAntennaNote':
|
||||||
// 1日以上経過している場合は無視
|
|
||||||
if (Date.now() - data.dateTime > 1000 * 60 * 60 * 24) break;
|
if (Date.now() - data.dateTime > 1000 * 60 * 60 * 24) break;
|
||||||
|
|
||||||
return createNotification(data);
|
return createNotification(data);
|
||||||
case 'readAllNotifications':
|
case 'readAllNotifications':
|
||||||
await globalThis.registration.getNotifications()
|
await globalThis.registration.getNotifications()
|
||||||
.then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close()));
|
.then((notifications) => notifications.forEach((n) => n.tag !== 'read_notification' && n.close()));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,9 +195,9 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'markAllAsRead':
|
case 'markAllAsRead':
|
||||||
await globalThis.registration.getNotifications()
|
await globalThis.registration.getNotifications()
|
||||||
.then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close()));
|
.then((notifications) => notifications.forEach((n) => n.tag !== 'read_notification' && n.close()));
|
||||||
await get<Pick<Misskey.entities.SignupResponse, 'id' | 'token'>[]>('accounts').then(accounts => {
|
await get<Pick<Misskey.entities.SignupResponse, 'id' | 'token'>[]>('accounts').then((accounts) => {
|
||||||
return Promise.all((accounts ?? []).map(async account => {
|
return Promise.all((accounts ?? []).map(async (account) => {
|
||||||
await swos.sendMarkAllAsRead(account.id);
|
await swos.sendMarkAllAsRead(account.id);
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
@ -196,18 +232,15 @@ globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEv
|
||||||
|
|
||||||
globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => {
|
globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => {
|
||||||
ev.waitUntil((async (): Promise<void> => {
|
ev.waitUntil((async (): Promise<void> => {
|
||||||
switch (ev.data) {
|
if (ev.data === 'clear') {
|
||||||
case 'clear':
|
|
||||||
// Cache Storage全削除
|
|
||||||
await caches.keys()
|
await caches.keys()
|
||||||
.then(cacheNames => Promise.all(
|
.then((cacheNames) => Promise.all(
|
||||||
cacheNames.map(name => caches.delete(name)),
|
cacheNames.map((name) => caches.delete(name)),
|
||||||
));
|
));
|
||||||
return; // TODO
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof ev.data === 'object') {
|
if (typeof ev.data === 'object') {
|
||||||
// E.g. '[object Array]' → 'array'
|
|
||||||
const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase();
|
const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase();
|
||||||
|
|
||||||
if (otype === 'object') {
|
if (otype === 'object') {
|
||||||
|
|
81
yume-mods/legal/privacy-policy-zh.md
Normal file
81
yume-mods/legal/privacy-policy-zh.md
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
mi.yumechi.jp 的隱私權政策
|
||||||
|
|
||||||
|
ゆめちのくに (YumechiNoKuni) 堅決致力於保護使用者的隱私並證明我們是值得信賴的。本隱私權政策概述了我們收到的個人資訊的類型、如何處理這些資訊、採取了哪些措施來保護您的數據,以及如何驗證我們是否遵守我們的政策。
|
||||||
|
|
||||||
|
TL;DR 非正式版本:據我們所知,我們是聯邦宇宙中最透明、最保護隱私的實例之一。
|
||||||
|
|
||||||
|
## 個人資訊
|
||||||
|
|
||||||
|
### 我們收集的資訊
|
||||||
|
|
||||||
|
- **帳戶資訊**:當您註冊帳戶時,我們將收集您的使用者名稱、電子郵件地址和密碼等資訊。您的使用者名稱將公開顯示,但您的電子郵件地址將保密,管理員除外。您的密碼將使用 bcrypt 進行加密,如果不進行強力搜尋就無法恢復,因此設定一個不易被猜到的密碼至關重要。
|
||||||
|
- **個人資料資訊**:您可以選擇在您的個人資料中提供其他信息,例如顯示名稱、頭像和個人簡介。此資訊將公開顯示並與其他實例聯合,我們無法阻止這種情況。
|
||||||
|
- **貼文、頁面和其他內容**:您在網站上發布的任何內容都將儲存在我們的伺服器上,除非您將貼文設定為「私人」或「僅限追蹤者」(前提是您沒有不在我們實例上的追蹤者)”,我們不能保證您的內容不會被非預期方看到,任何其他實例也不會尊重您刪除資料的請求。這是聯合協議的硬性限制,我們無法更改這一點,但是我們非常願意允許您註冊一個專用於私人內容的新帳戶。
|
||||||
|
- **多媒體和檔案文件**:文件儲存功能允許您像雲端儲存服務一樣上傳文件,但請注意,任何擁有該文件 ID 或連結的人都可以存取該文件。雖然該 ID 理論上很難猜測,但它不被認為是安全的,不應該用於敏感資訊。它也沒有加密到您的帳戶,這意味著(雖然我們已採取措施防止這種情況)伺服器上的惡意程式可能會存取您的檔案。
|
||||||
|
- **IP 位址**:軟體支援記錄用於登入嘗試的 IP 位址,您可以在「安全」標籤的帳戶設定中查看。目前沒有自助方式可以停用此功能,但您可以聯絡我們請求我們不再記錄您的 IP 位址。
|
||||||
|
- **伺服器日誌**:我們記錄其他實例的查詢或導致錯誤的請求以用於偵錯目的。雖然它們通常不容易追蹤到特定用戶,但我們可能知道有人試圖存取特定資源。
|
||||||
|
- **指標**:我們在伺服器端收集指標。這些指標是高度聚合的,不包含任何標識信息,它包含的信息包括處理請求所用時間的直方圖(按發出的請求類型劃分)、請求是否成功、伺服器使用的內存量以及傳入和傳出聯合訊息的數量。
|
||||||
|
|
||||||
|
## 我們如何使用您的資訊(以及我們如何證明它)
|
||||||
|
|
||||||
|
### 審核
|
||||||
|
|
||||||
|
我們要求所有版主和管理員不得將其特權帳戶用於審核以外的任何目的(甚至在沒有特定目的的情況下登入)。但我們無法保證您的資料在日常系統維護期間不會意外訪問,例如許多資料庫管理需要直接檢查資料。我們承諾不會利用任何意外存取您的數據,並盡力盡快忘記它。
|
||||||
|
|
||||||
|
### 法律請求
|
||||||
|
|
||||||
|
雖然我會盡力審查收到的每一項法律請求,但我不能保證在出現法律請求時我能夠盡全力保護您的資料。我將盡力告知您我收到的任何法律要求以及我是否已遵守這些要求,除非法律禁止我這樣做。我位於美國德克薩斯州。
|
||||||
|
|
||||||
|
### 程式碼訪問
|
||||||
|
|
||||||
|
與上游所需的 AGPL 許可證一樣,此實例的原始程式碼可從 https://forge.yumechi.jp/yume/yumechi-no-kuni 取得。我們還努力確保環境可以輕鬆複製,無需手動幹預新功能(例如 Pgroonga 全文搜索),並且我們添加了提交哈希的構建時注入,以便您可以輕鬆查看到底是什麼版本代碼正在運行(您可以從任何不處於開發模式的正在運行的實例透過“/nodeinfo/2.1”端點存取它)。
|
||||||
|
|
||||||
|
### 電子郵件
|
||||||
|
|
||||||
|
我們不使用任何第三方電子郵件服務來傳送或接收電子郵件。所有電子郵件通訊均完全在內部處理。我們已採取措施確保我們的電子郵件安全:
|
||||||
|
|
||||||
|
- 使用 SPF、DKIM 和 DMARC 防止電子郵件欺騙。
|
||||||
|
- 使用 MTA-STS 確保您傳送給我們的所有電子郵件通訊已加密。
|
||||||
|
- 請所有外寄電子郵件均使用 STARTTLS 加密。
|
||||||
|
|
||||||
|
但是,管理員聯絡電子郵件可能由第三方服務處理。如果您不能接受,請透過實例上的直接訊息與我們聯絡。
|
||||||
|
|
||||||
|
### 資料儲存
|
||||||
|
|
||||||
|
您的資料儲存在位於奧地利維也納的伺服器上。我們對伺服器套用了全碟加密,並將每項服務僅限於自己的用戶,並啟用強制存取控制以防止未經授權的存取您的資料。您可以聯絡我們索取您的資料副本,我們將在 7 天內處理您的請求,您可以要求我們認為相對完整的資料包,也可以指定您希望接收的資料類型。
|
||||||
|
|
||||||
|
如果您對我們提供的資料不滿意,您可以使用本地環境準備並發送您想要結果的SQL查詢,前提是不損害服務的完整性或侵犯其他使用者的隱私。
|
||||||
|
|
||||||
|
### 網路請求
|
||||||
|
|
||||||
|
當您使用我們的服務時,您的裝置將向我們的伺服器發出請求。我們已採取措施確保您在使用我們的服務時進行的所有通訊不會被第三方觀察到。我們已採取措施確保這一點:
|
||||||
|
|
||||||
|
- 不使用會解密您與我們服務的連線的第三方 CDN。
|
||||||
|
- 使用預先載入 HSTS 的 HTTPS 來確保您的連線是加密且安全的。這意味著即使是新安裝的瀏覽器如果無法建立安全連線也會拒絕連線到我們的服務。
|
||||||
|
- 在您的瀏覽器上實施沙箱,以防止任何外部內容或意外腳本在我們的網頁上運行。它由多個 HTTP 標頭組成,包括嚴格的內容安全策略、內容類型選項和幀限制。
|
||||||
|
- 防止第三方網站追蹤您,我們使用嚴格的推薦人政策來防止您在我們的服務上點擊的任何連結被發送到第三方網站。我們還要求您的瀏覽器在我們的瀏覽器沙箱上停用已知具有可疑隱私屬性的功能,例如「fLoC」、「主題 API」、「歸因報告」和 DRM。您可以造訪 https://securityheaders.com/?q=https%3A%2F%2Fmi.yumechi.jp%2F 查看我們的安全標頭以及專業說明。
|
||||||
|
- 在所有媒體檔案前面放置一個代理,隱藏請求的來源並防止下載危險的檔案格式。代理程式的原始碼可在 https://forge.yumechi.jp/yume/yumechi-no-kuni-proxy-worker 取得。
|
||||||
|
|
||||||
|
但是,有兩個例外:
|
||||||
|
|
||||||
|
#### 僅限追蹤者的帖子
|
||||||
|
|
||||||
|
雖然您的網頁請求永遠不會直接發送給第三方,但您查找外部資源的請求(例如透過 URL 上傳文件、遠端使用者和註釋查找)將導致從我們的伺服器向外部伺服器發出請求,並且取決於是否外部伺服器聲稱他們需要用戶身份驗證,該請求可能會追溯到您。
|
||||||
|
|
||||||
|
#### 第三方應用程式
|
||||||
|
|
||||||
|
雖然我們使用了所有主流瀏覽器強制執行的安全功能,但我們不能保證第三方應用程式將保持相同的安全等級。如果您使用網站或 PWA(「新增至主畫面」功能)以外的服務,您應該注意,我們無法保證我們在上一節中所做的承諾。
|
||||||
|
|
||||||
|
### 指標
|
||||||
|
|
||||||
|
雖然我們不允許公眾訪問生產中的指標端點(將來可能允許長期用戶訪問),但我們的暫存環境中的原始指標端點在 https://test0.mi.yumechi 上開放供公眾審查.jp/metrics和https://test0.mi.yumechi.jp/metrics/cluster。
|
||||||
|
|
||||||
|
此資訊將發送至第三方服務 [Grafana Cloud](https://grafana.com/products/cloud/) 以進行視覺化和警報。我們定期發佈公共儀表板的 PDF 匯出,展示我們在 https://mi.yumechi.jp/@mihari 收集的指標。
|
||||||
|
|
||||||
|
## 您可以採取哪些措施來保護您的隱私
|
||||||
|
|
||||||
|
### 帳戶安全
|
||||||
|
|
||||||
|
- **使用強密碼**:為了確保我們的網站不依賴第三方服務,我們僅對失敗的登入嘗試使用冷卻期。請使用不易被猜到的強密碼。
|
||||||
|
- **啟用雙重認證**:我們支援使用 TOTP 或 WebAuthn 的雙重認證。您可以在「安全性」標籤的帳戶設定中啟用它。我們已經更改了上游的行為,這樣,如果您僅將硬體金鑰用於2FA,我們將不喜歡但不要求您使用密碼保護您的硬體金鑰,因為硬體金鑰的系統使用者通常會保留物理密鑰。
|
||||||
|
- **重置您的登入權杖**:這是目前從上游繼承的限制,我們正在研究解決方案,但與此同時,請不要依賴註銷功能、請轉到“設定”->“安全性”->“重新產生登入權杖”」以重置您的令牌、如果您懷疑您的登入會話不再安全。
|
83
yume-mods/legal/privacy-policy.md
Normal file
83
yume-mods/legal/privacy-policy.md
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
ZH version: [隐私政策](./privacy-policy-zh.md)
|
||||||
|
|
||||||
|
Privacy Policy of mi.yumechi.jp
|
||||||
|
|
||||||
|
ゆめちのくに (YumechiNoKuni) is strongly committed to protecting user's privacy and proving that we are trustworthy. This privacy policy outlines the types of personal information we receive, how it might be processed, what measures have been taken to protect your data, and how to verify our compliance with our policy.
|
||||||
|
|
||||||
|
The informal version: We are as far as we know the most transparent and privacy-preserving instance in the fediverse.
|
||||||
|
|
||||||
|
## Personal Information
|
||||||
|
|
||||||
|
### Information We Collect
|
||||||
|
|
||||||
|
- **Account Information**: When you sign up for an account, we will collect information such as your username, email address, and password. Your username will be publicly displayed but your email address will be kept private except to the administrators. Your password will be encrypted with bcrypt and will not be recoverable without a brute force search, thus it is paramount that you set a password that is not easily guessable.
|
||||||
|
- **Profile Information**: You may choose to provide additional information on your profile, such as a display name, avatar, and bio. This information will be publicly displayed and federated to other instances, and there is no way to prevent this.
|
||||||
|
- **Posts, Pages and other Content**: Any content you post on the site will be stored on our servers, unless you have set the post to be "private" or "followers-only (under the condition that you do not have followers not on our instance)", we cannot guarantee that your content will not be seen by unintended parties, nor any foreign instances will honor your request to delete the data. This is a hard limit of the federation protocol and we cannot change this, however we are more than willing to allow you to register a new account dedicated to private content.
|
||||||
|
- **Multimedia and Drive Files**: The drive feature allows you to upload files as if it were a cloud storage service, however please note that anyone who has the ID or link to the file can access it. While the ID is theoretically hard to guess, it is not considered secure and should not be used for sensitive information. It was also not encrypted to your account, which means (while we have taken measures to prevent this) it is possible a malicious program on the server could access your files.
|
||||||
|
- **IP Address**: There is built-in support for logging IP addresses used for login attempts, which you can review in your account settings in the "Security" tab. There is currently no self-service way to disable this feature, however you can request for us no longer to log your IP address by contacting us.
|
||||||
|
- **Server Logs**: Requests that result in queries to other instances or cause errors are logged for debugging purposes. While they are usually not easily traceable to a specific user, we may know someone was trying to access a specific resource.
|
||||||
|
- **Metrics**: We collect metrics on the server side. The metrics are highly aggregated and do not contain any identifying information, it contains information such as a histogram of time taken to process the request by the kind of request is being made and whether the request was successful, the amount of memory used by the server and the amount of incoming and outgoing federation messages.
|
||||||
|
|
||||||
|
## How We Use Your Information (and How we can Prove it)
|
||||||
|
|
||||||
|
### Moderation
|
||||||
|
|
||||||
|
We have required all moderators and administrators to not use their privileged accounts for any purpose other than moderation (or even logging in without a specific purpose). However we cannot guarantee that your data will not be accidentally accessed during routine system maintenance, for example many database management requires inspecting the data directly. We promise we will not make any use of any accidental access to your data and try our best to forget it as soon as possible.
|
||||||
|
|
||||||
|
### Legal
|
||||||
|
|
||||||
|
While I will make an effort to vet every legal request I receive, I cannot guarantee that I will be able to make every power to protect your data in the event of a legal request. I will make an effort to inform you of any legal request I receive and whether I have complied with it, unless I am legally prohibited from doing so. I am located in Texas, US.
|
||||||
|
|
||||||
|
### Code Access
|
||||||
|
|
||||||
|
As with the AGPL license required by upstream, the source code for this instance is available at https://forge.yumechi.jp/yume/yumechi-no-kuni. We have also made effort to ensure that the environment can easily be replicated by not requiring manual intervention for new features such as Pgroonga full text search, and we have added build-time injection of the commit hash so you can easily see exactly what version of the code is running (you can access it via the `/nodeinfo/2.1` endpoint from any of our running instance that is not in development mode).
|
||||||
|
|
||||||
|
### Email
|
||||||
|
|
||||||
|
We do not use any third-party email services to send or receive emails. All email communications are handled completely in-house. We have taken measures to ensure our email safety by:
|
||||||
|
|
||||||
|
- Using SPF, DKIM, and DMARC to prevent email spoofing.
|
||||||
|
- Using MTA-STS to ensure that all email communications you sent to us are encrypted.
|
||||||
|
- Requiring all outgoing emails to be encrypted with STARTTLS.
|
||||||
|
|
||||||
|
However the moderator contact email may be handled by a third-party service. If this is not acceptable to you, please contact us through a direct message on the instance.
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
|
||||||
|
Your data is stored on a server located in Wien, Austria. We have applied full disk encryption to the server and confined each service to its own user and enabled mandatory access control to prevent unauthorized access to your data. You may request a copy of your data by contacting us and your request will be processed within 7 days, you can either request for a package that we deemed to be relatively complete or specify the kind of data you want to receive.
|
||||||
|
|
||||||
|
If you are not satisfied with the data we provide, you can prepare using local environment and send in a SQL query you want the result of, provided it does not harm the integrity of the service or invade the privacy of other users.
|
||||||
|
|
||||||
|
### Network Requests
|
||||||
|
|
||||||
|
When you use our service, your device will make requests to our servers. We have taken measures to ensure that all communication you make while using our service is never observed by a third party. We have taken steps to ensure this by:
|
||||||
|
|
||||||
|
- Not using a third-party CDN that will decrypt your connection to our service.
|
||||||
|
- Using HTTPS with preloaded HSTS to ensure that your connection is encrypted and secure. This means even a newly-installed browser will refuse to connect to our service if it cannot establish a secure connection.
|
||||||
|
- Enforcing a sandbox on your browser to prevent any external contents or unintended scripts from running on our webpage. This consists of several HTTP headers including strict Content Security Policy, Content Type Options, and Frame Restrictions.
|
||||||
|
- Preventing third-party websites from tracking you, we have used a strict Referrer Policy to prevent any links you click on our service from being sent to the third-party website. We also requested your browser to disable features known to have questionable privacy properties such as `fLoC`, `Topics API`, `Attribution Reporting` and DRM on our browser sandbox. You can review our security headers along with a professional explanation by visiting https://securityheaders.com/?q=https%3A%2F%2Fmi.yumechi.jp%2F.
|
||||||
|
- Place a proxy in front of all media files that hides the origin of the request and prevents dangerous file formats from being downloaded. The source code of the proxy is available at https://forge.yumechi.jp/yume/yumechi-no-kuni-proxy-worker.
|
||||||
|
|
||||||
|
However, there are two exceptions to this:
|
||||||
|
|
||||||
|
#### Follower-only Posts
|
||||||
|
|
||||||
|
While your network requests are never directly sent to a third party, your requests to look up external resources such as uploading files by URL, remote user and note lookups, will result in a request from our server to the external server, and depending on whether the external server claims they require user authentication, this request might be traced back to you.
|
||||||
|
|
||||||
|
#### Third-party Apps
|
||||||
|
|
||||||
|
While we used security features that are enforced by all mainstream browsers, we cannot guarantee that third-party apps will maintain the same level of security. If you use services other than the website or PWA (the 'Add to Home Screen' feature), you should be aware that we cannot guarantee the promises we made in the previous section.
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
|
||||||
|
While we do not allow public access to our metrics endpoint in production (we may allow long-time users access in the future), the raw metrics endpoint in our staging environment is open for public review at https://test0.mi.yumechi.jp/metrics and https://test0.mi.yumechi.jp/metrics/cluster.
|
||||||
|
|
||||||
|
This information is sent to a third-party service [Grafana Cloud](https://grafana.com/products/cloud/) for visualization and alerting. We post periodic PDF exports of a public dashboard demonstrating the metrics we collect at https://mi.yumechi.jp/@mihari.
|
||||||
|
|
||||||
|
## What you can do to Protect Your Privacy
|
||||||
|
|
||||||
|
### Account Security
|
||||||
|
|
||||||
|
- **Use a Strong Password**: In order to guarantee our website does not depend on a third-party service, we only use a cool-down period for failed login attempts. Please use a strong password that is not easily guessable.
|
||||||
|
- **Enable Two-Factor Authentication**: We support two-factor authentication using TOTP or WebAuthn. You can enable it in your account settings in the "Security" tab. We have changed the behavior from upstream such that if you only use your hardware key for 2FA, we will not prefer but not require you to password-protect your hardware key as it is a common practice for systematic users of hardware keys to keep a physically secure backup key.
|
||||||
|
- **Reset your Token**: This is currently a limitation inherited from upstream and we are working on a solution, but in the meantime, please go to Settings -> Security -> Regenerate Login Token from a secure device to invalidate all your sessions whenever you logged in from a public computer or suspect one of your sessions has been compromised.
|
19
yume-mods/legal/term-of-service-zh.md
Normal file
19
yume-mods/legal/term-of-service-zh.md
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
mi.yumechi.jp 實例規則 (經 Pari Network 授權轉載,並經修改)
|
||||||
|
|
||||||
|
ゆめちのくに (YumechiNoKuni) 鼓勵人們自由地創作與表達,因此以保護用戶隱私與改善體驗為目標,制定了以下實例規則。
|
||||||
|
|
||||||
|
用戶與內容管理策略:
|
||||||
|
- 停權:檔案內容被舉報並確認為兒童性虐待材料的使用者。
|
||||||
|
- 停權:宣揚在 Wikipedia 被認定的恐怖組織清單中列出的恐怖主義的用戶。
|
||||||
|
- 停權:使用 BLOCK ALERT BOT 等極度侵犯用戶隱私程式的用戶。
|
||||||
|
- 停權:發布欺詐、廣告與騷擾內容的用戶。
|
||||||
|
- 停權:發布侵犯個人隱私內容的用戶。
|
||||||
|
- 內容警告:色情、血腥、暴力與極端言論內容,需要使用 CW(隱藏內容)並在外部對可能令人不適的內容進行描述,或者添加 #nsfw 標籤。未按此條要求發佈相關內容的用戶在被多次警告或被檢舉後會被停權。
|
||||||
|
- 內容移除:因種族、膚色或性取向而貶低他人,煽動仇恨暴力而被檢舉報告的內容。
|
||||||
|
|
||||||
|
實例管理策略:
|
||||||
|
- 封鎖:使用 BLOCK ALERT BOT 等侵犯用戶隱私程式的實例。
|
||||||
|
- 封鎖:以欺詐、騷擾、廣告或攻擊為目的搭建的實例。
|
||||||
|
- 封鎖:存在大量違反上述用戶策略(敏感內容除外)的用戶且無人管理的實例。
|
||||||
|
- 封鎖:採用非正常方式獲取用戶資料與隱私的實例。
|
||||||
|
- 為保證互聯,實例管理並不會永久生效,所有實例級別的管理最新資訊請參閱 https://mi.yumechi.jp/@yume/pages/instance_moderation 或使用「實例」功能於「關於」部分查看,或使用 `/api/federation/instances` 端點進行程式化查詢。
|
19
yume-mods/legal/term-of-service.md
Normal file
19
yume-mods/legal/term-of-service.md
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Instance Rules for mi.yumechi.jp (Reproduced under permission from Pari Network, with modifications)
|
||||||
|
|
||||||
|
ゆめちのくに (YumechiNoKuni) encourages people to freely create and express themselves. To protect user privacy and improve the overall experience, the following instance rules have been established.
|
||||||
|
|
||||||
|
User and Content Management Policy:
|
||||||
|
- Account Suspension: Users whose content was reported and confirmed as child sexual abuse material.
|
||||||
|
- Account Suspension: Users who promote terrorism as listed in the Wikipedia-recognized list of terrorist organizations.
|
||||||
|
- Account Suspension: Users who use extreme privacy-invading programs like BLOCK ALERT BOT.
|
||||||
|
- Account Suspension: Users who post fraudulent, advertising, or harassing content.
|
||||||
|
- Account Suspension: Users who publish content that violates personal privacy.
|
||||||
|
- Content Warning: Pornographic, gory, violent, and extreme content must use CW (Content Warning) and include an external description of potentially disturbing content, or add the #nsfw tag. Users who fail to comply with these requirements after multiple warnings or reports will be suspended.
|
||||||
|
- Content Removal: Content that degrades others based on race, color, or sexual orientation, incites hate or violence, and is reported will be removed.
|
||||||
|
|
||||||
|
Instance Management Policy:
|
||||||
|
- Instance Blocking: Instances using privacy-invading programs like BLOCK ALERT BOT.
|
||||||
|
- Instance Blocking: Instances created for purposes of fraud, harassment, advertising, or attacks.
|
||||||
|
- Instance Blocking: Instances with a large number of users violating the above user policies (excluding sensitive content) and lacking active management.
|
||||||
|
- Instance Blocking: Instances that obtain user data and privacy through abnormal means.
|
||||||
|
- To ensure connectivity, instance management will not be permanently enforced for up-to-date of all instance-level moderation see https://mi.yumechi.jp/@yume/pages/instance_moderation or use the "Instances" feature in the About section, or programmatically with the `/api/federation/instances` endpoint.
|
40
yume-mods/nyuukyou/Cargo.lock
generated
40
yume-mods/nyuukyou/Cargo.lock
generated
|
@ -139,7 +139,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_path_to_error",
|
"serde_path_to_error",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper 1.0.1",
|
"sync_wrapper 1.0.2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower 0.5.1",
|
"tower 0.5.1",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
|
@ -162,7 +162,7 @@ dependencies = [
|
||||||
"mime",
|
"mime",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"sync_wrapper 1.0.1",
|
"sync_wrapper 1.0.2",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
@ -421,7 +421,7 @@ checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fedivet"
|
name = "fedivet"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://forge.yumechi.jp/yume/fedivet?tag=testing-audit%2Brelay%2Bfilter#b1b051dc2f1319a3948d7afcecfd3ac8f92a07de"
|
source = "git+https://forge.yumechi.jp/yume/fedivet?tag=v0.0.1#46456b0a61b449dad7bbe85e0342bdd5e3b6e031"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
|
@ -588,9 +588,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.6"
|
version = "0.4.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
|
checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -683,9 +683,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.5.0"
|
version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a"
|
checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
|
@ -940,9 +940,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.11"
|
version = "1.0.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
|
@ -1192,9 +1192,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.89"
|
version = "1.0.92"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
|
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
@ -1307,7 +1307,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper 1.0.1",
|
"sync_wrapper 1.0.2",
|
||||||
"system-configuration",
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
|
@ -1408,9 +1408,9 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schannel"
|
name = "schannel"
|
||||||
version = "0.1.26"
|
version = "0.1.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1"
|
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
@ -1564,9 +1564,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.87"
|
version = "2.0.89"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1581,9 +1581,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sync_wrapper"
|
name = "sync_wrapper"
|
||||||
version = "1.0.1"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
|
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
]
|
]
|
||||||
|
@ -1814,9 +1814,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.13"
|
version = "1.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
|
|
|
@ -7,7 +7,7 @@ edition = "2021"
|
||||||
axum = "0.7"
|
axum = "0.7"
|
||||||
clap = { version = "4.5.20", features = ["derive"] }
|
clap = { version = "4.5.20", features = ["derive"] }
|
||||||
env_logger = "0.11.5"
|
env_logger = "0.11.5"
|
||||||
fedivet = { git = "https://forge.yumechi.jp/yume/fedivet", tag = "testing-audit+relay+filter" }
|
fedivet = { git = "https://forge.yumechi.jp/yume/fedivet", tag = "v0.0.1" }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
serde = { version = "1.0.210", features = ["derive"] }
|
serde = { version = "1.0.210", features = ["derive"] }
|
||||||
tokio = { version = "1" }
|
tokio = { version = "1" }
|
||||||
|
|
Loading…
Reference in a new issue