Compare commits
No commits in common. "master" and "master-bak" have entirely different histories.
master
...
master-bak
69 changed files with 850 additions and 1211 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
Normal file
97
.github/ISSUE_TEMPLATE/01_bug-report.yml
vendored
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
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
Normal file
22
.github/ISSUE_TEMPLATE/02_feature-request.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
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
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
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
Normal file
23
.github/PULL_REQUEST_TEMPLATE/01_bug.md
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<!-- ℹ お読みください / 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
Normal file
23
.github/PULL_REQUEST_TEMPLATE/02_enhance.md
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<!-- ℹ お読みください / 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
Normal file
20
.github/PULL_REQUEST_TEMPLATE/03_release.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
## 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://forge.yumechi.jp/yume/yumechi-no-kuni/src/branch/master/CONTRIBUTING.md
|
https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## What
|
## What
|
||||||
|
@ -17,7 +17,7 @@ https://forge.yumechi.jp/yume/yumechi-no-kuni/src/branch/master/CONTRIBUTING.md
|
||||||
<!-- Test perspective, etc -->
|
<!-- Test perspective, etc -->
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
- [ ] Read the [contribution guide](https://forge.yumechi.jp/yume/yumechi-no-kuni/src/branch/master/CONTRIBUTING.md)
|
- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/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
|
||||||
|
|
6
.okteto/okteto-pipeline.yml
Normal file
6
.okteto/okteto-pipeline.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
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,23 +1,3 @@
|
||||||
|
|
||||||
## 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 タッグをマージする
|
||||||
|
@ -78,7 +58,6 @@ 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)
|
||||||
|
@ -113,7 +92,6 @@ 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,31 +1,49 @@
|
||||||
# ゆめちのくに
|
<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>
|
||||||
|
|
||||||
YumechiNoKuni is a fork of Misskey, with a focus on security, observability and reliability.
|
**🌎 **Misskey** is an open source, federated social media platform that's free forever! 🚀**
|
||||||
|
|
||||||
[mi.yumechi.jp](https://mi.yumechi.jp) is running this version.
|
[Learn more](https://misskey-hub.net/)
|
||||||
|
|
||||||
[Learn more about Misskey](https://misskey-hub.net/)
|
---
|
||||||
|
|
||||||
## Main differences
|
<a href="https://misskey-hub.net/servers/">
|
||||||
|
<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>
|
||||||
|
|
||||||
### Unique features
|
<a href="https://misskey-hub.net/docs/for-admin/install/guides/">
|
||||||
|
<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>
|
||||||
|
|
||||||
- Strict ActivityPub sanitization by whitelisting properties and normalizing all referential properties.
|
<a href="./CONTRIBUTING.md">
|
||||||
- Strict Content Security Policy.
|
<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>
|
||||||
- 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.
|
|
||||||
|
|
||||||
### Picked from github.com/paricafe/misskey
|
<a href="https://discord.gg/Wp8gVStHW3">
|
||||||
|
<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>
|
||||||
|
|
||||||
- pgroonga full-text search (with modifications).
|
<a href="https://www.patreon.com/syuilo">
|
||||||
- Better Service Worker caching.
|
<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 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,12 +1,15 @@
|
||||||
# Reporting Security Issues
|
# Reporting Security Issues
|
||||||
|
|
||||||
If you discover a security issue in this project, please use the `git blame` command to identify the source of the issue,
|
If you discover a security issue in Misskey, please report it by **[this form](https://github.com/misskey-dev/misskey/security/advisories/new)**.
|
||||||
if it was introduced by this fork please contact me at secity<at>yumechi.jp.
|
|
||||||
|
|
||||||
For upstream issues please report by **[this form](https://github.com/misskey-dev/misskey/security/advisories/new)**.
|
This will allow us to assess the risk, and make a fix available before we add a
|
||||||
|
bug report to the GitHub repository.
|
||||||
|
|
||||||
Thanks for helping make YumechiNoKuni safe for everyone.
|
Thanks for helping make Misskey safe for everyone.
|
||||||
|
|
||||||
## When create a patch
|
## When create a patch
|
||||||
|
|
||||||
If you can also create a patch to fix the vulnerability, please send a diff file with the report.
|
If you can also create a patch to fix the vulnerability, please create a PR on the private fork.
|
||||||
|
|
||||||
|
> [!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,7 +586,6 @@ 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,7 +1242,6 @@ 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}"
|
||||||
|
@ -1254,8 +1253,6 @@ 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"
|
||||||
|
@ -1267,7 +1264,6 @@ _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"
|
||||||
|
@ -1278,7 +1274,6 @@ _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,7 +586,6 @@ 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,7 +586,6 @@ masterVolume: "마스터 볼륨"
|
||||||
notUseSound: "음소거 하기"
|
notUseSound: "음소거 하기"
|
||||||
useSoundOnlyWhenActive: "Misskey를 활성화한 때에만 소리를 출력하기"
|
useSoundOnlyWhenActive: "Misskey를 활성화한 때에만 소리를 출력하기"
|
||||||
details: "자세히"
|
details: "자세히"
|
||||||
renoteDetails: "리노트 상세 내용"
|
|
||||||
chooseEmoji: "이모지 선택"
|
chooseEmoji: "이모지 선택"
|
||||||
unableToProcess: "작업을 완료할 수 없습니다"
|
unableToProcess: "작업을 완료할 수 없습니다"
|
||||||
recentUsed: "최근 사용"
|
recentUsed: "최근 사용"
|
||||||
|
@ -1300,7 +1299,6 @@ thisContentsAreMarkedAsSigninRequiredByAuthor: "게시자에 의해 로그인해
|
||||||
lockdown: "잠금"
|
lockdown: "잠금"
|
||||||
pleaseSelectAccount: "계정을 선택해주세요."
|
pleaseSelectAccount: "계정을 선택해주세요."
|
||||||
availableRoles: "사용 가능한 역할"
|
availableRoles: "사용 가능한 역할"
|
||||||
acknowledgeNotesAndEnable: "활성화 하기 전에 주의 사항을 확인했습니다."
|
|
||||||
_accountSettings:
|
_accountSettings:
|
||||||
requireSigninToViewContents: "콘텐츠 열람을 위해 로그인으 필수로 설정하기"
|
requireSigninToViewContents: "콘텐츠 열람을 위해 로그인으 필수로 설정하기"
|
||||||
requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다."
|
requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다."
|
||||||
|
@ -1457,8 +1455,6 @@ _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: "다른 계정에서 이 계정으로 이사"
|
||||||
|
@ -2741,6 +2737,3 @@ _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,7 +586,6 @@ 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.8",
|
"version": "2024.11.0-yumechinokuni.5",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
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';
|
||||||
|
@ -44,14 +45,6 @@ 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, {
|
||||||
|
@ -68,6 +61,7 @@ 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
|
||||||
|
@ -76,6 +70,13 @@ 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);
|
||||||
|
@ -138,4 +139,18 @@ 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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -313,7 +313,6 @@ 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,10 +6,9 @@
|
||||||
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 { HttpsProxyAgent } from 'hpagent';
|
import { HttpProxyAgent, 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';
|
||||||
|
@ -26,97 +25,8 @@ export type HttpRequestSendOptions = {
|
||||||
validators?: ((res: Response) => void)[];
|
validators?: ((res: Response) => void)[];
|
||||||
};
|
};
|
||||||
|
|
||||||
declare module 'node:http' {
|
|
||||||
interface Agent {
|
|
||||||
createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class HttpRequestServiceAgent extends http.Agent {
|
|
||||||
constructor(
|
|
||||||
private config: Config,
|
|
||||||
options?: http.AgentOptions,
|
|
||||||
) {
|
|
||||||
super(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket {
|
|
||||||
const socket = super.createConnection(options, callback)
|
|
||||||
.on('connect', () => {
|
|
||||||
const address = socket.remoteAddress;
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
if (address && ipaddr.isValid(address)) {
|
|
||||||
if (this.isPrivateIp(address)) {
|
|
||||||
socket.destroy(new Error(`Blocked address: ${address}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return socket;
|
|
||||||
};
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private isPrivateIp(ip: string): boolean {
|
|
||||||
const parsedIp = ipaddr.parse(ip);
|
|
||||||
|
|
||||||
for (const net of this.config.allowedPrivateNetworks ?? []) {
|
|
||||||
const cidr = ipaddr.parseCIDR(net);
|
|
||||||
if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedIp.range() !== 'unicast';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class HttpsRequestServiceAgent extends https.Agent {
|
|
||||||
constructor(
|
|
||||||
private config: Config,
|
|
||||||
options?: https.AgentOptions,
|
|
||||||
) {
|
|
||||||
super(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket {
|
|
||||||
const socket = super.createConnection(options, callback)
|
|
||||||
.on('connect', () => {
|
|
||||||
const address = socket.remoteAddress;
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
if (address && ipaddr.isValid(address)) {
|
|
||||||
if (this.isPrivateIp(address)) {
|
|
||||||
socket.destroy(new Error(`Blocked address: ${address}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return socket;
|
|
||||||
};
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private isPrivateIp(ip: string): boolean {
|
|
||||||
const parsedIp = ipaddr.parse(ip);
|
|
||||||
|
|
||||||
for (const net of this.config.allowedPrivateNetworks ?? []) {
|
|
||||||
const cidr = ipaddr.parseCIDR(net);
|
|
||||||
if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedIp.range() !== 'unicast';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HttpRequestService {
|
export class HttpRequestService {
|
||||||
/**
|
|
||||||
* Get https non-proxy agent (without local address filtering)
|
|
||||||
*/
|
|
||||||
private httpsNative: https.Agent;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get https non-proxy agent
|
* Get https non-proxy agent
|
||||||
*/
|
*/
|
||||||
|
@ -137,17 +47,13 @@ export class HttpRequestService {
|
||||||
lookup: false, // nativeのdns.lookupにfallbackしない
|
lookup: false, // nativeのdns.lookupにfallbackしない
|
||||||
});
|
});
|
||||||
|
|
||||||
const agentOption = {
|
this.https = new https.Agent({
|
||||||
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,
|
minVersion: 'TLSv1.2',
|
||||||
};
|
});
|
||||||
|
|
||||||
this.httpsNative = new https.Agent(agentOption);
|
|
||||||
|
|
||||||
this.https = new HttpsRequestServiceAgent(config, agentOption);
|
|
||||||
|
|
||||||
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
||||||
|
|
||||||
|
@ -171,10 +77,9 @@ export class HttpRequestService {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public getAgentByUrl(url: URL, bypassProxy = false): https.Agent {
|
public getAgentByUrl(url: URL, bypassProxy = false): https.Agent {
|
||||||
if (url.protocol && url.protocol !== 'https:') {
|
if (url.protocol !== 'https:') {
|
||||||
throw new Error('Invalid protocol');
|
throw new Error('Invalid protocol');
|
||||||
}
|
}
|
||||||
url.protocol = 'https:';
|
|
||||||
if (url.port && url.port !== '443') {
|
if (url.port && url.port !== '443') {
|
||||||
throw new Error('Invalid port');
|
throw new Error('Invalid port');
|
||||||
}
|
}
|
||||||
|
@ -186,7 +91,7 @@ export class HttpRequestService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise<IObject> {
|
public async getActivityJson(url: string): Promise<IObject> {
|
||||||
const res = await this.send(url, {
|
const res = await this.send(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -194,7 +99,6 @@ export class HttpRequestService {
|
||||||
},
|
},
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
size: 1024 * 256,
|
size: 1024 * 256,
|
||||||
isLocalAddressAllowed: isLocalAddressAllowed,
|
|
||||||
}, {
|
}, {
|
||||||
throwErrorWhenResponseNotOk: true,
|
throwErrorWhenResponseNotOk: true,
|
||||||
validators: [validateContentTypeSetAsActivityPub],
|
validators: [validateContentTypeSetAsActivityPub],
|
||||||
|
@ -203,13 +107,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, [finalUrl]);
|
assertActivityMatchesUrls(activity, [url, finalUrl]);
|
||||||
|
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getJson<T = unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>, isLocalAddressAllowed = false): Promise<T> {
|
public async getJson<T = unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<T> {
|
||||||
const res = await this.send(url, {
|
const res = await this.send(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: Object.assign({
|
headers: Object.assign({
|
||||||
|
@ -217,21 +121,19 @@ 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>, isLocalAddressAllowed = false): Promise<string> {
|
public async getHtml(url: string, accept = 'text/html, */*', headers?: Record<string, string>): 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();
|
||||||
|
@ -246,7 +148,6 @@ 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,
|
||||||
|
@ -278,7 +179,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, false),
|
agent: (url) => this.getAgentByUrl(url),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,6 @@ 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';
|
||||||
|
|
||||||
|
@ -219,7 +218,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
@ -546,21 +544,13 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
this.followingsRepository.findBy({
|
this.followingsRepository.findBy({
|
||||||
followeeId: user.id,
|
followeeId: user.id,
|
||||||
notify: 'normal',
|
notify: 'normal',
|
||||||
}).then(async followings => {
|
}).then(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;
|
this.notificationService.createNotification(following.followerId, 'note', {
|
||||||
if (isPureRenote) {
|
noteId: note.id,
|
||||||
const userIdsWhoMeMutingRenotes = await this.cacheService.renoteMutingsCache.fetch(following.followerId);
|
}, user.id);
|
||||||
isRenoteMuted = userIdsWhoMeMutingRenotes.has(user.id);
|
|
||||||
}
|
|
||||||
if (!isRenoteMuted) {
|
|
||||||
this.notificationService.createNotification(following.followerId, 'note', {
|
|
||||||
noteId: note.id,
|
|
||||||
}, user.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -83,11 +83,7 @@ export class WebAuthnService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async verifyRegistration(
|
public async verifyRegistration(userId: MiUser['id'], response: RegistrationResponseJSON): Promise<{
|
||||||
userId: MiUser['id'],
|
|
||||||
response: RegistrationResponseJSON,
|
|
||||||
twoFactorOnly: boolean = false,
|
|
||||||
): Promise<{
|
|
||||||
credentialID: string;
|
credentialID: string;
|
||||||
credentialPublicKey: Uint8Array;
|
credentialPublicKey: Uint8Array;
|
||||||
attestationObject: Uint8Array;
|
attestationObject: Uint8Array;
|
||||||
|
@ -115,7 +111,7 @@ export class WebAuthnService {
|
||||||
expectedChallenge: challenge,
|
expectedChallenge: challenge,
|
||||||
expectedOrigin: relyingParty.origin,
|
expectedOrigin: relyingParty.origin,
|
||||||
expectedRPID: relyingParty.rpId,
|
expectedRPID: relyingParty.rpId,
|
||||||
requireUserVerification: !twoFactorOnly,
|
requireUserVerification: true,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -249,11 +245,7 @@ export class WebAuthnService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async verifyAuthentication(
|
public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise<boolean> {
|
||||||
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) {
|
||||||
|
@ -310,7 +302,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: !twoFactorOnly,
|
requireUserVerification: true,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
|
@ -16,7 +16,6 @@ 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 */
|
||||||
|
@ -64,9 +63,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||||
const separator = '/';
|
const separator = '/';
|
||||||
|
|
||||||
const uri = new URL(getApId(value));
|
const uri = new URL(getApId(value));
|
||||||
if (toASCII(uri.host) !== toASCII(this.config.host)) {
|
if (uri.origin !== this.config.url) return { local: false, uri: uri.href };
|
||||||
return { local: false, uri: uri.href };
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, type, id, ...rest] = uri.pathname.split(separator);
|
const [, type, id, ...rest] = uri.pathname.split(separator);
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -487,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, actor, resolver, silent);
|
await this.apNoteService.createNote(note, resolver, silent);
|
||||||
return 'ok';
|
return 'ok';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof StatusError && !err.isRetryable) {
|
if (err instanceof StatusError && !err.isRetryable) {
|
||||||
|
@ -835,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, actor, resolver).catch(err => console.error(err));
|
await this.apQuestionService.updateQuestion(object, 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)}`;
|
||||||
|
|
|
@ -11,14 +11,11 @@ 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;
|
||||||
|
@ -148,7 +145,6 @@ 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') と言われる
|
||||||
|
@ -255,11 +251,7 @@ 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;
|
|
||||||
|
|
||||||
assertActivityMatchesUrls(activity, [url, finalUrl]);
|
return await res.json();
|
||||||
|
|
||||||
return activity;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,6 @@ 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, IUnsanitizedObject } 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>;
|
||||||
|
@ -55,16 +53,11 @@ export class Resolver {
|
||||||
return Array.from(this.history);
|
return Array.from(this.history);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public getRecursionLimit(): number {
|
|
||||||
return this.recursionLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolveCollection(value: string | IObject): Promise<ICollection | IOrderedCollection> {
|
public async resolveCollection(value: string | IObject): Promise<ICollection | IOrderedCollection> {
|
||||||
const collection = typeof value === 'string'
|
const collection = typeof value === 'string'
|
||||||
? await this.resolve(value)
|
? await this.resolve(value)
|
||||||
: yumeNormalizeObject(value);
|
: value;
|
||||||
|
|
||||||
if (isCollectionOrOrderedCollection(collection)) {
|
if (isCollectionOrOrderedCollection(collection)) {
|
||||||
return collection;
|
return collection;
|
||||||
|
@ -74,7 +67,7 @@ export class Resolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async resolveNotNormalized(value: string | IObject): Promise<IUnsanitizedObject> {
|
public async resolveNotNormalized(value: string | IObject): Promise<IUnsanitizedObject> {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
@ -121,17 +114,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,28 +4,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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,7 +3,6 @@
|
||||||
* 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';
|
||||||
|
@ -37,7 +36,6 @@ 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 {
|
||||||
|
@ -79,7 +77,7 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public validateNote(object: IObject, uri: string, actor?: MiRemoteUser): Error | null {
|
public validateNote(object: IObject, uri: string): Error | null {
|
||||||
const expectHost = this.utilityService.extractDbHost(uri);
|
const expectHost = this.utilityService.extractDbHost(uri);
|
||||||
const apType = getApType(object);
|
const apType = getApType(object);
|
||||||
|
|
||||||
|
@ -100,14 +98,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,14 +115,11 @@ export class ApNoteService {
|
||||||
* Noteを作成します。
|
* Noteを作成します。
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise<MiNote | null> {
|
public async createNote(value: string | IObject, resolver: Resolver, silent = false): Promise<MiNote | null> {
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
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, actor);
|
const err = this.validateNote(object, entryUri);
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err.message, {
|
this.logger.error(err.message, {
|
||||||
resolver: { history: resolver.getHistory() },
|
resolver: { history: resolver.getHistory() },
|
||||||
|
@ -146,27 +133,14 @@ 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 == null) {
|
if (note.id && !checkHttps(note.id)) {
|
||||||
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 != null) {
|
if (url && !checkHttps(url)) {
|
||||||
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}`);
|
||||||
|
@ -179,9 +153,8 @@ export class ApNoteService {
|
||||||
const uri = getOneApId(note.attributedTo);
|
const uri = getOneApId(note.attributedTo);
|
||||||
|
|
||||||
// ローカルで投稿者を検索し、もし凍結されていたらスキップ
|
// ローカルで投稿者を検索し、もし凍結されていたらスキップ
|
||||||
// eslint-disable-next-line no-param-reassign
|
const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser;
|
||||||
actor ??= await this.apPersonService.fetchPerson(uri) as MiRemoteUser | undefined;
|
if (cachedActor && cachedActor.isSuspended) {
|
||||||
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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,8 +186,7 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser;
|
||||||
actor ??= await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser;
|
|
||||||
|
|
||||||
// 解決した投稿者が凍結されていたらスキップ
|
// 解決した投稿者が凍結されていたらスキップ
|
||||||
if (actor.isSuspended) {
|
if (actor.isSuspended) {
|
||||||
|
@ -373,11 +345,15 @@ 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, undefined, options.resolver, true);
|
return await this.createNote(createFrom, this.apResolverService.createResolver(), true);
|
||||||
} finally {
|
} finally {
|
||||||
unlock();
|
unlock();
|
||||||
}
|
}
|
||||||
|
|
|
@ -661,7 +661,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updateFeatured(userId: MiUser['id'], resolver?: Resolver): Promise<void> {
|
public async updateFeatured(userId: MiUser['id'], resolver?: Resolver): Promise<void> {
|
||||||
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
const user = await this.usersRepository.findOneByOrFail({ id: userId, isDeleted: false });
|
||||||
if (!this.userEntityService.isRemoteUser(user)) return;
|
if (!this.userEntityService.isRemoteUser(user)) return;
|
||||||
if (!user.featured) return;
|
if (!user.featured) return;
|
||||||
|
|
||||||
|
|
|
@ -5,19 +5,16 @@
|
||||||
|
|
||||||
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 { UsersRepository, NotesRepository, PollsRepository } from '@/models/_.js';
|
import type { 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 { getOneApId, isQuestion } from '../type.js';
|
import { 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 } from '../type.js';
|
import type { IObject, IQuestion } from '../type.js';
|
||||||
import { yumeAssertAcceptableURL } from '../misc/validator.js';
|
|
||||||
import { toASCII } from 'punycode';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApQuestionService {
|
export class ApQuestionService {
|
||||||
|
@ -27,9 +24,6 @@ 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,
|
||||||
|
|
||||||
|
@ -71,41 +65,28 @@ export class ApQuestionService {
|
||||||
* @returns true if updated
|
* @returns true if updated
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updateQuestion(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver): Promise<boolean> {
|
public async updateQuestion(value: string | IObject, resolver?: Resolver): Promise<boolean> {
|
||||||
const uriIn = typeof value === 'string' ? value : value.id;
|
const uri = typeof value === 'string' ? value : value.id;
|
||||||
if (uriIn == null) throw new Error('uri is null');
|
if (uri == null) throw new Error('uri is null');
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
const uri = yumeAssertAcceptableURL(uriIn);
|
if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local');
|
||||||
|
|
||||||
if (toASCII(this.config.host) === uri.host) throw new Error('uri points local');
|
|
||||||
|
|
||||||
//#region このサーバーに既に登録されているか
|
//#region このサーバーに既に登録されているか
|
||||||
const note = await this.notesRepository.findOneBy({ uri: uriIn });
|
const note = await this.notesRepository.findOneBy({ uri });
|
||||||
if (note == null) throw new Error('Question is not registered');
|
if (note == null) throw new Error('Question is not registered');
|
||||||
|
|
||||||
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);
|
const question = await resolver.resolve(value) as IQuestion;
|
||||||
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
||||||
|
|
||||||
if (!isQuestion(question)) throw new Error('object is not a Question');
|
if (question.type !== '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);
|
||||||
|
@ -115,7 +96,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 || !(Number.isInteger(newCount) && newCount >= 0)) throw new Error('invalid newCount: ' + newCount);
|
if (newCount == null) throw new Error('invalid newCount: ' + newCount);
|
||||||
|
|
||||||
if (oldCount !== newCount) {
|
if (oldCount !== newCount) {
|
||||||
changed = true;
|
changed = true;
|
||||||
|
|
|
@ -103,33 +103,26 @@ export function yumeNormalizeRecursive<O extends IUnsanitizedObject | string | (
|
||||||
if (object.length > 64) {
|
if (object.length > 64) {
|
||||||
throw new bull.UnrecoverableError('array length limit exceeded');
|
throw new bull.UnrecoverableError('array length limit exceeded');
|
||||||
}
|
}
|
||||||
return object.flatMap((x) => yumeNormalizeRecursive(x, depth + (object.length + 3 / 4)));
|
return object.flatMap(yumeNormalizeRecursive);
|
||||||
}
|
}
|
||||||
|
|
||||||
return yumeNormalizeObject(object, depth + 1);
|
return yumeNormalizeObject(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function yumeNormalizeObject(object: IUnsanitizedObject, depth = 0): IObject {
|
export function yumeNormalizeObject(object: IUnsanitizedObject): IObject {
|
||||||
if (object.cc) {
|
if (object.cc) {
|
||||||
object.cc = yumeNormalizeRecursive(object.cc, depth + 1);
|
object.cc = yumeNormalizeRecursive(object.cc);
|
||||||
}
|
}
|
||||||
if (object.id) {
|
if (object.id) {
|
||||||
object.id = yumeNormalizeURL(object.id);
|
object.id = yumeNormalizeURL(object.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (object.url) {
|
if (object.url) {
|
||||||
object.url = yumeNormalizeRecursive(object.url, depth + 1);
|
object.url = yumeNormalizeRecursive(object.url);
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
if (object.inReplyTo) {
|
||||||
object.inReplyTo = yumeNormalizeRecursive(object.inReplyTo, depth + 1);
|
object.inReplyTo = yumeNormalizeRecursive(object.inReplyTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return object as IObject;
|
return object as IObject;
|
||||||
|
@ -202,8 +195,6 @@ export interface IActivity extends IObject {
|
||||||
|
|
||||||
export interface SafeList {
|
export interface SafeList {
|
||||||
id: string;
|
id: string;
|
||||||
content: string | null;
|
|
||||||
tag: IObject | IObject[];
|
|
||||||
published: string;
|
published: string;
|
||||||
visibility: string;
|
visibility: string;
|
||||||
mentionedUsers: any[];
|
mentionedUsers: any[];
|
||||||
|
@ -213,8 +204,6 @@ export interface SafeList {
|
||||||
function extractSafe(object: IObject): Partial<SafeList> {
|
function extractSafe(object: IObject): Partial<SafeList> {
|
||||||
return {
|
return {
|
||||||
id: object.id,
|
id: object.id,
|
||||||
content: object.content,
|
|
||||||
tag: object.tag,
|
|
||||||
published: object.published,
|
published: object.published,
|
||||||
visibility: object.visibility,
|
visibility: object.visibility,
|
||||||
mentionedUsers: object.mentionedUsers,
|
mentionedUsers: object.mentionedUsers,
|
||||||
|
@ -622,7 +611,7 @@ export function yumeDowncastRemove(object: IObject): IRemove | null {
|
||||||
export function yumeDowncastLike(object: IObject): ILike | null {
|
export function yumeDowncastLike(object: IObject): ILike | null {
|
||||||
if (getApType(object) !== 'Like') return null;
|
if (getApType(object) !== 'Like') return null;
|
||||||
const obj = object as ILike;
|
const obj = object as ILike;
|
||||||
if (!obj.actor || !obj.object) return null;
|
if (!obj.actor || !obj.object || !obj.target) return null;
|
||||||
return {
|
return {
|
||||||
...extractMisskeyVendorKeys(object),
|
...extractMisskeyVendorKeys(object),
|
||||||
...extractSafe(object),
|
...extractSafe(object),
|
||||||
|
|
|
@ -255,8 +255,6 @@ 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();
|
||||||
|
|
|
@ -105,7 +105,7 @@ export class ActivityPubServerService {
|
||||||
let signature;
|
let signature;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'host', 'date'], authorizationHeaderName: 'signature' });
|
signature = httpSignature.parseRequest(request.raw, { 'headers': [] });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reply.code(401);
|
reply.code(401);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -278,17 +278,7 @@ 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',
|
reply.header('permissions-policy', 'interest-cohort=()'); // Disable FLoC
|
||||||
[
|
|
||||||
'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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -255,7 +255,7 @@ export class SigninApiService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential, !profile.usePasswordLessLogin);
|
const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential);
|
||||||
|
|
||||||
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, me);
|
await this.deleteAccoountService.deleteAccount(user);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, me) => {
|
super(meta, paramDef, async (ps) => {
|
||||||
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, me);
|
await this.deleteAccountService.deleteAccount(user);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ 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,11 +118,6 @@ 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;
|
||||||
|
@ -140,7 +135,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return await this.mergePack(
|
return await this.mergePack(
|
||||||
me,
|
me,
|
||||||
isActor(object) ? await this.apPersonService.createPerson(getApId(object), resolver) : null,
|
isActor(object) ? await this.apPersonService.createPerson(getApId(object), resolver) : null,
|
||||||
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, resolver, true) : null,
|
isPost(object) ? await this.apNoteService.createNote(getApId(object), 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, !profile.usePasswordLessLogin);
|
const keyInfo = await this.webAuthnService.verifyRegistration(me.id, ps.credential);
|
||||||
const keyId = keyInfo.credentialID;
|
const keyId = keyInfo.credentialID;
|
||||||
|
|
||||||
await this.userSecurityKeysRepository.insert({
|
await this.userSecurityKeysRepository.insert({
|
||||||
|
|
|
@ -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} src="${dataURI}"></${options.tagName}>`;
|
return `<${options.tagName} data="${dataURI}"></${options.tagName}>`;
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -248,6 +248,16 @@ 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);
|
||||||
|
@ -493,7 +503,6 @@ 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'),
|
||||||
});
|
});
|
||||||
|
@ -552,7 +561,6 @@ 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);
|
||||||
|
@ -603,25 +611,16 @@ export class ClientServerService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region SSR
|
//#region SSR (for crawlers)
|
||||||
// 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) {
|
||||||
|
@ -637,19 +636,11 @@ 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 {
|
||||||
// リモートユーザーなので
|
// リモートユーザーなので
|
||||||
|
@ -663,7 +654,6 @@ 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) {
|
||||||
|
@ -703,9 +693,6 @@ 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);
|
||||||
|
@ -715,14 +702,9 @@ 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: IsNull(),
|
host: host ?? IsNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
|
@ -799,9 +781,6 @@ 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,6 +118,7 @@ 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;
|
||||||
|
@ -144,6 +145,6 @@ export class UrlPreviewService {
|
||||||
contentLengthRequired: meta.urlPreviewRequireContentLength,
|
contentLengthRequired: meta.urlPreviewRequireContentLength,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.httpRequestService.getJson<SummalyResult>(`${proxy}?${queryStr}`, 'application/json, */*', undefined, true);
|
return this.httpRequestService.getJson<SummalyResult>(`${proxy}?${queryStr}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,136 +5,6 @@
|
||||||
|
|
||||||
'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) => {
|
||||||
|
@ -146,24 +16,10 @@ class Systemd {
|
||||||
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) {
|
||||||
await systemd.startSync('Force Error Service', () => {
|
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
|
||||||
throw new Error('This error is forced by having forceError in local storage.');
|
return;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Detect language & fetch translations
|
//#region Detect language & fetch translations
|
||||||
|
@ -181,7 +37,7 @@ class Systemd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const metaRes = await systemd.start('Fetch /api/meta',window.fetch('/api/meta', {
|
const metaRes = await window.fetch('/api/meta', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({}),
|
body: JSON.stringify({}),
|
||||||
credentials: 'omit',
|
credentials: 'omit',
|
||||||
|
@ -189,12 +45,12 @@ class Systemd {
|
||||||
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 systemd.start('Parse /api/meta', metaRes.json());
|
const meta = await metaRes.json();
|
||||||
const v = meta.version;
|
const v = meta.version;
|
||||||
if (v == null) {
|
if (v == null) {
|
||||||
renderError('META_FETCH_V');
|
renderError('META_FETCH_V');
|
||||||
|
@ -207,7 +63,7 @@ class Systemd {
|
||||||
lang = 'en-US';
|
lang = 'en-US';
|
||||||
}
|
}
|
||||||
|
|
||||||
const localRes = await systemd.start('Fetch Locale files', window.fetch(`/assets/locales/${lang}.${v}.json`));
|
const localRes = await 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());
|
||||||
|
@ -221,25 +77,19 @@ class Systemd {
|
||||||
|
|
||||||
//#region Script
|
//#region Script
|
||||||
async function importAppScript() {
|
async function importAppScript() {
|
||||||
await systemd.start('Load App Script', import(`/vite/${CLIENT_ENTRY}`))
|
await 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') {
|
||||||
systemd.start('import App Script', importAppScript());
|
importAppScript();
|
||||||
} else {
|
} else {
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
systemd.start('import App Script', importAppScript());
|
importAppScript();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -247,21 +97,19 @@ class Systemd {
|
||||||
//#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());
|
|
||||||
|
|
||||||
// HTMLの theme-color 適用
|
// HTMLの theme-color 適用
|
||||||
if (k === 'htmlThemeColor') {
|
if (k === 'htmlThemeColor') {
|
||||||
for (const tag of document.head.children) {
|
for (const tag of document.head.children) {
|
||||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||||
tag.setAttribute('content', v);
|
tag.setAttribute('content', v);
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
const colorScheme = localStorage.getItem('colorScheme');
|
const colorScheme = localStorage.getItem('colorScheme');
|
||||||
if (colorScheme) {
|
if (colorScheme) {
|
||||||
|
@ -286,22 +134,181 @@ class Systemd {
|
||||||
|
|
||||||
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) {
|
||||||
systemd.emergency_mode(code, details);
|
// Cannot set property 'innerHTML' of null を回避
|
||||||
|
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,32 +9,6 @@ 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;
|
||||||
|
@ -68,7 +42,7 @@ html {
|
||||||
left: 0;
|
left: 0;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 60px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
transform: translateY(70px);
|
transform: translateY(70px);
|
||||||
color: var(--MI_THEME-accent);
|
color: var(--MI_THEME-accent);
|
||||||
|
@ -86,16 +60,6 @@ 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: 60px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
transform: translateY(70px);
|
transform: translateY(70px);
|
||||||
color: var(--MI_THEME-accent);
|
color: var(--MI_THEME-accent);
|
||||||
|
@ -82,16 +82,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,151 +0,0 @@
|
||||||
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,8 +56,18 @@ 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
|
||||||
<span>Loading...</span>
|
:dataTag(tagName='img' mimeType='image/svg+xml')
|
||||||
|
<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,6 +9,17 @@ 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
|
||||||
|
@ -61,10 +72,8 @@ 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(type='application/json' id='misskey_clientCtx' data-generated-at=now)
|
|
||||||
!= clientCtx
|
|
||||||
|
|
||||||
|
script(integrity=bootJS.integrity) !{bootJS.content}
|
||||||
|
|
||||||
body
|
body
|
||||||
noscript: p
|
noscript: p
|
||||||
|
@ -72,11 +81,18 @@ 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
|
||||||
<span>Loading...</span>
|
:dataTag(tagName='img' mimeType='image/svg+xml')
|
||||||
|
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||||
script(integrity=bootJS.integrity) !{bootJS.content}
|
<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
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
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
|
||||||
|
|
|
@ -207,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, undefined, resolver, true);
|
const note = await noteService.createNote(post.id, resolver, true);
|
||||||
|
|
||||||
assert.deepStrictEqual(note?.uri, post.id);
|
assert.deepStrictEqual(note?.uri, post.id);
|
||||||
assert.deepStrictEqual(note.visibility, 'public');
|
assert.deepStrictEqual(note.visibility, 'public');
|
||||||
|
@ -370,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, undefined, resolver);
|
const note = await noteService.createNote(firstNote.id as string, resolver);
|
||||||
assert.strictEqual(note?.uri, firstNote.id);
|
assert.strictEqual(note?.uri, firstNote.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -45,8 +45,7 @@ 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((e) => {
|
.catch(() => {
|
||||||
console.error(e);
|
|
||||||
return Promise.reject(null);
|
return Promise.reject(null);
|
||||||
})
|
})
|
||||||
.then((credential) => {
|
.then((credential) => {
|
||||||
|
@ -54,7 +53,7 @@ async function queryKey() {
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
queryingKey.value = false;
|
queryingKey.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
|
@ -25,18 +25,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, inject, ref } from 'vue';
|
import { computed, 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 { misskeyApi, 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 * 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 { $i } from '@/account.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -126,31 +125,9 @@ 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" :class="[$style.bgcircle]"/>
|
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
|
||||||
</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" :class="[$style.fgpath]"/>
|
<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;"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,16 +109,4 @@ 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>
|
||||||
|
|
|
@ -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 }} (Upstream)
|
{{ i18n.ts.aboutMisskey }}
|
||||||
</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,28 +33,25 @@ 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 { getServerContext } from '@/server-context.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
|
|
||||||
const CTX_CLIP = getServerContext('clip');
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
clipId: string,
|
clipId: string,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const clip = ref<Misskey.entities.Clip | null>(CTX_CLIP);
|
const clip = ref<Misskey.entities.Clip | null>(null);
|
||||||
const favorited = ref(false);
|
const favorited = ref(false);
|
||||||
const pagination = {
|
const pagination = {
|
||||||
endpoint: 'clips/notes' as const,
|
endpoint: 'clips/notes' as const,
|
||||||
|
@ -67,11 +64,6 @@ 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,22 +15,18 @@ 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) {
|
||||||
const menuItems: MenuItem[] = [];
|
os.popupMenu([{
|
||||||
menuItems.push({
|
|
||||||
type: 'label',
|
type: 'label',
|
||||||
text: ':' + props.emoji.name + ':',
|
text: ':' + props.emoji.name + ':',
|
||||||
}, {
|
}, {
|
||||||
|
@ -52,28 +48,8 @@ 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,16 +62,13 @@ 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>(CTX_NOTE);
|
const note = ref<null | Misskey.entities.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);
|
||||||
|
@ -119,12 +116,6 @@ 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 && user.roles.length > 0" class="roles">
|
<div v-if="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 v-if="user.createdAt" class="field">
|
<dl 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,7 +39,6 @@ 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'));
|
||||||
|
@ -53,8 +52,6 @@ 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;
|
||||||
|
@ -64,24 +61,13 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
const tab = ref(props.page);
|
const tab = ref(props.page);
|
||||||
|
|
||||||
const user = ref<null | Misskey.entities.UserDetailed>(CTX_USER);
|
const user = ref<null | Misskey.entities.UserDetailed>(null);
|
||||||
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', {
|
misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => {
|
||||||
username,
|
|
||||||
host,
|
|
||||||
}).then(u => {
|
|
||||||
user.value = u;
|
user.value = u;
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
error.value = err;
|
error.value = err;
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,6 +5,8 @@ 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は、要望、提案、問題の報告にのみ使用してください。
|
||||||
- 質問は、@yume@mi.yumechi.jp / yume@mi.yumechi.jp でお願いします。
|
- 質問は、[GitHub Discussions](https://github.com/misskey-dev/misskey/discussions)や[Discord](https://discord.gg/Wp8gVStHW3)でお願いします。
|
||||||
|
|
||||||
## PRの作成
|
## PRの作成
|
||||||
PRを作成する前に、以下をご確認ください:
|
PRを作成する前に、以下をご確認ください:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "misskey-js",
|
"name": "misskey-js",
|
||||||
"version": "2024.11.0-yumechinokuni.7",
|
"version": "2024.11.0-yumechinokuni.5",
|
||||||
"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://forge.yumechi.jp/yume/yumechi-no-kuni.git",
|
"url": "https://github.com/misskey-dev/misskey.git",
|
||||||
"directory": "packages/misskey-js"
|
"directory": "packages/misskey-js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -12,242 +12,209 @@ 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';
|
||||||
|
|
||||||
const STATIC_CACHE_NAME = `misskey-static-${_VERSION_}`;
|
globalThis.addEventListener('install', () => {
|
||||||
const PATHS_TO_CACHE = ['/assets/', '/static-assets/', '/emoji/', '/twemoji/', '/fluent-emoji/', '/vite/'];
|
// ev.waitUntil(globalThis.skipWaiting());
|
||||||
|
|
||||||
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 !== STATIC_CACHE_NAME && v !== swLang.cacheName)
|
.filter((v) => v !== swLang.cacheName)
|
||||||
.map((name) => caches.delete(name)),
|
.map(name => caches.delete(name)),
|
||||||
))
|
))
|
||||||
.then(() => globalThis.clients.claim()),
|
.then(() => globalThis.clients.claim()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function offlineContentHTML() {
|
async function offlineContentHTML() {
|
||||||
const i18n = await (swLang.i18n ?? swLang.fetchLocale()) as Partial<I18n<Locale>>;
|
const i18n = await (swLang.i18n ?? swLang.fetchLocale()) as Partial<I18n<Locale>>;
|
||||||
const messages = {
|
const messages = {
|
||||||
title: i18n.ts?._offlineScreen.title ?? 'Offline - Could not connect to server',
|
title: i18n.ts?._offlineScreen.title ?? 'Offline - Could not connect to server',
|
||||||
header: i18n.ts?._offlineScreen.header ?? 'Could not connect to server',
|
header: i18n.ts?._offlineScreen.header ?? 'Could not connect to server',
|
||||||
reload: i18n.ts?.reload ?? 'Reload',
|
reload: i18n.ts?.reload ?? 'Reload',
|
||||||
};
|
};
|
||||||
|
|
||||||
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>`;
|
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>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
globalThis.addEventListener('fetch', (ev) => {
|
globalThis.addEventListener('fetch', ev => {
|
||||||
const shouldCache = PATHS_TO_CACHE.some((path) => ev.request.url.includes(path));
|
let isHTMLRequest = false;
|
||||||
|
if (ev.request.headers.get('sec-fetch-dest') === 'document') {
|
||||||
|
isHTMLRequest = true;
|
||||||
|
} else if (ev.request.headers.get('accept')?.includes('/html')) {
|
||||||
|
isHTMLRequest = true;
|
||||||
|
} else if (ev.request.url.endsWith('/')) {
|
||||||
|
isHTMLRequest = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldCache) {
|
if (!isHTMLRequest) return;
|
||||||
ev.respondWith(
|
ev.respondWith(
|
||||||
caches.match(ev.request)
|
fetch(ev.request)
|
||||||
.then((response) => {
|
.catch(async () => {
|
||||||
if (response) return response;
|
const html = await offlineContentHTML();
|
||||||
|
return new Response(html, {
|
||||||
return fetch(ev.request).then((response) => {
|
status: 200,
|
||||||
if (!response || response.status !== 200 || response.type !== 'basic') return response;
|
headers: {
|
||||||
const responseToCache = response.clone();
|
'content-type': 'text/html',
|
||||||
caches.open(STATIC_CACHE_NAME)
|
},
|
||||||
.then((cache) => {
|
});
|
||||||
cache.put(ev.request, responseToCache);
|
}),
|
||||||
});
|
);
|
||||||
return response;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let isHTMLRequest = false;
|
|
||||||
if (ev.request.headers.get('sec-fetch-dest') === 'document') {
|
|
||||||
isHTMLRequest = true;
|
|
||||||
} else if (ev.request.headers.get('accept')?.includes('/html')) {
|
|
||||||
isHTMLRequest = true;
|
|
||||||
} else if (ev.request.url.endsWith('/')) {
|
|
||||||
isHTMLRequest = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isHTMLRequest) return;
|
|
||||||
ev.respondWith(
|
|
||||||
fetch(ev.request)
|
|
||||||
.catch(async () => {
|
|
||||||
const html = await offlineContentHTML();
|
|
||||||
return new Response(html, {
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
'content-type': 'text/html',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
globalThis.addEventListener('push', (ev) => {
|
globalThis.addEventListener('push', ev => {
|
||||||
ev.waitUntil(globalThis.clients.matchAll({
|
// クライアント取得
|
||||||
includeUncontrolled: true,
|
ev.waitUntil(globalThis.clients.matchAll({
|
||||||
type: 'window',
|
includeUncontrolled: true,
|
||||||
}).then(async () => {
|
type: 'window',
|
||||||
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.data?.json();
|
}).then(async () => {
|
||||||
|
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.data?.json();
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'notification':
|
// case 'driveFileCreated':
|
||||||
case 'unreadAntennaNote':
|
case 'notification':
|
||||||
if (Date.now() - data.dateTime > 1000 * 60 * 60 * 24) break;
|
case 'unreadAntennaNote':
|
||||||
|
// 1日以上経過している場合は無視
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
await createEmptyNotification();
|
await createEmptyNotification();
|
||||||
return;
|
return;
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEventMap['notificationclick']) => {
|
globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEventMap['notificationclick']) => {
|
||||||
ev.waitUntil((async (): Promise<void> => {
|
ev.waitUntil((async (): Promise<void> => {
|
||||||
if (_DEV_) {
|
if (_DEV_) {
|
||||||
console.log('notificationclick', ev.action, ev.notification.data);
|
console.log('notificationclick', ev.action, ev.notification.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { action, notification } = ev;
|
const { action, notification } = ev;
|
||||||
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data ?? {};
|
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data ?? {};
|
||||||
const { userId: loginId } = data;
|
const { userId: loginId } = data;
|
||||||
let client: WindowClient | null = null;
|
let client: WindowClient | null = null;
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'notification':
|
case 'notification':
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'follow':
|
case 'follow':
|
||||||
if ('userId' in data.body) await swos.api('following/create', loginId, { userId: data.body.userId });
|
if ('userId' in data.body) await swos.api('following/create', loginId, { userId: data.body.userId });
|
||||||
break;
|
break;
|
||||||
case 'showUser':
|
case 'showUser':
|
||||||
if ('user' in data.body) client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId);
|
if ('user' in data.body) client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId);
|
||||||
break;
|
break;
|
||||||
case 'reply':
|
case 'reply':
|
||||||
if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId);
|
if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId);
|
||||||
break;
|
break;
|
||||||
case 'renote':
|
case 'renote':
|
||||||
if ('note' in data.body) await swos.api('notes/create', loginId, { renoteId: data.body.note.id });
|
if ('note' in data.body) await swos.api('notes/create', loginId, { renoteId: data.body.note.id });
|
||||||
break;
|
break;
|
||||||
case 'accept':
|
case 'accept':
|
||||||
switch (data.body.type) {
|
switch (data.body.type) {
|
||||||
case 'receiveFollowRequest':
|
case 'receiveFollowRequest':
|
||||||
await swos.api('following/requests/accept', loginId, { userId: data.body.userId });
|
await swos.api('following/requests/accept', loginId, { userId: data.body.userId });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'reject':
|
case 'reject':
|
||||||
switch (data.body.type) {
|
switch (data.body.type) {
|
||||||
case 'receiveFollowRequest':
|
case 'receiveFollowRequest':
|
||||||
await swos.api('following/requests/reject', loginId, { userId: data.body.userId });
|
await swos.api('following/requests/reject', loginId, { userId: data.body.userId });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'showFollowRequests':
|
case 'showFollowRequests':
|
||||||
client = await swos.openClient('push', '/my/follow-requests', loginId);
|
client = await swos.openClient('push', '/my/follow-requests', loginId);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
switch (data.body.type) {
|
switch (data.body.type) {
|
||||||
case 'receiveFollowRequest':
|
case 'receiveFollowRequest':
|
||||||
client = await swos.openClient('push', '/my/follow-requests', loginId);
|
client = await swos.openClient('push', '/my/follow-requests', loginId);
|
||||||
break;
|
break;
|
||||||
case 'reaction':
|
case 'reaction':
|
||||||
client = await swos.openNote(data.body.note.id, loginId);
|
client = await swos.openNote(data.body.note.id, loginId);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if ('note' in data.body) {
|
if ('note' in data.body) {
|
||||||
client = await swos.openNote(data.body.note.id, loginId);
|
client = await swos.openNote(data.body.note.id, loginId);
|
||||||
} else if ('user' in data.body) {
|
} else if ('user' in data.body) {
|
||||||
client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId);
|
client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'unreadAntennaNote':
|
case 'unreadAntennaNote':
|
||||||
client = await swos.openAntenna(data.body.antenna.id, loginId);
|
client = await swos.openAntenna(data.body.antenna.id, loginId);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
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);
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'settings':
|
case 'settings':
|
||||||
client = await swos.openClient('push', '/settings/notifications', loginId);
|
client = await swos.openClient('push', '/settings/notifications', loginId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client) {
|
if (client) {
|
||||||
client.focus();
|
client.focus();
|
||||||
}
|
}
|
||||||
if (data.type === 'notification') {
|
if (data.type === 'notification') {
|
||||||
await swos.sendMarkAllAsRead(loginId);
|
await swos.sendMarkAllAsRead(loginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
notification.close();
|
notification.close();
|
||||||
})());
|
})());
|
||||||
});
|
});
|
||||||
|
|
||||||
globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => {
|
globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => {
|
||||||
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data;
|
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data;
|
||||||
|
|
||||||
ev.waitUntil((async (): Promise<void> => {
|
ev.waitUntil((async (): Promise<void> => {
|
||||||
if (data.type === 'notification') {
|
if (data.type === 'notification') {
|
||||||
await swos.sendMarkAllAsRead(data.userId);
|
await swos.sendMarkAllAsRead(data.userId);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
})());
|
})());
|
||||||
});
|
});
|
||||||
|
|
||||||
globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => {
|
globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => {
|
||||||
ev.waitUntil((async (): Promise<void> => {
|
ev.waitUntil((async (): Promise<void> => {
|
||||||
if (ev.data === 'clear') {
|
switch (ev.data) {
|
||||||
await caches.keys()
|
case 'clear':
|
||||||
.then((cacheNames) => Promise.all(
|
// Cache Storage全削除
|
||||||
cacheNames.map((name) => caches.delete(name)),
|
await caches.keys()
|
||||||
));
|
.then(cacheNames => Promise.all(
|
||||||
return;
|
cacheNames.map(name => caches.delete(name)),
|
||||||
}
|
));
|
||||||
|
return; // TODO
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof ev.data === 'object') {
|
if (typeof ev.data === 'object') {
|
||||||
const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase();
|
// E.g. '[object Array]' → 'array'
|
||||||
|
const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase();
|
||||||
|
|
||||||
if (otype === 'object') {
|
if (otype === 'object') {
|
||||||
if (ev.data.msg === 'initialize') {
|
if (ev.data.msg === 'initialize') {
|
||||||
swLang.setLang(ev.data.lang);
|
swLang.setLang(ev.data.lang);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})());
|
})());
|
||||||
});
|
});
|
||||||
|
|
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.2",
|
"sync_wrapper 1.0.1",
|
||||||
"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.2",
|
"sync_wrapper 1.0.1",
|
||||||
"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=v0.0.1#46456b0a61b449dad7bbe85e0342bdd5e3b6e031"
|
source = "git+https://forge.yumechi.jp/yume/fedivet?tag=testing-audit%2Brelay%2Bfilter#b1b051dc2f1319a3948d7afcecfd3ac8f92a07de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
|
@ -588,9 +588,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.7"
|
version = "0.4.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
|
checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -683,9 +683,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.5.1"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f"
|
checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
|
@ -940,9 +940,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.13"
|
version = "1.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2"
|
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
|
@ -1192,9 +1192,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.92"
|
version = "1.0.89"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
|
||||||
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.2",
|
"sync_wrapper 1.0.1",
|
||||||
"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.27"
|
version = "0.1.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
|
checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1"
|
||||||
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.89"
|
version = "2.0.87"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e"
|
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||||
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.2"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
]
|
]
|
||||||
|
@ -1814,9 +1814,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.14"
|
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 = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||||
|
|
||||||
[[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 = "v0.0.1" }
|
fedivet = { git = "https://forge.yumechi.jp/yume/fedivet", tag = "testing-audit+relay+filter" }
|
||||||
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