Merge branch 'develop' into test
This commit is contained in:
commit
c1ec2f6ab6
5
.github/ISSUE_TEMPLATE/01_bug-report.md
vendored
5
.github/ISSUE_TEMPLATE/01_bug-report.md
vendored
@ -22,7 +22,10 @@ First, in order to avoid duplicate Issues, please search to see if the problem y
|
||||
|
||||
## 🤬 Actual Behavior
|
||||
|
||||
<!--- Tell us what happens instead of the expected behavior -->
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
## 📝 Steps to Reproduce
|
||||
|
||||
|
12
.github/labeler.yml
vendored
Normal file
12
.github/labeler.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
'⚙️Server':
|
||||
- packages/backend/**/*
|
||||
|
||||
'🖥️Client':
|
||||
- packages/client/**/*
|
||||
|
||||
'🧪Test':
|
||||
- cypress/**/*
|
||||
- packages/backend/test/**/*
|
||||
|
||||
'‼️ wrong locales':
|
||||
- any: ['locales/*.yml', '!locales/ja-JP.yml']
|
16
.github/workflows/labeler.yml
vendored
Normal file
16
.github/workflows/labeler.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
pull_request_target:
|
||||
branches-ignore:
|
||||
- 'l10n_develop'
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
36
.github/workflows/ok-to-test.yml
vendored
Normal file
36
.github/workflows/ok-to-test.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event
|
||||
name: Ok To Test
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
ok-to-test:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run for PRs, not issue comments
|
||||
if: ${{ github.event.issue.pull_request }}
|
||||
steps:
|
||||
# Generate a GitHub App installation access token from an App ID and private key
|
||||
# To create a new GitHub App:
|
||||
# https://developer.github.com/apps/building-github-apps/creating-a-github-app/
|
||||
# See app.yml for an example app manifest
|
||||
- name: Generate token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.PRIVATE_KEY }}
|
||||
|
||||
- name: Slash Command Dispatch
|
||||
uses: peter-evans/slash-command-dispatch@v1
|
||||
env:
|
||||
TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
with:
|
||||
token: ${{ env.TOKEN }} # GitHub App installation access token
|
||||
# token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work
|
||||
reaction-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-type: pull-request
|
||||
commands: ok-to-test
|
||||
named-args: true
|
||||
permission: write
|
70
.github/workflows/pr-preview-deploy.yml
vendored
Normal file
70
.github/workflows/pr-preview-deploy.yml
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
# Run secret-dependent integration tests only after /ok-to-test approval
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [ok-to-test-command]
|
||||
|
||||
name: Deploy preview environment
|
||||
|
||||
jobs:
|
||||
# Repo owner has commented /ok-to-test on a (fork-based) pull request
|
||||
deploy-preview-environment:
|
||||
runs-on: ubuntu-latest
|
||||
if:
|
||||
github.event_name == 'repository_dispatch' &&
|
||||
github.event.client_payload.slash_command.sha != '' &&
|
||||
contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha)
|
||||
steps:
|
||||
|
||||
# Check out merge commit
|
||||
- name: Fork based /ok-to-test checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'
|
||||
|
||||
# <insert integration tests needing secrets>
|
||||
- name: Context
|
||||
uses: okteto/context@latest
|
||||
with:
|
||||
token: ${{ secrets.OKTETO_TOKEN }}
|
||||
|
||||
- name: Deploy preview environment
|
||||
uses: ikuradon/deploy-preview@latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
name: pr-${{ github.event.client_payload.pull_request.number }}-misskey-dev
|
||||
timeout: 15m
|
||||
|
||||
# Update check run called "integration-fork"
|
||||
- uses: actions/github-script@v5
|
||||
id: update-check-run
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
number: ${{ github.event.client_payload.pull_request.number }}
|
||||
job: ${{ github.job }}
|
||||
# Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run
|
||||
conclusion: ${{ job.status }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { data: pull } = await github.rest.pulls.get({
|
||||
...context.repo,
|
||||
pull_number: process.env.number
|
||||
});
|
||||
const ref = pull.head.sha;
|
||||
|
||||
const { data: checks } = await github.rest.checks.listForRef({
|
||||
...context.repo,
|
||||
ref
|
||||
});
|
||||
|
||||
const check = checks.check_runs.filter(c => c.name === process.env.job);
|
||||
|
||||
const { data: result } = await github.rest.checks.update({
|
||||
...context.repo,
|
||||
check_run_id: check[0].id,
|
||||
status: 'completed',
|
||||
conclusion: process.env.conclusion
|
||||
});
|
||||
|
||||
return result;
|
21
.github/workflows/pr-preview-destroy.yml
vendored
Normal file
21
.github/workflows/pr-preview-destroy.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# file: .github/workflows/preview-closed.yaml
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
name: Destroy preview environment
|
||||
|
||||
jobs:
|
||||
destroy-preview-environment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Context
|
||||
uses: okteto/context@latest
|
||||
with:
|
||||
token: ${{ secrets.OKTETO_TOKEN }}
|
||||
|
||||
- name: Destroy preview environment
|
||||
uses: okteto/destroy-preview@latest
|
||||
with:
|
||||
name: pr-${{ github.event.number }}-misskey-dev
|
@ -1 +1 @@
|
||||
v18.0.0
|
||||
v16.15.0
|
||||
|
54
CHANGELOG.md
54
CHANGELOG.md
@ -10,32 +10,50 @@ You should also include the user name that made the change.
|
||||
-->
|
||||
|
||||
## 12.x.x (unreleased)
|
||||
### NOTE
|
||||
- From this version, Node 18.0.0 or later is required.
|
||||
|
||||
### Improvements
|
||||
- enhance: ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina
|
||||
- enhance: API: notifications/readは配列でも受け付けるように #7667 @tamaina
|
||||
- enhance: プッシュ通知を複数アカウント対応に #7667 @tamaina
|
||||
- enhance: プッシュ通知にクリックやactionを設定 #7667 @tamaina
|
||||
- replaced webpack with Vite @tamaina
|
||||
- update dependencies @syuilo
|
||||
- enhance: display URL of QR code for TOTP registration @syuilo
|
||||
- make CAPTCHA required for signin to improve security @syuilo
|
||||
- Supports Unicode Emoji 14.0 @mei23
|
||||
- プッシュ通知を複数アカウント対応に #7667 @tamaina
|
||||
- プッシュ通知にクリックやactionを設定 #7667 @tamaina
|
||||
- ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina
|
||||
- Server: always remove completed tasks of job queue @Johann150
|
||||
- Client: make emoji stand out more on reaction button @Johann150
|
||||
- Client: display URL of QR code for TOTP registration @tamaina
|
||||
- API: notifications/readは配列でも受け付けるように #7667 @tamaina
|
||||
- API: ユーザー検索で、クエリがusernameの条件を満たす場合はusernameもLIKE検索するように @tamaina
|
||||
- MFM: Allow speed changes in all animated MFMs @Johann150
|
||||
- The theme color is now better validated. @Johann150
|
||||
Your own theme color may be unset if it was in an invalid format.
|
||||
Admins should check their instance settings if in doubt.
|
||||
- Perform port diagnosis at startup only when Listen fails @mei23
|
||||
- Rate limiting is now also usable for non-authenticated users. @Johann150 @mei23
|
||||
Admins should make sure the reverse proxy sets the `X-Forwarded-For` header to the original address.
|
||||
|
||||
### Bugfixes
|
||||
- Client: fix settings page @tamaina
|
||||
- Client: fix profile tabs @futchitwo
|
||||
- Server: keep file order of note attachement @Johann150
|
||||
- Server: fix caching @Johann150
|
||||
- Server: await promises when following or unfollowing users @Johann150
|
||||
- Client: fix abuse reports page to be able to show all reports @Johann150
|
||||
- Federation: Add rel attribute to host-meta @mei23
|
||||
- Client: fix profile picture height in mentions @tamaina
|
||||
- MFM: more animated functions support `speed` parameter @futchitwo
|
||||
- Federation: Fix quote renotes containing no text being federated correctly @Johann150
|
||||
- Server: fix missing foreign key for reports leading to reports page being unusable @Johann150
|
||||
- Server: fix internal in-memory caching @Johann150
|
||||
- Server: use correct order of attachments on notes @Johann150
|
||||
- Server: prevent crash when processing certain PNGs @syuilo
|
||||
- Server: Fix unable to generate video thumbnails @mei23
|
||||
- Server: Fix `Cannot find module` issue @mei23
|
||||
- Federation: Add rel attribute to host-meta @mei23
|
||||
- Federation: add id for activitypub follows @Johann150
|
||||
- Federation: ensure resolver does not fetch local resources via HTTP(S) @Johann150
|
||||
- Federation: correctly render empty note text @Johann150
|
||||
- Federation: Fix quote renotes containing no text being federated correctly @Johann150
|
||||
- Federation: remove duplicate br tag/newline @Johann150
|
||||
- Federation: add missing authorization checks @Johann150
|
||||
- Client: fix profile picture height in mentions @tamaina
|
||||
- Client: fix abuse reports page to be able to show all reports @Johann150
|
||||
- Client: fix settings page @tamaina
|
||||
- Client: fix profile tabs @futchitwo
|
||||
- Client: fix popout URL @futchitwo
|
||||
- Client: correctly handle MiAuth URLs with query string @sn0w
|
||||
- Client: ノート詳細ページの新しいノートを表示する機能の動作が正しくなるように修正する @xianonn
|
||||
- MFM: more animated functions support `speed` parameter @futchitwo
|
||||
- MFM: limit large MFM @Johann150
|
||||
|
||||
## 12.110.1 (2022/04/23)
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
# Contribution guide
|
||||
We're glad you're interested in contributing Misskey! In this document you will find the information you need to contribute to the project.
|
||||
|
||||
**ℹ️ Important:** This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.**
|
||||
Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\
|
||||
The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language.
|
||||
It will also allow the reader to use the translation tool of their preference if necessary.
|
||||
> **Note**
|
||||
> This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.**
|
||||
> Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\
|
||||
> The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language.
|
||||
> It will also allow the reader to use the translation tool of their preference if necessary.
|
||||
|
||||
## Roadmap
|
||||
See [ROADMAP.md](./ROADMAP.md)
|
||||
@ -16,6 +17,9 @@ Before creating an issue, please check the following:
|
||||
- Issues should only be used to feature requests, suggestions, and bug tracking.
|
||||
- Please ask questions or troubleshooting in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3).
|
||||
|
||||
> **Warning**
|
||||
> Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
|
||||
|
||||
## Before implementation
|
||||
When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented.
|
||||
|
||||
@ -67,15 +71,17 @@ For now, basically only @syuilo has the authority to merge PRs into develop beca
|
||||
However, minor fixes, refactoring, and urgent changes may be merged at the discretion of a contributor.
|
||||
|
||||
## Release
|
||||
For now, basically only @syuilo has the authority to release Misskey.
|
||||
However, in case of emergency, a release can be made at the discretion of a contributor.
|
||||
|
||||
### Release Instructions
|
||||
1. commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json))
|
||||
2. follow the `master` branch to the `develop` branch.
|
||||
3. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases)
|
||||
- The target branch must be `master`
|
||||
- The tag name must be the version
|
||||
1. Commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json))
|
||||
2. Create a release PR.
|
||||
- Into `master` from `develop` branch.
|
||||
- The title must be in the format `Release: x.y.z`.
|
||||
- `x.y.z` is the new version you are trying to release.
|
||||
3. ~~Deploy and perform a simple QA check. Also verify that the tests passed.~~ (TODO)
|
||||
4. Merge it.
|
||||
5. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases)
|
||||
- The target branch must be `master`
|
||||
- The tag name must be the version
|
||||
|
||||
## Localization (l10n)
|
||||
Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management.
|
||||
|
57
README.md
57
README.md
@ -1,27 +1,29 @@
|
||||
[![Misskey](https://github.com/misskey-dev/assets/blob/main/banner.png?raw=true)](https://join.misskey.page/)
|
||||
|
||||
<div align="center">
|
||||
|
||||
**🌎 A forever evolving, interplanetary microblogging platform. 🚀**
|
||||
|
||||
**Misskey** is a distributed microblogging platform with advanced features such as Reactions and a highly customizable UI.
|
||||
|
||||
[Learn more](https://misskey-hub.net/)
|
||||
|
||||
<a href="https://misskey-hub.net">
|
||||
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/>
|
||||
</a>
|
||||
|
||||
**🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀**
|
||||
|
||||
---
|
||||
|
||||
[✨ Find an instance](https://misskey-hub.net/instances.html)
|
||||
•
|
||||
[📦 Create your own instance](https://misskey-hub.net/docs/install.html)
|
||||
•
|
||||
[🛠️ Contribute](./CONTRIBUTING.md)
|
||||
•
|
||||
[🚀 Join the community](https://discord.gg/Wp8gVStHW3)
|
||||
<a href="https://misskey-hub.net/instances.html">
|
||||
<img src="https://custom-icon-badges.herokuapp.com/badge/find_an-instance-acea31?logoColor=acea31&style=for-the-badge&logo=misskey&labelColor=363B40" alt="find an instance"/></a>
|
||||
|
||||
<a href="https://misskey-hub.net/docs/install.html">
|
||||
<img src="https://custom-icon-badges.herokuapp.com/badge/create_an-instance-FBD53C?logoColor=FBD53C&style=for-the-badge&logo=server&labelColor=363B40" alt="create an instance"/></a>
|
||||
|
||||
<a href="./CONTRIBUTING.md">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<a href="https://www.patreon.com/syuilo">
|
||||
<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>
|
||||
|
||||
---
|
||||
|
||||
<a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -30,22 +32,25 @@
|
||||
|
||||
## ✨ Features
|
||||
- **ActivityPub support**\
|
||||
It is possible to interact with other software.
|
||||
Not on Misskey? No problem! Not only can Misskey instances talk to each other, but you can make friends with people on other networks like Mastodon and Pixelfed!
|
||||
- **Reactions**\
|
||||
You can add "reactions" to each post, making it easy for you to express your feelings.
|
||||
You can add emoji reactions to any post! No longer are you bound by a like button, show everyone exactly how you feel with the tap of a button.
|
||||
- **Drive**\
|
||||
An interface to manage uploaded files such as images, videos, sounds, etc.
|
||||
You can also organize your favorite content into folders, making it easy to share again.
|
||||
With Misskey's built in drive, you get cloud storage right in your social media, where you can upload any files, make folders, and find media from posts you've made!
|
||||
- **Rich Web UI**\
|
||||
Misskey has a rich WebUI by default.
|
||||
It is highly customizable by flexibly changing the layout and installing various widgets and themes.
|
||||
Furthermore, plug-ins can be created using AiScript, a original programming language.
|
||||
- and more...
|
||||
Misskey has a rich and easy to use Web UI!
|
||||
It is highly customizable, from changing the layout and adding widgets to making custom themes.
|
||||
Furthermore, plugins can be created using AiScript, an original programming language.
|
||||
- And much more...
|
||||
|
||||
</div>
|
||||
|
||||
<div style="clear: both;"></div>
|
||||
|
||||
## Documentation
|
||||
|
||||
Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/), some of the links and graphics above also lead to specific portions of it.
|
||||
|
||||
## Sponsors
|
||||
<div align="center">
|
||||
<a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a>
|
||||
|
67
assets/title_float.svg
Normal file
67
assets/title_float.svg
Normal file
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg10"
|
||||
version="1.1"
|
||||
viewBox="0 0 162.642 54.261"
|
||||
height="205.08"
|
||||
width="614.71">
|
||||
<metadata
|
||||
id="metadata16">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<style>
|
||||
#g8 {
|
||||
animation-name: floating;
|
||||
animation-duration: 3s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes floating {
|
||||
0% { transform: translate(0, 0px); }
|
||||
50% { transform: translate(0, -5px); }
|
||||
100% { transform: translate(0, 0px); }
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="myGradient" gradientTransform="rotate(90)">
|
||||
<stop offset="5%" stop-color="#A1CA03" />
|
||||
<stop offset="95%" stop-color="#91BA03" />
|
||||
</linearGradient>
|
||||
<defs
|
||||
id="defs14" />
|
||||
<g
|
||||
id="g8"
|
||||
fill="url('#myGradient')"
|
||||
word-spacing="0"
|
||||
letter-spacing="0"
|
||||
font-family="OTADESIGN Rounded"
|
||||
font-weight="400">
|
||||
<g
|
||||
id="g4"
|
||||
style="line-height:476.69509888px;-inkscape-font-specification:'OTADESIGN Rounded'">
|
||||
<path
|
||||
id="path2"
|
||||
font-size="141.034"
|
||||
aria-label="Mi"
|
||||
d="m 27.595,34.59 c -1.676,0.006 -3.115,-1.004 -3.793,-2.179 -0.363,-0.513 -1.08,-0.696 -1.09,0 v 3.214 c 0,1.291 -0.47,2.408 -1.412,3.35 -0.915,0.914 -2.031,1.371 -3.35,1.371 -1.29,0 -2.407,-0.457 -3.349,-1.372 -0.914,-0.941 -1.372,-2.058 -1.372,-3.349 V 17.95 c 0,-0.995 0.283,-1.896 0.848,-2.703 0.591,-0.834 1.345,-1.413 2.26,-1.735 0.516591,-0.189385 1.062793,-0.285215 1.613,-0.283 1.453,0 2.664,0.565 3.632,1.695 l 4.832,5.608 c 0.108,0.08 0.424,0.697 1.18,0.697 0.758,0 1.115,-0.617 1.222,-0.698 l 4.791,-5.607 c 0.996,-1.13 2.22,-1.695 3.673,-1.695 0.538,0 1.076,0.094 1.614,0.283 0.914,0.322 1.654,0.9 2.22,1.735 0.591,0.807 0.887,1.708 0.887,2.703 v 17.675 c 0,1.291 -0.47,2.408 -1.412,3.35 -0.915,0.914 -2.032,1.371 -3.35,1.371 -1.291,0 -2.407,-0.457 -3.35,-1.372 -0.914,-0.941 -1.371,-2.058 -1.371,-3.349 v -3.214 c -0.08,-0.877 -0.855,-0.324 -1.13,0 -0.726,1.345 -2.118,2.173 -3.793,2.18 z M 47.806,21.38 c -1.13,0 -2.098333,-0.39 -2.905,-1.17 -0.78,-0.806667 -1.17,-1.775 -1.17,-2.905 0,-1.13 0.39,-2.085 1.17,-2.865 0.806667,-0.806667 1.775,-1.21 2.905,-1.21 1.13,0 2.098667,0.403333 2.906,1.21 0.806667,0.78 1.21,1.735 1.21,2.865 0,1.13 -0.403333,2.098333 -1.21,2.905 -0.807333,0.78 -1.776,1.17 -2.906,1.17 z m 0.04,0.808 c 1.13,0 2.085333,0.403333 2.866,1.21 0.806667,0.806667 1.21,1.775333 1.21,2.906 v 9.967 c 0,1.13 -0.403333,2.098333 -1.21,2.905 -0.78,0.78 -1.735333,1.17 -2.866,1.17 -1.129333,0 -2.097667,-0.39 -2.905,-1.17 -0.806667,-0.806667 -1.21,-1.775 -1.21,-2.905 v -9.967 c 0,-1.13 0.403333,-2.098667 1.21,-2.906 0.806667,-0.806667 1.775,-1.21 2.905,-1.21 z"
|
||||
style="font-size:141.03399658px;-inkscape-font-specification:'OTADESIGN Rounded'" />
|
||||
</g>
|
||||
<path
|
||||
id="path6"
|
||||
d="M60.925 27.24q.968.243 2.42.525 2.42.403 3.792 1.29 2.582 1.695 2.582 5.083 0 2.743-1.815 4.478-2.098 2.017-5.85 2.017-2.742 0-6.13-.767-1.09-.242-1.776-1.089-.645-.847-.645-1.896 0-1.29.887-2.178.928-.928 2.179-.928.363 0 .685.081 1.17.242 4.478.605.444 0 .968-.04.202 0 .202-.242.04-.202-.242-.283-1.372-.242-2.542-.524-1.33-.282-1.896-.484-1.129-.323-1.895-.847-2.582-1.694-2.622-5.083 0-2.702 1.855-4.477 2.26-2.179 6.414-1.977 2.783.121 5.567.726 1.048.242 1.734 1.09.686.846.686 1.936 0 1.25-.928 2.178-.887.887-2.178.887-.323 0-.645-.08-1.17-.242-4.518-.565-.404-.04-.767 0-.323.04-.323.242.04.242.323.323zm17.555 0q.968.243 2.42.525 2.42.403 3.792 1.29 2.581 1.695 2.581 5.083 0 2.743-1.815 4.478-2.098 2.017-5.849 2.017-2.743 0-6.131-.767-1.09-.242-1.775-1.089-.646-.847-.646-1.896 0-1.29.888-2.178.927-.928 2.178-.928.363 0 .686.081 1.17.242 4.477.605.444 0 .968-.04.202 0 .202-.242.04-.202-.242-.283-1.371-.242-2.541-.524-1.331-.282-1.896-.484-1.13-.323-1.896-.847-2.582-1.694-2.622-5.083 0-2.702 1.855-4.477 2.26-2.179 6.414-1.977 2.784.121 5.567.726 1.049.242 1.735 1.09.685.846.685 1.936 0 1.25-.927 2.178-.888.887-2.179.887-.322 0-.645-.08-1.17-.242-4.518-.565-.403-.04-.767 0-.322.04-.322.242.04.242.322.323zm26.075 3.335q.12.08 2.864 2.783 1.25 1.21 1.25 2.945 0 1.613-1.17 2.864-1.17 1.21-2.904 1.21-1.654 0-2.864-1.17l-4.034-3.913q-.161-.12-.323-.12-.322 0-.322 1.21 0 1.694-1.21 2.904-1.21 1.17-2.905 1.17-1.694 0-2.904-1.17-1.17-1.21-1.17-2.905V17.586q0-1.694 1.17-2.864 1.21-1.21 2.904-1.21t2.904 1.21q1.21 1.17 1.21 2.864v6.293q0 .403.283.524.242.121.524-.08.162-.081 4.841-3.188 1.049-.645 2.259-.645 2.219 0 3.429 1.815.645 1.05.645 2.26 0 2.218-1.815 3.428l-2.541 1.614v.04l-.081.04q-.565.363-.04.888zm15.599 10.058q-4.195 0-7.18-2.945-2.945-2.985-2.945-7.18 0-4.155 2.945-7.1 2.985-2.985 7.18-2.985 4.155 0 6.979 2.784.928.927.928 2.259 0 1.33-.928 2.259l-4.68 4.639q-1.008 1.008-2.016 1.008-1.453 0-2.26-.807-.806-.807-.806-2.138 0-1.29.928-2.218l.806-.847q.162-.121.081-.243-.12-.08-.323-.04-.806.202-1.371.807-1.13 1.09-1.13 2.622 0 1.573 1.09 2.703 1.13 1.089 2.702 1.089 1.533 0 2.622-1.13.928-.927 2.26-.927 1.33 0 2.258.927.928.928.928 2.26 0 1.33-.928 2.258-2.985 2.945-7.14 2.945zm29.259-15.786v5.607q0 .564-.08 1.21v7.382q0 4.518-2.744 7.22-2.702 2.703-7.301 2.703-2.662 0-4.8-1.008-2.138-.968-2.138-3.348 0-.807.363-1.533.968-2.179 3.348-2.179.565 0 1.573.323 1.009.323 1.654.323 1.694 0 2.219-.726.201-.283.08-.444-.161-.242-.564-.161-.686.12-1.493.12-4.074 0-6.979-2.904-2.904-2.904-2.904-6.978v-5.607q0-1.695 1.17-2.864 1.21-1.21 2.904-1.21t2.905 1.21q1.21 1.17 1.21 2.864v5.607q0 .685.484 1.21.524.484 1.21.484.726 0 1.21-.484.484-.525.484-1.21v-5.607q0-1.695 1.21-2.864 1.21-1.21 2.905-1.21 1.694 0 2.864 1.21 1.21 1.17 1.21 2.864z"
|
||||
style="line-height:136.34428406px;-inkscape-font-specification:'OTADESIGN Rounded'" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.1 KiB |
@ -1,11 +1,6 @@
|
||||
describe('Before setup instance', () => {
|
||||
beforeEach(() => {
|
||||
cy.window(win => {
|
||||
win.indexedDB.deleteDatabase('keyval-store');
|
||||
});
|
||||
cy.request('POST', '/api/reset-db').as('reset');
|
||||
cy.get('@reset').its('status').should('equal', 204);
|
||||
cy.reload(true);
|
||||
cy.resetState();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -35,18 +30,10 @@ describe('Before setup instance', () => {
|
||||
|
||||
describe('After setup instance', () => {
|
||||
beforeEach(() => {
|
||||
cy.window(win => {
|
||||
win.indexedDB.deleteDatabase('keyval-store');
|
||||
});
|
||||
cy.request('POST', '/api/reset-db').as('reset');
|
||||
cy.get('@reset').its('status').should('equal', 204);
|
||||
cy.reload(true);
|
||||
cy.resetState();
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.request('POST', '/api/admin/accounts/create', {
|
||||
username: 'admin',
|
||||
password: 'pass',
|
||||
}).its('body').as('admin');
|
||||
cy.registerUser('admin', 'pass', true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -76,24 +63,13 @@ describe('After setup instance', () => {
|
||||
|
||||
describe('After user signup', () => {
|
||||
beforeEach(() => {
|
||||
cy.window(win => {
|
||||
win.indexedDB.deleteDatabase('keyval-store');
|
||||
});
|
||||
cy.request('POST', '/api/reset-db').as('reset');
|
||||
cy.get('@reset').its('status').should('equal', 204);
|
||||
cy.reload(true);
|
||||
cy.resetState();
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.request('POST', '/api/admin/accounts/create', {
|
||||
username: 'admin',
|
||||
password: 'pass',
|
||||
}).its('body').as('admin');
|
||||
cy.registerUser('admin', 'pass', true);
|
||||
|
||||
// ユーザー作成
|
||||
cy.request('POST', '/api/signup', {
|
||||
username: 'alice',
|
||||
password: 'alice1234',
|
||||
}).its('body').as('alice');
|
||||
cy.registerUser('alice', 'alice1234');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -138,34 +114,15 @@ describe('After user signup', () => {
|
||||
|
||||
describe('After user singed in', () => {
|
||||
beforeEach(() => {
|
||||
cy.window(win => {
|
||||
win.indexedDB.deleteDatabase('keyval-store');
|
||||
});
|
||||
cy.request('POST', '/api/reset-db').as('reset');
|
||||
cy.get('@reset').its('status').should('equal', 204);
|
||||
cy.reload(true);
|
||||
cy.resetState();
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.request('POST', '/api/admin/accounts/create', {
|
||||
username: 'admin',
|
||||
password: 'pass',
|
||||
}).its('body').as('admin');
|
||||
cy.registerUser('admin', 'pass', true);
|
||||
|
||||
// ユーザー作成
|
||||
cy.request('POST', '/api/signup', {
|
||||
username: 'alice',
|
||||
password: 'alice1234',
|
||||
}).its('body').as('alice');
|
||||
cy.registerUser('alice', 'alice1234');
|
||||
|
||||
cy.visit('/');
|
||||
|
||||
cy.intercept('POST', '/api/signin').as('signin');
|
||||
|
||||
cy.get('[data-cy-signin]').click();
|
||||
cy.get('[data-cy-signin-username] input').type('alice');
|
||||
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
|
||||
|
||||
cy.wait('@signin').as('signedIn');
|
||||
cy.login('alice', 'alice1234');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
65
cypress/integration/widgets.js
Normal file
65
cypress/integration/widgets.js
Normal file
@ -0,0 +1,65 @@
|
||||
describe('After user signed in', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
cy.viewport('macbook-16');
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser('admin', 'pass', true);
|
||||
|
||||
// ユーザー作成
|
||||
cy.registerUser('alice', 'alice1234');
|
||||
|
||||
cy.login('alice', 'alice1234');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||
// waitを入れることでそれを防止できる
|
||||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it('widget edit toggle is visible', () => {
|
||||
cy.get('.mk-widget-edit').should('be.visible');
|
||||
});
|
||||
|
||||
it('widget select should be visible in edit mode', () => {
|
||||
cy.get('.mk-widget-edit').click();
|
||||
cy.get('.mk-widget-select').should('be.visible');
|
||||
});
|
||||
|
||||
it('first widget should be removed', () => {
|
||||
cy.get('.mk-widget-edit').click();
|
||||
cy.get('.customize-container:first-child .remove._button').click();
|
||||
cy.get('.customize-container').should('have.length', 2);
|
||||
});
|
||||
|
||||
function buildWidgetTest(widgetName) {
|
||||
it(`${widgetName} widget should get added`, () => {
|
||||
cy.get('.mk-widget-edit').click();
|
||||
cy.get('.mk-widget-select select').select(widgetName, { force: true });
|
||||
cy.get('.bg._modalBg.transparent').click({ multiple: true, force: true });
|
||||
cy.get('.mk-widget-add').click({ force: true });
|
||||
cy.get(`.mkw-${widgetName}`).should('exist');
|
||||
});
|
||||
}
|
||||
|
||||
buildWidgetTest('memo');
|
||||
buildWidgetTest('notifications');
|
||||
buildWidgetTest('timeline');
|
||||
buildWidgetTest('calendar');
|
||||
buildWidgetTest('rss');
|
||||
buildWidgetTest('trends');
|
||||
buildWidgetTest('clock');
|
||||
buildWidgetTest('activity');
|
||||
buildWidgetTest('photos');
|
||||
buildWidgetTest('digitalClock');
|
||||
buildWidgetTest('federation');
|
||||
buildWidgetTest('postForm');
|
||||
buildWidgetTest('slideshow');
|
||||
buildWidgetTest('serverMetric');
|
||||
buildWidgetTest('onlineUsers');
|
||||
buildWidgetTest('jobQueue');
|
||||
buildWidgetTest('button');
|
||||
buildWidgetTest('aiscript');
|
||||
buildWidgetTest('aichan');
|
||||
});
|
@ -23,3 +23,33 @@
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
|
||||
Cypress.Commands.add('resetState', () => {
|
||||
cy.window(win => {
|
||||
win.indexedDB.deleteDatabase('keyval-store');
|
||||
});
|
||||
cy.request('POST', '/api/reset-db').as('reset');
|
||||
cy.get('@reset').its('status').should('equal', 204);
|
||||
cy.reload(true);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
|
||||
const route = isAdmin ? '/api/admin/accounts/create' : '/api/signup';
|
||||
|
||||
cy.request('POST', route, {
|
||||
username: username,
|
||||
password: password,
|
||||
}).its('body').as(username);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('login', (username, password) => {
|
||||
cy.visit('/');
|
||||
|
||||
cy.intercept('POST', '/api/signin').as('signin');
|
||||
|
||||
cy.get('[data-cy-signin]').click();
|
||||
cy.get('[data-cy-signin-username] input').type(username);
|
||||
cy.get('[data-cy-signin-password] input').type(`${password}{enter}`);
|
||||
|
||||
cy.wait('@signin').as('signedIn');
|
||||
});
|
||||
|
@ -425,7 +425,7 @@ quoteQuestion: "引用として添付しますか?"
|
||||
noMessagesYet: "まだチャットはありません"
|
||||
newMessageExists: "新しいメッセージがあります"
|
||||
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
|
||||
signinRequired: "ログインしてください"
|
||||
signinRequired: "続行する前に、サインアップまたはサインインが必要です"
|
||||
invitations: "招待"
|
||||
invitationCode: "招待コード"
|
||||
checking: "確認しています"
|
||||
@ -842,6 +842,7 @@ oneDay: "1日"
|
||||
oneWeek: "1週間"
|
||||
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
|
||||
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
|
||||
rateLimitExceeded: "レート制限を超えました"
|
||||
|
||||
_emailUnavailable:
|
||||
used: "既に使用されています"
|
||||
@ -1110,7 +1111,6 @@ _sfx:
|
||||
channel: "チャンネル通知"
|
||||
|
||||
_ago:
|
||||
unknown: "謎"
|
||||
future: "未来"
|
||||
justNow: "たった今"
|
||||
secondsAgo: "{n}秒前"
|
||||
|
@ -5,6 +5,6 @@
|
||||
"loader=./test/loader.js"
|
||||
],
|
||||
"slow": 1000,
|
||||
"timeout": 35000,
|
||||
"timeout": 10000,
|
||||
"exit": true
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
export class uniformThemecolor1652859567549 {
|
||||
name = 'uniformThemecolor1652859567549'
|
||||
|
||||
async up(queryRunner) {
|
||||
const formatColor = (color) => {
|
||||
let tc = new tinycolor(color);
|
||||
if (tc.isValid()) {
|
||||
return tc.toHexString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
await queryRunner.query('SELECT "id", "themeColor" FROM "instance" WHERE "themeColor" IS NOT NULL')
|
||||
.then(instances => Promise.all(instances.map(instance => {
|
||||
// update theme color to uniform format, e.g. #00ff00
|
||||
// invalid theme colors get set to null
|
||||
return queryRunner.query('UPDATE "instance" SET "themeColor" = $1 WHERE "id" = $2', [formatColor(instance.themeColor), instance.id]);
|
||||
})));
|
||||
|
||||
// also fix own theme color
|
||||
await queryRunner.query('SELECT "themeColor" FROM "meta" WHERE "themeColor" IS NOT NULL LIMIT 1')
|
||||
.then(metas => {
|
||||
if (metas.length > 0) {
|
||||
return queryRunner.query('UPDATE "meta" SET "themeColor" = $1', [formatColor(metas[0].themeColor)]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
// The original representation is not stored, so migrating back is not possible.
|
||||
// The new format also works in older versions so this is not a problem.
|
||||
}
|
||||
}
|
@ -15,11 +15,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/koa": "3.10.4",
|
||||
"@discordapp/twemoji": "13.1.1",
|
||||
"@discordapp/twemoji": "14.0.2",
|
||||
"@elastic/elasticsearch": "7.11.0",
|
||||
"@koa/cors": "3.1.0",
|
||||
"@koa/multer": "3.0.0",
|
||||
"@koa/router": "9.0.1",
|
||||
"@peertube/http-signature": "1.6.0",
|
||||
"@sinonjs/fake-timers": "9.1.2",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"abort-controller": "3.0.0",
|
||||
@ -30,7 +31,7 @@
|
||||
"aws-sdk": "2.1135.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "1.1.5",
|
||||
"broadcast-channel": "4.11.0",
|
||||
"broadcast-channel": "4.12.0",
|
||||
"bull": "4.8.3",
|
||||
"cacheable-lookup": "6.0.4",
|
||||
"cbor": "8.1.0",
|
||||
@ -47,15 +48,14 @@
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"got": "12.0.4",
|
||||
"hpagent": "0.1.2",
|
||||
"http-signature": "1.3.6",
|
||||
"ip-cidr": "3.0.7",
|
||||
"ip-cidr": "3.0.8",
|
||||
"is-svg": "4.3.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "19.0.0",
|
||||
"json5": "2.2.1",
|
||||
"json5-loader": "4.0.1",
|
||||
"jsonld": "5.2.0",
|
||||
"jsrsasign": "10.5.20",
|
||||
"jsrsasign": "10.5.22",
|
||||
"koa": "2.13.4",
|
||||
"koa-bodyparser": "4.3.0",
|
||||
"koa-favicon": "2.1.0",
|
||||
@ -65,10 +65,10 @@
|
||||
"koa-send": "5.0.1",
|
||||
"koa-slow": "2.1.0",
|
||||
"koa-views": "7.0.2",
|
||||
"mfm-js": "0.21.0",
|
||||
"mfm-js": "0.22.1",
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "0.0.14",
|
||||
"mocha": "9.2.2",
|
||||
"mocha": "10.0.0",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"multer": "1.4.4",
|
||||
"nested-property": "4.0.0",
|
||||
@ -77,7 +77,6 @@
|
||||
"os-utils": "0.0.14",
|
||||
"parse5": "6.0.1",
|
||||
"pg": "8.7.3",
|
||||
"portscanner": "2.2.0",
|
||||
"private-ip": "2.3.3",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
@ -102,15 +101,15 @@
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"style-loader": "3.3.1",
|
||||
"summaly": "2.5.0",
|
||||
"summaly": "2.5.1",
|
||||
"syslog-pro": "1.0.0",
|
||||
"systeminformation": "5.11.15",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tmp": "0.2.1",
|
||||
"ts-loader": "9.3.0",
|
||||
"ts-node": "10.7.0",
|
||||
"tsc-alias": "1.4.1",
|
||||
"tsconfig-paths": "3.14.1",
|
||||
"ts-node": "10.8.0",
|
||||
"tsc-alias": "1.6.7",
|
||||
"tsconfig-paths": "4.0.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.6",
|
||||
"ulid": "2.3.0",
|
||||
@ -146,12 +145,11 @@
|
||||
"@types/koa__multer": "2.0.4",
|
||||
"@types/koa__router": "8.0.11",
|
||||
"@types/mocha": "9.1.1",
|
||||
"@types/node": "17.0.33",
|
||||
"@types/node": "17.0.35",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.4",
|
||||
"@types/oauth": "0.9.1",
|
||||
"@types/parse5": "6.0.3",
|
||||
"@types/portscanner": "2.1.1",
|
||||
"@types/pug": "2.0.6",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/qrcode": "1.4.2",
|
||||
@ -169,10 +167,10 @@
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.23.0",
|
||||
"@typescript-eslint/parser": "5.23.0",
|
||||
"typescript": "4.6.4",
|
||||
"eslint": "8.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.26.0",
|
||||
"@typescript-eslint/parser": "5.26.0",
|
||||
"typescript": "4.7.2",
|
||||
"eslint": "8.16.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
|
||||
"cross-env": "7.0.3",
|
||||
|
@ -1,4 +1,4 @@
|
||||
declare module 'http-signature' {
|
||||
declare module '@peertube/http-signature' {
|
||||
import { IncomingMessage, ClientRequest } from 'node:http';
|
||||
|
||||
interface ISignature {
|
||||
|
@ -5,7 +5,6 @@ import * as os from 'node:os';
|
||||
import cluster from 'node:cluster';
|
||||
import chalk from 'chalk';
|
||||
import chalkTemplate from 'chalk-template';
|
||||
import * as portscanner from 'portscanner';
|
||||
import semver from 'semver';
|
||||
|
||||
import Logger from '@/services/logger.js';
|
||||
@ -48,11 +47,6 @@ function greet() {
|
||||
bootLogger.info(`Misskey v${meta.version}`, null, true);
|
||||
}
|
||||
|
||||
function isRoot() {
|
||||
// maybe process.getuid will be undefined under not POSIX environment (e.g. Windows)
|
||||
return process.getuid != null && process.getuid() === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init master process
|
||||
*/
|
||||
@ -67,7 +61,6 @@ export async function masterMain() {
|
||||
showNodejsVersion();
|
||||
config = loadConfigBoot();
|
||||
await connectDb();
|
||||
await validatePort(config);
|
||||
} catch (e) {
|
||||
bootLogger.error('Fatal error occurred during initialization', null, true);
|
||||
process.exit(1);
|
||||
@ -97,8 +90,6 @@ function showEnvironment(): void {
|
||||
logger.warn('The environment is not in production mode.');
|
||||
logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true);
|
||||
}
|
||||
|
||||
logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
|
||||
}
|
||||
|
||||
function showNodejsVersion(): void {
|
||||
@ -152,29 +143,6 @@ async function connectDb(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function validatePort(config: Config): Promise<void> {
|
||||
const isWellKnownPort = (port: number) => port < 1024;
|
||||
|
||||
async function isPortAvailable(port: number): Promise<boolean> {
|
||||
return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed';
|
||||
}
|
||||
|
||||
if (config.port == null || Number.isNaN(config.port)) {
|
||||
bootLogger.error('The port is not configured. Please configure port.', null, true);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
|
||||
bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!await isPortAvailable(config.port)) {
|
||||
bootLogger.error(`Port ${config.port} is already in use`, null, true);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function spawnWorkers(limit: number = 1) {
|
||||
const workers = Math.min(limit, os.cpus().length);
|
||||
bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
|
||||
@ -186,6 +154,10 @@ function spawnWorker(): Promise<void> {
|
||||
return new Promise(res => {
|
||||
const worker = cluster.fork();
|
||||
worker.on('message', message => {
|
||||
if (message === 'listenFailed') {
|
||||
bootLogger.error(`The server Listen failed due to the previous error.`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (message !== 'ready') return;
|
||||
res();
|
||||
});
|
||||
|
@ -46,7 +46,7 @@ export default function load() {
|
||||
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
|
||||
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
|
||||
mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
|
||||
mixin.clientEntry = clientManifest['src/init.ts'].file.replace(/^_client_dist_\//, '');
|
||||
mixin.clientEntry = clientManifest['src/init.ts'];
|
||||
|
||||
if (!config.redis.prefix) config.redis.prefix = mixin.host;
|
||||
|
||||
|
@ -5,9 +5,6 @@ pg.types.setTypeParser(20, Number);
|
||||
import { Logger, DataSource } from 'typeorm';
|
||||
import * as highlight from 'cli-highlight';
|
||||
import config from '@/config/index.js';
|
||||
import { envOption } from '../env.js';
|
||||
|
||||
import { dbLogger } from './logger.js';
|
||||
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
@ -74,6 +71,9 @@ import { UserPending } from '@/models/entities/user-pending.js';
|
||||
|
||||
import { entities as charts } from '@/services/chart/entities.js';
|
||||
import { Webhook } from '@/models/entities/webhook.js';
|
||||
import { envOption } from '../env.js';
|
||||
import { dbLogger } from './logger.js';
|
||||
import { redisClient } from './redis.js';
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
|
||||
|
||||
@ -208,16 +208,25 @@ export const db = new DataSource({
|
||||
migrations: ['../../migration/*.js'],
|
||||
});
|
||||
|
||||
export async function initDb() {
|
||||
export async function initDb(force = false) {
|
||||
if (force) {
|
||||
if (db.isInitialized) {
|
||||
await db.destroy();
|
||||
}
|
||||
await db.initialize();
|
||||
return;
|
||||
}
|
||||
|
||||
if (db.isInitialized) {
|
||||
// nop
|
||||
} else {
|
||||
await db.connect();
|
||||
await db.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetDb() {
|
||||
const reset = async () => {
|
||||
await redisClient.FLUSHDB();
|
||||
const tables = await db.query(`SELECT relname AS "table"
|
||||
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
|
||||
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
|
||||
|
@ -6,6 +6,9 @@ const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
|
||||
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
|
||||
|
||||
export function fromHtml(html: string, hashtagNames?: string[]): string {
|
||||
// some AP servers like Pixelfed use br tags as well as newlines
|
||||
html = html.replace(/<br\s?\/?>\r?\n/gi, '\n');
|
||||
|
||||
const dom = parse5.parseFragment(html);
|
||||
|
||||
let text = '';
|
||||
|
@ -1,10 +1,19 @@
|
||||
import * as tmp from 'tmp';
|
||||
|
||||
export function createTemp(): Promise<[string, any]> {
|
||||
return new Promise<[string, any]>((res, rej) => {
|
||||
export function createTemp(): Promise<[string, () => void]> {
|
||||
return new Promise<[string, () => void]>((res, rej) => {
|
||||
tmp.file((e, path, fd, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function createTempDir(): Promise<[string, () => void]> {
|
||||
return new Promise<[string, () => void]>((res, rej) => {
|
||||
tmp.dir((e, path, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
9
packages/backend/src/misc/get-ip-hash.ts
Normal file
9
packages/backend/src/misc/get-ip-hash.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import IPCIDR from 'ip-cidr';
|
||||
|
||||
export function getIpHash(ip: string) {
|
||||
// because a single person may control many IPv6 addresses,
|
||||
// only a /64 subnet prefix of any IP will be taken into account.
|
||||
// (this means for IPv4 the entire address is used)
|
||||
const prefix = IPCIDR.createAddress(ip).mask(64);
|
||||
return 'ip-' + BigInt('0b' + prefix).toString(36);
|
||||
}
|
@ -29,7 +29,9 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
||||
|
||||
getPublicProperties(file: DriveFile): DriveFile['properties'] {
|
||||
if (file.properties.orientation != null) {
|
||||
const properties = structuredClone(file.properties);
|
||||
// TODO
|
||||
//const properties = structuredClone(file.properties);
|
||||
const properties = JSON.parse(JSON.stringify(file.properties));
|
||||
if (file.properties.orientation >= 5) {
|
||||
[properties.width, properties.height] = [properties.height, properties.width];
|
||||
}
|
||||
|
@ -144,13 +144,7 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||
return true;
|
||||
} else {
|
||||
// 指定されているかどうか
|
||||
const specified = note.visibleUserIds.some((id: any) => meId === id);
|
||||
|
||||
if (specified) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return note.visibleUserIds.some((id: any) => meId === id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,16 +162,25 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||
return true;
|
||||
} else {
|
||||
// フォロワーかどうか
|
||||
const following = await Followings.findOneBy({
|
||||
followeeId: note.userId,
|
||||
followerId: meId,
|
||||
});
|
||||
const [following, user] = await Promise.all([
|
||||
Followings.count({
|
||||
where: {
|
||||
followeeId: note.userId,
|
||||
followerId: meId,
|
||||
},
|
||||
take: 1,
|
||||
}),
|
||||
Users.findOneByOrFail({ id: meId }),
|
||||
]);
|
||||
|
||||
if (following == null) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
/* If we know the following, everyhting is fine.
|
||||
|
||||
But if we do not know the following, it might be that both the
|
||||
author of the note and the author of the like are remote users,
|
||||
in which case we can never know the following. Instead we have
|
||||
to assume that the users are following each other.
|
||||
*/
|
||||
return following > 0 || (note.userHost != null && user.host != null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,47 +61,58 @@ export const UserRepository = db.getRepository(User).extend({
|
||||
//#endregion
|
||||
|
||||
async getRelation(me: User['id'], target: User['id']) {
|
||||
const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
|
||||
Followings.findOneBy({
|
||||
followerId: me,
|
||||
followeeId: target,
|
||||
}),
|
||||
Followings.findOneBy({
|
||||
followerId: target,
|
||||
followeeId: me,
|
||||
}),
|
||||
FollowRequests.findOneBy({
|
||||
followerId: me,
|
||||
followeeId: target,
|
||||
}),
|
||||
FollowRequests.findOneBy({
|
||||
followerId: target,
|
||||
followeeId: me,
|
||||
}),
|
||||
Blockings.findOneBy({
|
||||
blockerId: me,
|
||||
blockeeId: target,
|
||||
}),
|
||||
Blockings.findOneBy({
|
||||
blockerId: target,
|
||||
blockeeId: me,
|
||||
}),
|
||||
Mutings.findOneBy({
|
||||
muterId: me,
|
||||
muteeId: target,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
return awaitAll({
|
||||
id: target,
|
||||
isFollowing: following1 != null,
|
||||
hasPendingFollowRequestFromYou: followReq1 != null,
|
||||
hasPendingFollowRequestToYou: followReq2 != null,
|
||||
isFollowed: following2 != null,
|
||||
isBlocking: toBlocking != null,
|
||||
isBlocked: fromBlocked != null,
|
||||
isMuted: mute != null,
|
||||
};
|
||||
isFollowing: Followings.count({
|
||||
where: {
|
||||
followerId: me,
|
||||
followeeId: target,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
isFollowed: Followings.count({
|
||||
where: {
|
||||
followerId: target,
|
||||
followeeId: me,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
hasPendingFollowRequestFromYou: FollowRequests.count({
|
||||
where: {
|
||||
followerId: me,
|
||||
followeeId: target,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
hasPendingFollowRequestToYou: FollowRequests.count({
|
||||
where: {
|
||||
followerId: target,
|
||||
followeeId: me,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
isBlocking: Blockings.count({
|
||||
where: {
|
||||
blockerId: me,
|
||||
blockeeId: target,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
isBlocked: Blockings.count({
|
||||
where: {
|
||||
blockerId: target,
|
||||
blockeeId: me,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
isMuted: Mutings.count({
|
||||
where: {
|
||||
muterId: me,
|
||||
muteeId: target,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
});
|
||||
},
|
||||
|
||||
async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import httpSignature from 'http-signature';
|
||||
import httpSignature from '@peertube/http-signature';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import config from '@/config/index.js';
|
||||
@ -305,11 +305,13 @@ export default function() {
|
||||
systemQueue.add('resyncCharts', {
|
||||
}, {
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
systemQueue.add('cleanCharts', {
|
||||
}, {
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
systemQueue.add('checkExpiredMutings', {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import Bull from 'bull';
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import { queueLogger } from '../../logger.js';
|
||||
import { addFile } from '@/services/drive/add-file.js';
|
||||
import { format as dateFormat } from 'date-fns';
|
||||
import { getFullApAccount } from '@/misc/convert-host.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { Users, Blockings } from '@/models/index.js';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { DbUserJobData } from '@/queue/types.js';
|
||||
@ -22,73 +22,72 @@ export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): P
|
||||
}
|
||||
|
||||
// Create temp file
|
||||
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
|
||||
tmp.file((e, path, fd, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
});
|
||||
});
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
||||
logger.info(`Temp file is ${path}`);
|
||||
|
||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
try {
|
||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
|
||||
let exportedCount = 0;
|
||||
let cursor: any = null;
|
||||
let exportedCount = 0;
|
||||
let cursor: any = null;
|
||||
|
||||
while (true) {
|
||||
const blockings = await Blockings.find({
|
||||
where: {
|
||||
blockerId: user.id,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
});
|
||||
while (true) {
|
||||
const blockings = await Blockings.find({
|
||||
where: {
|
||||
blockerId: user.id,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (blockings.length === 0) {
|
||||
job.progress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = blockings[blockings.length - 1].id;
|
||||
|
||||
for (const block of blockings) {
|
||||
const u = await Users.findOneBy({ id: block.blockeeId });
|
||||
if (u == null) {
|
||||
exportedCount++; continue;
|
||||
if (blockings.length === 0) {
|
||||
job.progress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
const content = getFullApAccount(u.username, u.host);
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
rej(err);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
cursor = blockings[blockings.length - 1].id;
|
||||
|
||||
for (const block of blockings) {
|
||||
const u = await Users.findOneBy({ id: block.blockeeId });
|
||||
if (u == null) {
|
||||
exportedCount++; continue;
|
||||
}
|
||||
|
||||
const content = getFullApAccount(u.username, u.host);
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
rej(err);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
});
|
||||
});
|
||||
exportedCount++;
|
||||
}
|
||||
|
||||
const total = await Blockings.countBy({
|
||||
blockerId: user.id,
|
||||
});
|
||||
exportedCount++;
|
||||
|
||||
job.progress(exportedCount / total);
|
||||
}
|
||||
|
||||
const total = await Blockings.countBy({
|
||||
blockerId: user.id,
|
||||
});
|
||||
stream.end();
|
||||
logger.succ(`Exported to: ${path}`);
|
||||
|
||||
job.progress(exportedCount / total);
|
||||
const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
|
||||
const driveFile = await addFile({ user, path, name: fileName, force: true });
|
||||
|
||||
logger.succ(`Exported to: ${driveFile.id}`);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
stream.end();
|
||||
logger.succ(`Exported to: ${path}`);
|
||||
|
||||
const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
|
||||
const driveFile = await addFile({ user, path, name: fileName, force: true });
|
||||
|
||||
logger.succ(`Exported to: ${driveFile.id}`);
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import Bull from 'bull';
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import { ulid } from 'ulid';
|
||||
@ -10,6 +9,7 @@ import { addFile } from '@/services/drive/add-file.js';
|
||||
import { format as dateFormat } from 'date-fns';
|
||||
import { Users, Emojis } from '@/models/index.js';
|
||||
import { } from '@/queue/types.js';
|
||||
import { createTemp, createTempDir } from '@/misc/create-temp.js';
|
||||
import { downloadUrl } from '@/misc/download-url.js';
|
||||
import config from '@/config/index.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
@ -25,13 +25,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
|
||||
return;
|
||||
}
|
||||
|
||||
// Create temp dir
|
||||
const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => {
|
||||
tmp.dir((e, path, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
});
|
||||
});
|
||||
const [path, cleanup] = await createTempDir();
|
||||
|
||||
logger.info(`Temp dir is ${path}`);
|
||||
|
||||
@ -98,12 +92,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
|
||||
metaStream.end();
|
||||
|
||||
// Create archive
|
||||
const [archivePath, archiveCleanup] = await new Promise<[string, () => void]>((res, rej) => {
|
||||
tmp.file((e, path, fd, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
});
|
||||
});
|
||||
const [archivePath, archiveCleanup] = await createTemp();
|
||||
const archiveStream = fs.createWriteStream(archivePath);
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 0 },
|
||||
|
@ -1,11 +1,11 @@
|
||||
import Bull from 'bull';
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import { queueLogger } from '../../logger.js';
|
||||
import { addFile } from '@/services/drive/add-file.js';
|
||||
import { format as dateFormat } from 'date-fns';
|
||||
import { getFullApAccount } from '@/misc/convert-host.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { Users, Followings, Mutings } from '@/models/index.js';
|
||||
import { In, MoreThan, Not } from 'typeorm';
|
||||
import { DbUserJobData } from '@/queue/types.js';
|
||||
@ -23,73 +23,72 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () =>
|
||||
}
|
||||
|
||||
// Create temp file
|
||||
const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => {
|
||||
tmp.file((e, path, fd, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
});
|
||||
});
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
||||
logger.info(`Temp file is ${path}`);
|
||||
|
||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
try {
|
||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
|
||||
let cursor: Following['id'] | null = null;
|
||||
let cursor: Following['id'] | null = null;
|
||||
|
||||
const mutings = job.data.excludeMuting ? await Mutings.findBy({
|
||||
muterId: user.id,
|
||||
}) : [];
|
||||
const mutings = job.data.excludeMuting ? await Mutings.findBy({
|
||||
muterId: user.id,
|
||||
}) : [];
|
||||
|
||||
while (true) {
|
||||
const followings = await Followings.find({
|
||||
where: {
|
||||
followerId: user.id,
|
||||
...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}),
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
}) as Following[];
|
||||
while (true) {
|
||||
const followings = await Followings.find({
|
||||
where: {
|
||||
followerId: user.id,
|
||||
...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}),
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
}) as Following[];
|
||||
|
||||
if (followings.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = followings[followings.length - 1].id;
|
||||
|
||||
for (const following of followings) {
|
||||
const u = await Users.findOneBy({ id: following.followeeId });
|
||||
if (u == null) {
|
||||
continue;
|
||||
if (followings.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) {
|
||||
continue;
|
||||
}
|
||||
cursor = followings[followings.length - 1].id;
|
||||
|
||||
const content = getFullApAccount(u.username, u.host);
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
rej(err);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
for (const following of followings) {
|
||||
const u = await Users.findOneBy({ id: following.followeeId });
|
||||
if (u == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = getFullApAccount(u.username, u.host);
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
rej(err);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
stream.end();
|
||||
logger.succ(`Exported to: ${path}`);
|
||||
|
||||
const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
|
||||
const driveFile = await addFile({ user, path, name: fileName, force: true });
|
||||
|
||||
logger.succ(`Exported to: ${driveFile.id}`);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
stream.end();
|
||||
logger.succ(`Exported to: ${path}`);
|
||||
|
||||
const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
|
||||
const driveFile = await addFile({ user, path, name: fileName, force: true });
|
||||
|
||||
logger.succ(`Exported to: ${driveFile.id}`);
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import Bull from 'bull';
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import { queueLogger } from '../../logger.js';
|
||||
import { addFile } from '@/services/drive/add-file.js';
|
||||
import { format as dateFormat } from 'date-fns';
|
||||
import { getFullApAccount } from '@/misc/convert-host.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { Users, Mutings } from '@/models/index.js';
|
||||
import { IsNull, MoreThan } from 'typeorm';
|
||||
import { DbUserJobData } from '@/queue/types.js';
|
||||
@ -22,74 +22,73 @@ export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promi
|
||||
}
|
||||
|
||||
// Create temp file
|
||||
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
|
||||
tmp.file((e, path, fd, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
});
|
||||
});
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
||||
logger.info(`Temp file is ${path}`);
|
||||
|
||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
try {
|
||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
|
||||
let exportedCount = 0;
|
||||
let cursor: any = null;
|
||||
let exportedCount = 0;
|
||||
let cursor: any = null;
|
||||
|
||||
while (true) {
|
||||
const mutes = await Mutings.find({
|
||||
where: {
|
||||
muterId: user.id,
|
||||
expiresAt: IsNull(),
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
});
|
||||
while (true) {
|
||||
const mutes = await Mutings.find({
|
||||
where: {
|
||||
muterId: user.id,
|
||||
expiresAt: IsNull(),
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (mutes.length === 0) {
|
||||
job.progress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = mutes[mutes.length - 1].id;
|
||||
|
||||
for (const mute of mutes) {
|
||||
const u = await Users.findOneBy({ id: mute.muteeId });
|
||||
if (u == null) {
|
||||
exportedCount++; continue;
|
||||
if (mutes.length === 0) {
|
||||
job.progress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
const content = getFullApAccount(u.username, u.host);
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
rej(err);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
cursor = mutes[mutes.length - 1].id;
|
||||
|
||||
for (const mute of mutes) {
|
||||
const u = await Users.findOneBy({ id: mute.muteeId });
|
||||
if (u == null) {
|
||||
exportedCount++; continue;
|
||||
}
|
||||
|
||||
const content = getFullApAccount(u.username, u.host);
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
rej(err);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
});
|
||||
});
|
||||
exportedCount++;
|
||||
}
|
||||
|
||||
const total = await Mutings.countBy({
|
||||
muterId: user.id,
|
||||
});
|
||||
exportedCount++;
|
||||
|
||||
job.progress(exportedCount / total);
|
||||
}
|
||||
|
||||
const total = await Mutings.countBy({
|
||||
muterId: user.id,
|
||||
});
|
||||
stream.end();
|
||||
logger.succ(`Exported to: ${path}`);
|
||||
|
||||
job.progress(exportedCount / total);
|
||||
const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
|
||||
const driveFile = await addFile({ user, path, name: fileName, force: true });
|
||||
|
||||
logger.succ(`Exported to: ${driveFile.id}`);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
stream.end();
|
||||
logger.succ(`Exported to: ${path}`);
|
||||
|
||||
const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
|
||||
const driveFile = await addFile({ user, path, name: fileName, force: true });
|
||||
|
||||
logger.succ(`Exported to: ${driveFile.id}`);
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import Bull from 'bull';
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import { queueLogger } from '../../logger.js';
|
||||
@ -10,6 +9,7 @@ import { MoreThan } from 'typeorm';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { Poll } from '@/models/entities/poll.js';
|
||||
import { DbUserJobData } from '@/queue/types.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
|
||||
const logger = queueLogger.createSubLogger('export-notes');
|
||||
|
||||
@ -23,82 +23,81 @@ export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Prom
|
||||
}
|
||||
|
||||
// Create temp file
|
||||
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
|
||||
tmp.file((e, path, fd, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
});
|
||||
});
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
||||
logger.info(`Temp file is ${path}`);
|
||||
|
||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
try {
|
||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
|
||||
const write = (text: string): Promise<void> => {
|
||||
return new Promise<void>((res, rej) => {
|
||||
stream.write(text, err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
rej(err);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
const write = (text: string): Promise<void> => {
|
||||
return new Promise<void>((res, rej) => {
|
||||
stream.write(text, err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
rej(err);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
await write('[');
|
||||
await write('[');
|
||||
|
||||
let exportedNotesCount = 0;
|
||||
let cursor: Note['id'] | null = null;
|
||||
let exportedNotesCount = 0;
|
||||
let cursor: Note['id'] | null = null;
|
||||
|
||||
while (true) {
|
||||
const notes = await Notes.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
}) as Note[];
|
||||
while (true) {
|
||||
const notes = await Notes.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
}) as Note[];
|
||||
|
||||
if (notes.length === 0) {
|
||||
job.progress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = notes[notes.length - 1].id;
|
||||
|
||||
for (const note of notes) {
|
||||
let poll: Poll | undefined;
|
||||
if (note.hasPoll) {
|
||||
poll = await Polls.findOneByOrFail({ noteId: note.id });
|
||||
if (notes.length === 0) {
|
||||
job.progress(100);
|
||||
break;
|
||||
}
|
||||
const content = JSON.stringify(serialize(note, poll));
|
||||
const isFirst = exportedNotesCount === 0;
|
||||
await write(isFirst ? content : ',\n' + content);
|
||||
exportedNotesCount++;
|
||||
|
||||
cursor = notes[notes.length - 1].id;
|
||||
|
||||
for (const note of notes) {
|
||||
let poll: Poll | undefined;
|
||||
if (note.hasPoll) {
|
||||
poll = await Polls.findOneByOrFail({ noteId: note.id });
|
||||
}
|
||||
const content = JSON.stringify(serialize(note, poll));
|
||||
const isFirst = exportedNotesCount === 0;
|
||||
await write(isFirst ? content : ',\n' + content);
|
||||
exportedNotesCount++;
|
||||
}
|
||||
|
||||
const total = await Notes.countBy({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
job.progress(exportedNotesCount / total);
|
||||
}
|
||||
|
||||
const total = await Notes.countBy({
|
||||
userId: user.id,
|
||||
});
|
||||
await write(']');
|
||||
|
||||
job.progress(exportedNotesCount / total);
|
||||
stream.end();
|
||||
logger.succ(`Exported to: ${path}`);
|
||||
|
||||
const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
|
||||
const driveFile = await addFile({ user, path, name: fileName, force: true });
|
||||
|
||||
logger.succ(`Exported to: ${driveFile.id}`);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
await write(']');
|
||||
|
||||
stream.end();
|
||||
logger.succ(`Exported to: ${path}`);
|
||||
|
||||
const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
|
||||
const driveFile = await addFile({ user, path, name: fileName, force: true });
|
||||
|
||||
logger.succ(`Exported to: ${driveFile.id}`);
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import Bull from 'bull';
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import { queueLogger } from '../../logger.js';
|
||||
import { addFile } from '@/services/drive/add-file.js';
|
||||
import { format as dateFormat } from 'date-fns';
|
||||
import { getFullApAccount } from '@/misc/convert-host.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { Users, UserLists, UserListJoinings } from '@/models/index.js';
|
||||
import { In } from 'typeorm';
|
||||
import { DbUserJobData } from '@/queue/types.js';
|
||||
@ -26,46 +26,45 @@ export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any):
|
||||
});
|
||||
|
||||
// Create temp file
|
||||
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
|
||||
tmp.file((e, path, fd, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
});
|
||||
});
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
||||
logger.info(`Temp file is ${path}`);
|
||||
|
||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
try {
|
||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
|
||||
for (const list of lists) {
|
||||
const joinings = await UserListJoinings.findBy({ userListId: list.id });
|
||||
const users = await Users.findBy({
|
||||
id: In(joinings.map(j => j.userId)),
|
||||
});
|
||||
|
||||
for (const u of users) {
|
||||
const acct = getFullApAccount(u.username, u.host);
|
||||
const content = `${list.name},${acct}`;
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
rej(err);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
});
|
||||
for (const list of lists) {
|
||||
const joinings = await UserListJoinings.findBy({ userListId: list.id });
|
||||
const users = await Users.findBy({
|
||||
id: In(joinings.map(j => j.userId)),
|
||||
});
|
||||
|
||||
for (const u of users) {
|
||||
const acct = getFullApAccount(u.username, u.host);
|
||||
const content = `${list.name},${acct}`;
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
rej(err);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
stream.end();
|
||||
logger.succ(`Exported to: ${path}`);
|
||||
|
||||
const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
|
||||
const driveFile = await addFile({ user, path, name: fileName, force: true });
|
||||
|
||||
logger.succ(`Exported to: ${driveFile.id}`);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
stream.end();
|
||||
logger.succ(`Exported to: ${path}`);
|
||||
|
||||
const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
|
||||
const driveFile = await addFile({ user, path, name: fileName, force: true });
|
||||
|
||||
logger.succ(`Exported to: ${driveFile.id}`);
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import Bull from 'bull';
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'node:fs';
|
||||
import unzipper from 'unzipper';
|
||||
|
||||
import { queueLogger } from '../../logger.js';
|
||||
import { createTempDir } from '@/misc/create-temp.js';
|
||||
import { downloadUrl } from '@/misc/download-url.js';
|
||||
import { DriveFiles, Emojis } from '@/models/index.js';
|
||||
import { DbUserImportJobData } from '@/queue/types.js';
|
||||
@ -25,13 +25,7 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
||||
return;
|
||||
}
|
||||
|
||||
// Create temp dir
|
||||
const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => {
|
||||
tmp.dir((e, path, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
});
|
||||
});
|
||||
const [path, cleanup] = await createTempDir();
|
||||
|
||||
logger.info(`Temp dir is ${path}`);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { URL } from 'node:url';
|
||||
import Bull from 'bull';
|
||||
import httpSignature from 'http-signature';
|
||||
import httpSignature from '@peertube/http-signature';
|
||||
import perform from '@/remote/activitypub/perform.js';
|
||||
import Logger from '@/services/logger.js';
|
||||
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
||||
|
@ -3,7 +3,7 @@ import { Note } from '@/models/entities/note';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Webhook } from '@/models/entities/webhook';
|
||||
import { IActivity } from '@/remote/activitypub/type.js';
|
||||
import httpSignature from 'http-signature';
|
||||
import httpSignature from '@peertube/http-signature';
|
||||
|
||||
export type DeliverJobData = {
|
||||
/** Actor */
|
||||
|
@ -5,14 +5,52 @@ import { User, IRemoteUser, CacheableRemoteUser, CacheableUser } from '@/models/
|
||||
import { UserPublickey } from '@/models/entities/user-publickey.js';
|
||||
import { MessagingMessage } from '@/models/entities/messaging-message.js';
|
||||
import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js';
|
||||
import { IObject, getApId } from './type.js';
|
||||
import { resolvePerson } from './models/person.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
|
||||
import { IObject, getApId } from './type.js';
|
||||
import { resolvePerson } from './models/person.js';
|
||||
|
||||
const publicKeyCache = new Cache<UserPublickey | null>(Infinity);
|
||||
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
|
||||
|
||||
export type UriParseResult = {
|
||||
/** wether the URI was generated by us */
|
||||
local: true;
|
||||
/** id in DB */
|
||||
id: string;
|
||||
/** hint of type, e.g. "notes", "users" */
|
||||
type: string;
|
||||
/** any remaining text after type and id, not including the slash after id. undefined if empty */
|
||||
rest?: string;
|
||||
} | {
|
||||
/** wether the URI was generated by us */
|
||||
local: false;
|
||||
/** uri in DB */
|
||||
uri: string;
|
||||
};
|
||||
|
||||
export function parseUri(value: string | IObject): UriParseResult {
|
||||
const uri = getApId(value);
|
||||
|
||||
// the host part of a URL is case insensitive, so use the 'i' flag.
|
||||
const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i');
|
||||
const matchLocal = uri.match(localRegex);
|
||||
|
||||
if (matchLocal) {
|
||||
return {
|
||||
local: true,
|
||||
type: matchLocal[1],
|
||||
id: matchLocal[2],
|
||||
rest: matchLocal[3],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
local: false,
|
||||
uri,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default class DbResolver {
|
||||
constructor() {
|
||||
}
|
||||
@ -21,60 +59,54 @@ export default class DbResolver {
|
||||
* AP Note => Misskey Note in DB
|
||||
*/
|
||||
public async getNoteFromApId(value: string | IObject): Promise<Note | null> {
|
||||
const parsed = this.parseUri(value);
|
||||
const parsed = parseUri(value);
|
||||
|
||||
if (parsed.local) {
|
||||
if (parsed.type !== 'notes') return null;
|
||||
|
||||
if (parsed.id) {
|
||||
return await Notes.findOneBy({
|
||||
id: parsed.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (parsed.uri) {
|
||||
} else {
|
||||
return await Notes.findOneBy({
|
||||
uri: parsed.uri,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getMessageFromApId(value: string | IObject): Promise<MessagingMessage | null> {
|
||||
const parsed = this.parseUri(value);
|
||||
const parsed = parseUri(value);
|
||||
|
||||
if (parsed.local) {
|
||||
if (parsed.type !== 'notes') return null;
|
||||
|
||||
if (parsed.id) {
|
||||
return await MessagingMessages.findOneBy({
|
||||
id: parsed.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (parsed.uri) {
|
||||
} else {
|
||||
return await MessagingMessages.findOneBy({
|
||||
uri: parsed.uri,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* AP Person => Misskey User in DB
|
||||
*/
|
||||
public async getUserFromApId(value: string | IObject): Promise<CacheableUser | null> {
|
||||
const parsed = this.parseUri(value);
|
||||
const parsed = parseUri(value);
|
||||
|
||||
if (parsed.local) {
|
||||
if (parsed.type !== 'users') return null;
|
||||
|
||||
if (parsed.id) {
|
||||
return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({
|
||||
id: parsed.id,
|
||||
}).then(x => x ?? undefined)) ?? null;
|
||||
}
|
||||
|
||||
if (parsed.uri) {
|
||||
} else {
|
||||
return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({
|
||||
uri: parsed.uri,
|
||||
}));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -120,31 +152,4 @@ export default class DbResolver {
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
public parseUri(value: string | IObject): UriParseResult {
|
||||
const uri = getApId(value);
|
||||
|
||||
const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/' + '(\\w+)' + '/' + '(\\w+)');
|
||||
const matchLocal = uri.match(localRegex);
|
||||
|
||||
if (matchLocal) {
|
||||
return {
|
||||
type: matchLocal[1],
|
||||
id: matchLocal[2],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
uri,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type UriParseResult = {
|
||||
/** id in DB (local object only) */
|
||||
id?: string;
|
||||
/** uri in DB (remote object only) */
|
||||
uri?: string;
|
||||
/** hint of type (local object only, ex: notes, users) */
|
||||
type?: string
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { getApLock } from '@/misc/app-lock.js';
|
||||
import { parseAudience } from '../../audience.js';
|
||||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
@ -52,6 +53,8 @@ export default async function(resolver: Resolver, actor: CacheableRemoteUser, ac
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (!await Notes.isVisibleForMe(renote, actor.id)) return 'skip: invalid actor for this activity';
|
||||
|
||||
logger.info(`Creating the (Re)Note: ${uri}`);
|
||||
|
||||
const activityAudience = await parseAudience(actor, activity.to, activity.cc);
|
||||
|
@ -13,37 +13,37 @@ export default async (actor: CacheableRemoteUser, activity: IDelete): Promise<st
|
||||
}
|
||||
|
||||
// 削除対象objectのtype
|
||||
let formarType: string | undefined;
|
||||
let formerType: string | undefined;
|
||||
|
||||
if (typeof activity.object === 'string') {
|
||||
// typeが不明だけど、どうせ消えてるのでremote resolveしない
|
||||
formarType = undefined;
|
||||
formerType = undefined;
|
||||
} else {
|
||||
const object = activity.object as IObject;
|
||||
if (isTombstone(object)) {
|
||||
formarType = toSingle(object.formerType);
|
||||
formerType = toSingle(object.formerType);
|
||||
} else {
|
||||
formarType = toSingle(object.type);
|
||||
formerType = toSingle(object.type);
|
||||
}
|
||||
}
|
||||
|
||||
const uri = getApId(activity.object);
|
||||
|
||||
// type不明でもactorとobjectが同じならばそれはPersonに違いない
|
||||
if (!formarType && actor.uri === uri) {
|
||||
formarType = 'Person';
|
||||
if (!formerType && actor.uri === uri) {
|
||||
formerType = 'Person';
|
||||
}
|
||||
|
||||
// それでもなかったらおそらくNote
|
||||
if (!formarType) {
|
||||
formarType = 'Note';
|
||||
if (!formerType) {
|
||||
formerType = 'Note';
|
||||
}
|
||||
|
||||
if (validPost.includes(formarType)) {
|
||||
if (validPost.includes(formerType)) {
|
||||
return await deleteNote(actor, uri);
|
||||
} else if (validActor.includes(formarType)) {
|
||||
} else if (validActor.includes(formerType)) {
|
||||
return await deleteActor(actor, uri);
|
||||
} else {
|
||||
return `Unknown type ${formarType}`;
|
||||
return `Unknown type ${formerType}`;
|
||||
}
|
||||
};
|
||||
|
@ -8,6 +8,7 @@ export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnoun
|
||||
|
||||
const note = await Notes.findOneBy({
|
||||
uri,
|
||||
userId: actor.id,
|
||||
});
|
||||
|
||||
if (!note) return 'skip: no such Announce';
|
||||
|
@ -3,8 +3,6 @@ import { Note } from '@/models/entities/note.js';
|
||||
import { toHtml } from '../../../mfm/to-html.js';
|
||||
|
||||
export default function(note: Note) {
|
||||
let html = note.text ? toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)) : null;
|
||||
if (html == null) html = '<p>.</p>';
|
||||
|
||||
return html;
|
||||
if (!note.text) return '';
|
||||
return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ import promiseLimit from 'promise-limit';
|
||||
import config from '@/config/index.js';
|
||||
import Resolver from '../resolver.js';
|
||||
import post from '@/services/note/create.js';
|
||||
import { resolvePerson, updatePerson } from './person.js';
|
||||
import { resolvePerson } from './person.js';
|
||||
import { resolveImage } from './image.js';
|
||||
import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import { htmlToMfm } from '../misc/html-to-mfm.js';
|
||||
import { extractApHashtags } from './tag.js';
|
||||
import { unique, toArray, toSingle } from '@/prelude/array.js';
|
||||
@ -15,7 +15,7 @@ import { apLogger } from '../logger.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
|
||||
import { extractDbHost, toPuny } from '@/misc/convert-host.js';
|
||||
import { Emojis, Polls, MessagingMessages, Users } from '@/models/index.js';
|
||||
import { Emojis, Polls, MessagingMessages } from '@/models/index.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js';
|
||||
import { Emoji } from '@/models/entities/emoji.js';
|
||||
|
@ -1,8 +1,20 @@
|
||||
import config from '@/config/index.js';
|
||||
import { ILocalUser, IRemoteUser } from '@/models/entities/user.js';
|
||||
import { Blocking } from '@/models/entities/blocking.js';
|
||||
|
||||
export default (blocker: ILocalUser, blockee: IRemoteUser) => ({
|
||||
type: 'Block',
|
||||
actor: `${config.url}/users/${blocker.id}`,
|
||||
object: blockee.uri,
|
||||
});
|
||||
/**
|
||||
* Renders a block into its ActivityPub representation.
|
||||
*
|
||||
* @param block The block to be rendered. The blockee relation must be loaded.
|
||||
*/
|
||||
export function renderBlock(block: Blocking) {
|
||||
if (block.blockee?.url == null) {
|
||||
throw new Error('renderBlock: missing blockee uri');
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Block',
|
||||
id: `${config.url}/blocks/${block.id}`,
|
||||
actor: `${config.url}/users/${block.blockerId}`,
|
||||
object: block.blockee.uri,
|
||||
};
|
||||
}
|
||||
|
@ -4,12 +4,11 @@ import { Users } from '@/models/index.js';
|
||||
|
||||
export default (follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string) => {
|
||||
const follow = {
|
||||
id: requestId ?? `${config.url}/follows/${follower.id}/${followee.id}`,
|
||||
type: 'Follow',
|
||||
actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri,
|
||||
object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri,
|
||||
} as any;
|
||||
|
||||
if (requestId) follow.id = requestId;
|
||||
|
||||
return follow;
|
||||
};
|
||||
|
@ -8,7 +8,7 @@ import { User } from '@/models/entities/user.js';
|
||||
export const renderActivity = (x: any): IActivity | null => {
|
||||
if (x == null) return null;
|
||||
|
||||
if (x !== null && typeof x === 'object' && x.id == null) {
|
||||
if (typeof x === 'object' && x.id == null) {
|
||||
x.id = `${config.url}/${uuid()}`;
|
||||
}
|
||||
|
||||
|
@ -82,15 +82,15 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
|
||||
|
||||
const files = await getPromisedFiles(note.fileIds);
|
||||
|
||||
const text = note.text;
|
||||
// text should never be undefined
|
||||
const text = note.text ?? null;
|
||||
let poll: Poll | null = null;
|
||||
|
||||
if (note.hasPoll) {
|
||||
poll = await Polls.findOneBy({ noteId: note.id });
|
||||
}
|
||||
|
||||
let apText = text;
|
||||
if (apText == null) apText = '';
|
||||
let apText = text ?? '';
|
||||
|
||||
if (quote) {
|
||||
apText += `\n\nRE: ${quote}`;
|
||||
|
@ -3,9 +3,18 @@ import { getJson } from '@/misc/fetch.js';
|
||||
import { ILocalUser } from '@/models/entities/user.js';
|
||||
import { getInstanceActor } from '@/services/instance-actor.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractDbHost, isSelfHost } from '@/misc/convert-host.js';
|
||||
import { signedGet } from './request.js';
|
||||
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js';
|
||||
import { FollowRequests, Notes, NoteReactions, Polls, Users } from '@/models/index.js';
|
||||
import { parseUri } from './db-resolver.js';
|
||||
import renderNote from '@/remote/activitypub/renderer/note.js';
|
||||
import { renderLike } from '@/remote/activitypub/renderer/like.js';
|
||||
import { renderPerson } from '@/remote/activitypub/renderer/person.js';
|
||||
import renderQuestion from '@/remote/activitypub/renderer/question.js';
|
||||
import renderCreate from '@/remote/activitypub/renderer/create.js';
|
||||
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||
import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
||||
|
||||
export default class Resolver {
|
||||
private history: Set<string>;
|
||||
@ -40,14 +49,25 @@ export default class Resolver {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value.includes('#')) {
|
||||
// URLs with fragment parts cannot be resolved correctly because
|
||||
// the fragment part does not get transmitted over HTTP(S).
|
||||
// Avoid strange behaviour by not trying to resolve these at all.
|
||||
throw new Error(`cannot resolve URL with fragment: ${value}`);
|
||||
}
|
||||
|
||||
if (this.history.has(value)) {
|
||||
throw new Error('cannot resolve already resolved one');
|
||||
}
|
||||
|
||||
this.history.add(value);
|
||||
|
||||
const meta = await fetchMeta();
|
||||
const host = extractDbHost(value);
|
||||
if (isSelfHost(host)) {
|
||||
return await this.resolveLocal(value);
|
||||
}
|
||||
|
||||
const meta = await fetchMeta();
|
||||
if (meta.blockedHosts.includes(host)) {
|
||||
throw new Error('Instance is blocked');
|
||||
}
|
||||
@ -70,4 +90,44 @@ export default class Resolver {
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
private resolveLocal(url: string): Promise<IObject> {
|
||||
const parsed = parseUri(url);
|
||||
if (!parsed.local) throw new Error('resolveLocal: not local');
|
||||
|
||||
switch (parsed.type) {
|
||||
case 'notes':
|
||||
return Notes.findOneByOrFail({ id: parsed.id })
|
||||
.then(note => {
|
||||
if (parsed.rest === 'activity') {
|
||||
// this refers to the create activity and not the note itself
|
||||
return renderActivity(renderCreate(renderNote(note)));
|
||||
} else {
|
||||
return renderNote(note);
|
||||
}
|
||||
});
|
||||
case 'users':
|
||||
return Users.findOneByOrFail({ id: parsed.id })
|
||||
.then(user => renderPerson(user as ILocalUser));
|
||||
case 'questions':
|
||||
// Polls are indexed by the note they are attached to.
|
||||
return Promise.all([
|
||||
Notes.findOneByOrFail({ id: parsed.id }),
|
||||
Polls.findOneByOrFail({ noteId: parsed.id }),
|
||||
])
|
||||
.then(([note, poll]) => renderQuestion({ id: note.userId }, note, poll));
|
||||
case 'likes':
|
||||
return NoteReactions.findOneByOrFail({ id: parsed.id }).then(reaction => renderActivity(renderLike(reaction, { uri: null })));
|
||||
case 'follows':
|
||||
// rest should be <followee id>
|
||||
if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI');
|
||||
|
||||
return Promise.all(
|
||||
[parsed.id, parsed.rest].map(id => Users.findOneByOrFail({ id }))
|
||||
)
|
||||
.then(([follower, followee]) => renderActivity(renderFollow(follower, followee, url)));
|
||||
default:
|
||||
throw new Error(`resolveLocal: type ${type} unhandled`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Router from '@koa/router';
|
||||
import json from 'koa-json-body';
|
||||
import httpSignature from 'http-signature';
|
||||
import httpSignature from '@peertube/http-signature';
|
||||
|
||||
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||
import renderNote from '@/remote/activitypub/renderer/note.js';
|
||||
@ -15,9 +15,10 @@ import { inbox as processInbox } from '@/queue/index.js';
|
||||
import { isSelfHost } from '@/misc/convert-host.js';
|
||||
import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js';
|
||||
import { ILocalUser, User } from '@/models/entities/user.js';
|
||||
import { In, IsNull } from 'typeorm';
|
||||
import { In, IsNull, Not } from 'typeorm';
|
||||
import { renderLike } from '@/remote/activitypub/renderer/like.js';
|
||||
import { getUserKeypair } from '@/misc/keypair-store.js';
|
||||
import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
@ -224,4 +225,30 @@ router.get('/likes/:like', async ctx => {
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
// follow
|
||||
router.get('/follows/:follower/:followee', async ctx => {
|
||||
// This may be used before the follow is completed, so we do not
|
||||
// check if the following exists.
|
||||
|
||||
const [follower, followee] = await Promise.all([
|
||||
Users.findOneBy({
|
||||
id: ctx.params.follower,
|
||||
host: IsNull(),
|
||||
}),
|
||||
Users.findOneBy({
|
||||
id: ctx.params.followee,
|
||||
host: Not(IsNull()),
|
||||
}),
|
||||
]);
|
||||
|
||||
if (follower == null || followee == null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = renderActivity(renderFollow(follower, followee));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
@ -2,10 +2,11 @@ import Koa from 'koa';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { limiter } from './limiter.js';
|
||||
import { CacheableLocalUser, User } from '@/models/entities/user.js';
|
||||
import endpoints, { IEndpoint } from './endpoints.js';
|
||||
import endpoints, { IEndpointMeta } from './endpoints.js';
|
||||
import { ApiError } from './error.js';
|
||||
import { apiLogger } from './logger.js';
|
||||
import { AccessToken } from '@/models/entities/access-token.js';
|
||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||
|
||||
const accessDenied = {
|
||||
message: 'Access denied.',
|
||||
@ -15,6 +16,7 @@ const accessDenied = {
|
||||
|
||||
export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => {
|
||||
const isSecure = user != null && token == null;
|
||||
const isModerator = user != null && (user.isModerator || user.isAdmin);
|
||||
|
||||
const ep = endpoints.find(e => e.name === endpoint);
|
||||
|
||||
@ -31,6 +33,32 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
||||
throw new ApiError(accessDenied);
|
||||
}
|
||||
|
||||
if (ep.meta.limit && !isModerator) {
|
||||
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
|
||||
let limitActor: string;
|
||||
if (user) {
|
||||
limitActor = user.id;
|
||||
} else {
|
||||
limitActor = getIpHash(ctx!.ip);
|
||||
}
|
||||
|
||||
const limit = Object.assign({}, ep.meta.limit);
|
||||
|
||||
if (!limit.key) {
|
||||
limit.key = ep.name;
|
||||
}
|
||||
|
||||
// Rate limit
|
||||
await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(e => {
|
||||
throw new ApiError({
|
||||
message: 'Rate limit exceeded. Please try again later.',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
|
||||
httpStatusCode: 429,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (ep.meta.requireCredential && user == null) {
|
||||
throw new ApiError({
|
||||
message: 'Credential required.',
|
||||
@ -53,7 +81,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
||||
throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
|
||||
}
|
||||
|
||||
if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) {
|
||||
if (ep.meta.requireModerator && !isModerator) {
|
||||
throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
|
||||
}
|
||||
|
||||
@ -65,18 +93,6 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
||||
});
|
||||
}
|
||||
|
||||
if (ep.meta.requireCredential && ep.meta.limit && !user!.isAdmin && !user!.isModerator) {
|
||||
// Rate limit
|
||||
await limiter(ep as IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user!).catch(e => {
|
||||
throw new ApiError({
|
||||
message: 'Rate limit exceeded. Please try again later.',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
|
||||
httpStatusCode: 429,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Cast non JSON input
|
||||
if (ep.meta.requireFile && ep.params.properties) {
|
||||
for (const k of Object.keys(ep.params.properties)) {
|
||||
|
@ -654,7 +654,6 @@ export interface IEndpointMeta {
|
||||
/**
|
||||
* エンドポイントのリミテーションに関するやつ
|
||||
* 省略した場合はリミテーションは無いものとして解釈されます。
|
||||
* また、withCredential が false の場合はリミテーションを行うことはできません。
|
||||
*/
|
||||
readonly limit?: {
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Signins, UserProfiles, Users } from '@/models/index.js';
|
||||
import define from '../../define.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@ -23,9 +23,12 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const user = await Users.findOneBy({ id: ps.userId });
|
||||
const [user, profile] = await Promise.all([
|
||||
Users.findOneBy({ id: ps.userId }),
|
||||
UserProfiles.findOneBy({ userId: ps.userId })
|
||||
]);
|
||||
|
||||
if (user == null) {
|
||||
if (user == null || profile == null) {
|
||||
throw new Error('user not found');
|
||||
}
|
||||
|
||||
@ -34,8 +37,37 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||
throw new Error('cannot show info of admin');
|
||||
}
|
||||
|
||||
if (!_me.isAdmin) {
|
||||
return {
|
||||
isModerator: user.isModerator,
|
||||
isSilenced: user.isSilenced,
|
||||
isSuspended: user.isSuspended,
|
||||
};
|
||||
}
|
||||
|
||||
const maskedKeys = ['accessToken', 'accessTokenSecret', 'refreshToken'];
|
||||
Object.keys(profile.integrations).forEach(integration => {
|
||||
maskedKeys.forEach(key => profile.integrations[integration][key] = '<MASKED>');
|
||||
});
|
||||
|
||||
const signins = await Signins.findBy({ userId: user.id });
|
||||
|
||||
return {
|
||||
...user,
|
||||
token: user.token != null ? '<MASKED>' : user.token,
|
||||
email: profile.email,
|
||||
emailVerified: profile.emailVerified,
|
||||
autoAcceptFollowed: profile.autoAcceptFollowed,
|
||||
noCrawle: profile.noCrawle,
|
||||
alwaysMarkNsfw: profile.alwaysMarkNsfw,
|
||||
carefulBot: profile.carefulBot,
|
||||
injectFeaturedNote: profile.injectFeaturedNote,
|
||||
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
||||
integrations: profile.integrations,
|
||||
mutedWords: profile.mutedWords,
|
||||
mutedInstances: profile.mutedInstances,
|
||||
mutingNotificationTypes: profile.mutingNotificationTypes,
|
||||
isModerator: user.isModerator,
|
||||
isSilenced: user.isSilenced,
|
||||
isSuspended: user.isSuspended,
|
||||
signins,
|
||||
};
|
||||
});
|
||||
|
@ -27,7 +27,7 @@ export const paramDef = {
|
||||
blockedHosts: { type: 'array', nullable: true, items: {
|
||||
type: 'string',
|
||||
} },
|
||||
themeColor: { type: 'string', nullable: true },
|
||||
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
|
||||
mascotImageUrl: { type: 'string', nullable: true },
|
||||
bannerUrl: { type: 'string', nullable: true },
|
||||
errorImageUrl: { type: 'string', nullable: true },
|
||||
|
@ -134,7 +134,7 @@ export const paramDef = {
|
||||
{
|
||||
// (re)note with text, files and poll are optional
|
||||
properties: {
|
||||
text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false },
|
||||
text: { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false },
|
||||
},
|
||||
required: ['text'],
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
@ -31,109 +32,72 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
|
||||
const [
|
||||
notesCount,
|
||||
repliesCount,
|
||||
renotesCount,
|
||||
repliedCount,
|
||||
renotedCount,
|
||||
pollVotesCount,
|
||||
pollVotedCount,
|
||||
localFollowingCount,
|
||||
remoteFollowingCount,
|
||||
localFollowersCount,
|
||||
remoteFollowersCount,
|
||||
sentReactionsCount,
|
||||
receivedReactionsCount,
|
||||
noteFavoritesCount,
|
||||
pageLikesCount,
|
||||
pageLikedCount,
|
||||
driveFilesCount,
|
||||
driveUsage,
|
||||
] = await Promise.all([
|
||||
Notes.createQueryBuilder('note')
|
||||
const result = await awaitAll({
|
||||
notesCount: Notes.createQueryBuilder('note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
Notes.createQueryBuilder('note')
|
||||
repliesCount: Notes.createQueryBuilder('note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.andWhere('note.replyId IS NOT NULL')
|
||||
.getCount(),
|
||||
Notes.createQueryBuilder('note')
|
||||
renotesCount: Notes.createQueryBuilder('note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.andWhere('note.renoteId IS NOT NULL')
|
||||
.getCount(),
|
||||
Notes.createQueryBuilder('note')
|
||||
repliedCount: Notes.createQueryBuilder('note')
|
||||
.where('note.replyUserId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
Notes.createQueryBuilder('note')
|
||||
renotedCount: Notes.createQueryBuilder('note')
|
||||
.where('note.renoteUserId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
PollVotes.createQueryBuilder('vote')
|
||||
pollVotesCount: PollVotes.createQueryBuilder('vote')
|
||||
.where('vote.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
PollVotes.createQueryBuilder('vote')
|
||||
pollVotedCount: PollVotes.createQueryBuilder('vote')
|
||||
.innerJoin('vote.note', 'note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
Followings.createQueryBuilder('following')
|
||||
localFollowingCount: Followings.createQueryBuilder('following')
|
||||
.where('following.followerId = :userId', { userId: user.id })
|
||||
.andWhere('following.followeeHost IS NULL')
|
||||
.getCount(),
|
||||
Followings.createQueryBuilder('following')
|
||||
remoteFollowingCount: Followings.createQueryBuilder('following')
|
||||
.where('following.followerId = :userId', { userId: user.id })
|
||||
.andWhere('following.followeeHost IS NOT NULL')
|
||||
.getCount(),
|
||||
Followings.createQueryBuilder('following')
|
||||
localFollowersCount: Followings.createQueryBuilder('following')
|
||||
.where('following.followeeId = :userId', { userId: user.id })
|
||||
.andWhere('following.followerHost IS NULL')
|
||||
.getCount(),
|
||||
Followings.createQueryBuilder('following')
|
||||
remoteFollowersCount: Followings.createQueryBuilder('following')
|
||||
.where('following.followeeId = :userId', { userId: user.id })
|
||||
.andWhere('following.followerHost IS NOT NULL')
|
||||
.getCount(),
|
||||
NoteReactions.createQueryBuilder('reaction')
|
||||
sentReactionsCount: NoteReactions.createQueryBuilder('reaction')
|
||||
.where('reaction.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
NoteReactions.createQueryBuilder('reaction')
|
||||
receivedReactionsCount: NoteReactions.createQueryBuilder('reaction')
|
||||
.innerJoin('reaction.note', 'note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
NoteFavorites.createQueryBuilder('favorite')
|
||||
noteFavoritesCount: NoteFavorites.createQueryBuilder('favorite')
|
||||
.where('favorite.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
PageLikes.createQueryBuilder('like')
|
||||
pageLikesCount: PageLikes.createQueryBuilder('like')
|
||||
.where('like.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
PageLikes.createQueryBuilder('like')
|
||||
pageLikedCount: PageLikes.createQueryBuilder('like')
|
||||
.innerJoin('like.page', 'page')
|
||||
.where('page.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
DriveFiles.createQueryBuilder('file')
|
||||
driveFilesCount: DriveFiles.createQueryBuilder('file')
|
||||
.where('file.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
DriveFiles.calcDriveUsageOf(user),
|
||||
]);
|
||||
driveUsage: DriveFiles.calcDriveUsageOf(user),
|
||||
});
|
||||
|
||||
return {
|
||||
notesCount,
|
||||
repliesCount,
|
||||
renotesCount,
|
||||
repliedCount,
|
||||
renotedCount,
|
||||
pollVotesCount,
|
||||
pollVotedCount,
|
||||
localFollowingCount,
|
||||
remoteFollowingCount,
|
||||
localFollowersCount,
|
||||
remoteFollowersCount,
|
||||
followingCount: localFollowingCount + remoteFollowingCount,
|
||||
followersCount: localFollowersCount + remoteFollowersCount,
|
||||
sentReactionsCount,
|
||||
receivedReactionsCount,
|
||||
noteFavoritesCount,
|
||||
pageLikesCount,
|
||||
pageLikedCount,
|
||||
driveFilesCount,
|
||||
driveUsage,
|
||||
};
|
||||
result.followingCount = result.localFollowingCount + result.remoteFollowingCount;
|
||||
result.followersCount = result.localFollowersCount + result.remoteFollowersCount;
|
||||
|
||||
return result;
|
||||
});
|
||||
|
@ -1,25 +1,17 @@
|
||||
import Limiter from 'ratelimiter';
|
||||
import { redisClient } from '../../db/redis.js';
|
||||
import { IEndpoint } from './endpoints.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { IEndpointMeta } from './endpoints.js';
|
||||
import { CacheableLocalUser, User } from '@/models/entities/user.js';
|
||||
import Logger from '@/services/logger.js';
|
||||
|
||||
const logger = new Logger('limiter');
|
||||
|
||||
export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user: CacheableLocalUser) => new Promise<void>((ok, reject) => {
|
||||
const limitation = endpoint.meta.limit;
|
||||
|
||||
const key = Object.prototype.hasOwnProperty.call(limitation, 'key')
|
||||
? limitation.key
|
||||
: endpoint.name;
|
||||
|
||||
const hasShortTermLimit =
|
||||
Object.prototype.hasOwnProperty.call(limitation, 'minInterval');
|
||||
export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string) => new Promise<void>((ok, reject) => {
|
||||
const hasShortTermLimit = typeof limitation.minInterval === 'number';
|
||||
|
||||
const hasLongTermLimit =
|
||||
Object.prototype.hasOwnProperty.call(limitation, 'duration') &&
|
||||
Object.prototype.hasOwnProperty.call(limitation, 'max');
|
||||
typeof limitation.duration === 'number' &&
|
||||
typeof limitation.max === 'number';
|
||||
|
||||
if (hasShortTermLimit) {
|
||||
min();
|
||||
@ -32,7 +24,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
|
||||
// Short-term limit
|
||||
function min(): void {
|
||||
const minIntervalLimiter = new Limiter({
|
||||
id: `${user.id}:${key}:min`,
|
||||
id: `${actor}:${limitation.key}:min`,
|
||||
duration: limitation.minInterval,
|
||||
max: 1,
|
||||
db: redisClient,
|
||||
@ -43,7 +35,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
|
||||
return reject('ERR');
|
||||
}
|
||||
|
||||
logger.debug(`@${Acct.toString(user)} ${endpoint.name} min remaining: ${info.remaining}`);
|
||||
logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
|
||||
|
||||
if (info.remaining === 0) {
|
||||
reject('BRIEF_REQUEST_INTERVAL');
|
||||
@ -60,7 +52,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
|
||||
// Long term limit
|
||||
function max(): void {
|
||||
const limiter = new Limiter({
|
||||
id: `${user.id}:${key}`,
|
||||
id: `${actor}:${limitation.key}`,
|
||||
duration: limitation.duration,
|
||||
max: limitation.max,
|
||||
db: redisClient,
|
||||
@ -71,7 +63,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
|
||||
return reject('ERR');
|
||||
}
|
||||
|
||||
logger.debug(`@${Acct.toString(user)} ${endpoint.name} max remaining: ${info.remaining}`);
|
||||
logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
|
||||
|
||||
if (info.remaining === 0) {
|
||||
reject('RATE_LIMIT_EXCEEDED');
|
||||
|
@ -59,6 +59,18 @@ export function genOpenapiSpec(lang = 'ja-JP') {
|
||||
desc += ` / **Permission**: *${kind}*`;
|
||||
}
|
||||
|
||||
const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
|
||||
const schema = endpoint.params;
|
||||
|
||||
if (endpoint.meta.requireFile) {
|
||||
schema.properties.file = {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
description: 'The file contents.',
|
||||
};
|
||||
schema.required.push('file');
|
||||
}
|
||||
|
||||
const info = {
|
||||
operationId: endpoint.name,
|
||||
summary: endpoint.name,
|
||||
@ -78,8 +90,8 @@ export function genOpenapiSpec(lang = 'ja-JP') {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: endpoint.params,
|
||||
[requestType]: {
|
||||
schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1,25 +1,22 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import Koa from 'koa';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import * as speakeasy from 'speakeasy';
|
||||
import { IsNull } from 'typeorm';
|
||||
import signin from '../common/signin.js';
|
||||
import config from '@/config/index.js';
|
||||
import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index.js';
|
||||
import { ILocalUser } from '@/models/entities/user.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha.js';
|
||||
import { verifyLogin, hash } from '../2fa.js';
|
||||
import signin from '../common/signin.js';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { limiter } from '../limiter.js';
|
||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||
|
||||
export default async (ctx: Koa.Context) => {
|
||||
ctx.set('Access-Control-Allow-Origin', config.url);
|
||||
ctx.set('Access-Control-Allow-Credentials', 'true');
|
||||
|
||||
const body = ctx.request.body as any;
|
||||
|
||||
const instance = await fetchMeta(true);
|
||||
|
||||
const username = body['username'];
|
||||
const password = body['password'];
|
||||
const token = body['token'];
|
||||
@ -29,6 +26,21 @@ export default async (ctx: Koa.Context) => {
|
||||
ctx.body = { error };
|
||||
}
|
||||
|
||||
try {
|
||||
// not more than 1 attempt per second and not more than 10 attempts per hour
|
||||
await limiter({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(ctx.ip));
|
||||
} catch (err) {
|
||||
ctx.status = 429;
|
||||
ctx.body = {
|
||||
error: {
|
||||
message: 'Too many failed attempts to sign in. Try again later.',
|
||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||
},
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof username !== 'string') {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
@ -84,18 +96,6 @@ export default async (ctx: Koa.Context) => {
|
||||
}
|
||||
|
||||
if (!profile.twoFactorEnabled) {
|
||||
if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
|
||||
await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => {
|
||||
ctx.throw(400, e);
|
||||
});
|
||||
}
|
||||
|
||||
if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
|
||||
await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => {
|
||||
ctx.throw(400, e);
|
||||
});
|
||||
}
|
||||
|
||||
if (same) {
|
||||
signin(ctx, user);
|
||||
return;
|
||||
@ -172,7 +172,7 @@ export default async (ctx: Koa.Context) => {
|
||||
body.credentialId
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/'),
|
||||
'base64',
|
||||
'base64'
|
||||
).toString('hex'),
|
||||
});
|
||||
|
||||
|
@ -4,11 +4,11 @@ import { dirname } from 'node:path';
|
||||
import Koa from 'koa';
|
||||
import send from 'koa-send';
|
||||
import rename from 'rename';
|
||||
import * as tmp from 'tmp';
|
||||
import { serverLogger } from '../index.js';
|
||||
import { contentDisposition } from '@/misc/content-disposition.js';
|
||||
import { DriveFiles } from '@/models/index.js';
|
||||
import { InternalStorage } from '@/services/drive/internal-storage.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { downloadUrl } from '@/misc/download-url.js';
|
||||
import { detectType } from '@/misc/get-file-info.js';
|
||||
import { convertToWebp, convertToJpeg, convertToPng } from '@/services/drive/image-processor.js';
|
||||
@ -50,12 +50,7 @@ export default async function(ctx: Koa.Context) {
|
||||
|
||||
if (!file.storedInternal) {
|
||||
if (file.isLink && file.uri) { // 期限切れリモートファイル
|
||||
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
|
||||
tmp.file((e, path, fd, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
});
|
||||
});
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
||||
try {
|
||||
await downloadUrl(file.uri, path);
|
||||
|
@ -2,6 +2,7 @@
|
||||
* Core Server
|
||||
*/
|
||||
|
||||
import cluster from 'node:cluster';
|
||||
import * as fs from 'node:fs';
|
||||
import * as http from 'node:http';
|
||||
import Koa from 'koa';
|
||||
@ -88,10 +89,10 @@ router.get('/avatar/@:acct', async ctx => {
|
||||
});
|
||||
|
||||
router.get('/identicon/:x', async ctx => {
|
||||
const [temp] = await createTemp();
|
||||
const [temp, cleanup] = await createTemp();
|
||||
await genIdenticon(ctx.params.x, fs.createWriteStream(temp));
|
||||
ctx.set('Content-Type', 'image/png');
|
||||
ctx.body = fs.createReadStream(temp);
|
||||
ctx.body = fs.createReadStream(temp).on('close', () => cleanup());
|
||||
});
|
||||
|
||||
router.get('/verify-email/:code', async ctx => {
|
||||
@ -142,5 +143,26 @@ export default () => new Promise(resolve => {
|
||||
|
||||
initializeStreamingServer(server);
|
||||
|
||||
server.on('error', e => {
|
||||
switch ((e as any).code) {
|
||||
case 'EACCES':
|
||||
serverLogger.error(`You do not have permission to listen on port ${config.port}.`);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
serverLogger.error(`Port ${config.port} is already in use by another process.`);
|
||||
break;
|
||||
default:
|
||||
serverLogger.error(e);
|
||||
break;
|
||||
}
|
||||
|
||||
if (cluster.isWorker) {
|
||||
process.send!('listenFailed');
|
||||
} else {
|
||||
// disableClustering
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(config.port, resolve);
|
||||
});
|
||||
|
@ -54,14 +54,10 @@
|
||||
//#endregion
|
||||
|
||||
//#region Script
|
||||
const salt = localStorage.getItem('salt')
|
||||
? `?salt=${localStorage.getItem('salt')}`
|
||||
: '';
|
||||
|
||||
import(`/assets/${CLIENT_ENTRY}${salt}`)
|
||||
.catch(async () => {
|
||||
import(`/assets/${CLIENT_ENTRY}`)
|
||||
.catch(async e => {
|
||||
await checkUpdate();
|
||||
renderError('APP_FETCH_FAILED');
|
||||
renderError('APP_FETCH_FAILED', JSON.stringify(e));
|
||||
})
|
||||
//#endregion
|
||||
|
||||
@ -142,9 +138,6 @@
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function refresh() {
|
||||
// Random
|
||||
localStorage.setItem('salt', Math.random().toString().substr(2, 8));
|
||||
|
||||
// Clear cache (service worker)
|
||||
try {
|
||||
navigator.serviceWorker.controller.postMessage('clear');
|
||||
|
@ -74,9 +74,9 @@ app.use(views(_dirname + '/views', {
|
||||
extension: 'pug',
|
||||
options: {
|
||||
version: config.version,
|
||||
clientEntry: () => process.env.NODE_ENV === 'production' ?
|
||||
getClientEntry: () => process.env.NODE_ENV === 'production' ?
|
||||
config.clientEntry :
|
||||
JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'].file.replace(/^_client_dist_\//, ''),
|
||||
JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'],
|
||||
config,
|
||||
},
|
||||
}));
|
||||
@ -247,7 +247,7 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
});
|
||||
ctx.set('Cache-Control', 'public, max-age=30');
|
||||
ctx.set('Cache-Control', 'public, max-age=15');
|
||||
} else {
|
||||
// リモートユーザーなので
|
||||
// モデレータがAPI経由で参照可能にするために404にはしない
|
||||
@ -292,7 +292,7 @@ router.get('/notes/:note', async (ctx, next) => {
|
||||
themeColor: meta.themeColor,
|
||||
});
|
||||
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
ctx.set('Cache-Control', 'public, max-age=15');
|
||||
|
||||
return;
|
||||
}
|
||||
@ -329,7 +329,7 @@ router.get('/@:user/pages/:page', async (ctx, next) => {
|
||||
});
|
||||
|
||||
if (['public'].includes(page.visibility)) {
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
ctx.set('Cache-Control', 'public, max-age=15');
|
||||
} else {
|
||||
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
|
||||
}
|
||||
@ -360,7 +360,7 @@ router.get('/clips/:clip', async (ctx, next) => {
|
||||
themeColor: meta.themeColor,
|
||||
});
|
||||
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
ctx.set('Cache-Control', 'public, max-age=15');
|
||||
|
||||
return;
|
||||
}
|
||||
@ -385,7 +385,7 @@ router.get('/gallery/:post', async (ctx, next) => {
|
||||
themeColor: meta.themeColor,
|
||||
});
|
||||
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
ctx.set('Cache-Control', 'public, max-age=15');
|
||||
|
||||
return;
|
||||
}
|
||||
@ -409,7 +409,7 @@ router.get('/channels/:channel', async (ctx, next) => {
|
||||
themeColor: meta.themeColor,
|
||||
});
|
||||
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
ctx.set('Cache-Control', 'public, max-age=15');
|
||||
|
||||
return;
|
||||
}
|
||||
@ -468,7 +468,7 @@ router.get('(.*)', async ctx => {
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
});
|
||||
ctx.set('Cache-Control', 'public, max-age=300');
|
||||
ctx.set('Cache-Control', 'public, max-age=15');
|
||||
});
|
||||
|
||||
// Register router
|
||||
|
@ -3,7 +3,9 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import manifest from './manifest.json' assert { type: 'json' };
|
||||
|
||||
export const manifestHandler = async (ctx: Koa.Context) => {
|
||||
const res = structuredClone(manifest);
|
||||
// TODO
|
||||
//const res = structuredClone(manifest);
|
||||
const res = JSON.parse(JSON.stringify(manifest));
|
||||
|
||||
const instance = await fetchMeta(true);
|
||||
|
||||
|
@ -39,28 +39,24 @@ html {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
transform: translateY(70px);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
#splashSpinner:before,
|
||||
#splashSpinner:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: solid 4px;
|
||||
}
|
||||
|
||||
#splashSpinner:before {
|
||||
border-color: currentColor;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
#splashSpinner:after {
|
||||
#splashSpinner > .spinner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
border-color: currentColor transparent transparent transparent;
|
||||
left: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
fill-rule: evenodd;
|
||||
clip-rule: evenodd;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-miterlimit: 1.5;
|
||||
}
|
||||
#splashSpinner > .spinner.bg {
|
||||
opacity: 0.275;
|
||||
}
|
||||
#splashSpinner > .spinner.fg {
|
||||
animation: splashSpinner 0.5s linear infinite;
|
||||
}
|
||||
|
||||
|
@ -1,17 +1,23 @@
|
||||
block vars
|
||||
|
||||
block loadClientEntry
|
||||
- const clientEntry = getClientEntry();
|
||||
|
||||
doctype html
|
||||
|
||||
!= '<!--\n'
|
||||
!= ' _____ _ _ \n'
|
||||
!= ' | |_|___ ___| |_ ___ _ _ \n'
|
||||
!= ' | | | | |_ -|_ -| \'_| -_| | |\n'
|
||||
!= ' |_|_|_|_|___|___|_,_|___|_ |\n'
|
||||
!= ' |___|\n'
|
||||
!= ' Thank you for using Misskey!\n'
|
||||
!= ' If you are reading this message... how about joining the development?\n'
|
||||
!= ' https://github.com/misskey-dev/misskey'
|
||||
!= '\n-->\n'
|
||||
//
|
||||
-
|
||||
|
||||
_____ _ _
|
||||
| |_|___ ___| |_ ___ _ _
|
||||
| | | | |_ -|_ -| \'_| -_| | |
|
||||
|_|_|_|_|___|___|_,_|___|_ |
|
||||
|___|
|
||||
Thank you for using Misskey!
|
||||
If you are reading this message... how about joining the development?
|
||||
https://github.com/misskey-dev/misskey
|
||||
|
||||
|
||||
|
||||
html
|
||||
|
||||
@ -30,8 +36,14 @@ html
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
|
||||
link(rel='preload' href='/assets/fontawesome/css/all.css' as='style')
|
||||
link(rel='stylesheet' href='/assets/fontawesome/css/all.css')
|
||||
link(rel='modulepreload' href=`/assets/${clientEntry.file}`)
|
||||
|
||||
each href in clientEntry.css
|
||||
link(rel='preload' href=`/assets/${href}` as='style')
|
||||
|
||||
each href in clientEntry.css
|
||||
link(rel='preload' href=`/assets/${href}` as='style')
|
||||
|
||||
title
|
||||
block title
|
||||
@ -52,7 +64,7 @@ html
|
||||
|
||||
script.
|
||||
var VERSION = "#{version}";
|
||||
var CLIENT_ENTRY = "#{clientEntry()}";
|
||||
var CLIENT_ENTRY = "#{clientEntry.file}";
|
||||
|
||||
script
|
||||
include ../boot.js
|
||||
@ -65,4 +77,14 @@ html
|
||||
div#splash
|
||||
img#splashIcon(src= icon || '/static-assets/splash.png')
|
||||
div#splashSpinner
|
||||
<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>
|
||||
<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
|
||||
|
@ -2,9 +2,10 @@ import { publishMainStream, publishUserEvent } from '@/services/stream.js';
|
||||
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||
import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
||||
import renderUndo from '@/remote/activitypub/renderer/undo.js';
|
||||
import renderBlock from '@/remote/activitypub/renderer/block.js';
|
||||
import { renderBlock } from '@/remote/activitypub/renderer/block.js';
|
||||
import { deliver } from '@/queue/index.js';
|
||||
import renderReject from '@/remote/activitypub/renderer/reject.js';
|
||||
import { Blocking } from '@/models/entities/blocking.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index.js';
|
||||
import { perUserFollowingChart } from '@/services/chart/index.js';
|
||||
@ -22,15 +23,19 @@ export default async function(blocker: User, blockee: User) {
|
||||
removeFromList(blockee, blocker),
|
||||
]);
|
||||
|
||||
await Blockings.insert({
|
||||
const blocking = {
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
blocker,
|
||||
blockerId: blocker.id,
|
||||
blockee,
|
||||
blockeeId: blockee.id,
|
||||
});
|
||||
} as Blocking;
|
||||
|
||||
await Blockings.insert(blocking);
|
||||
|
||||
if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
|
||||
const content = renderActivity(renderBlock(blocker, blockee));
|
||||
const content = renderActivity(renderBlock(blocking));
|
||||
deliver(blocker, content, blockee.inbox);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||
import renderBlock from '@/remote/activitypub/renderer/block.js';
|
||||
import { renderBlock } from '@/remote/activitypub/renderer/block.js';
|
||||
import renderUndo from '@/remote/activitypub/renderer/undo.js';
|
||||
import { deliver } from '@/queue/index.js';
|
||||
import Logger from '../logger.js';
|
||||
@ -19,11 +19,16 @@ export default async function(blocker: CacheableUser, blockee: CacheableUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since we already have the blocker and blockee, we do not need to fetch
|
||||
// them in the query above and can just manually insert them here.
|
||||
blocking.blocker = blocker;
|
||||
blocking.blockee = blockee;
|
||||
|
||||
Blockings.delete(blocking.id);
|
||||
|
||||
// deliver if remote bloking
|
||||
if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
|
||||
const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker));
|
||||
const content = renderActivity(renderUndo(renderBlock(blocking), blocker));
|
||||
deliver(blocker, content, blockee.inbox);
|
||||
}
|
||||
}
|
||||
|
@ -91,27 +91,20 @@ type ToJsonSchema<S> = {
|
||||
};
|
||||
|
||||
export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatten<ChartResult<S>>> {
|
||||
const object = {};
|
||||
for (const [k, v] of Object.entries(schema)) {
|
||||
nestedProperty.set(object, k, null);
|
||||
}
|
||||
const jsonSchema = {
|
||||
type: 'object',
|
||||
properties: {} as Record<string, unknown>,
|
||||
required: [],
|
||||
};
|
||||
|
||||
function f(obj: Record<string, null | Record<string, unknown>>) {
|
||||
const jsonSchema = {
|
||||
type: 'object',
|
||||
properties: {} as Record<string, unknown>,
|
||||
required: [],
|
||||
for (const k in schema) {
|
||||
jsonSchema.properties[k] = {
|
||||
type: 'array',
|
||||
items: { type: 'number' },
|
||||
};
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
jsonSchema.properties[k] = v === null ? {
|
||||
type: 'array',
|
||||
items: { type: 'number' },
|
||||
} : f(v as Record<string, null | Record<string, unknown>>);
|
||||
}
|
||||
return jsonSchema;
|
||||
}
|
||||
|
||||
return f(object) as ToJsonSchema<Unflatten<ChartResult<S>>>;
|
||||
return jsonSchema as ToJsonSchema<Unflatten<ChartResult<S>>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -11,6 +11,11 @@ import { entity as PerUserFollowingChart } from './charts/entities/per-user-foll
|
||||
import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js';
|
||||
import { entity as ApRequestChart } from './charts/entities/ap-request.js';
|
||||
|
||||
import { entity as TestChart } from './charts/entities/test.js';
|
||||
import { entity as TestGroupedChart } from './charts/entities/test-grouped.js';
|
||||
import { entity as TestUniqueChart } from './charts/entities/test-unique.js';
|
||||
import { entity as TestIntersectionChart } from './charts/entities/test-intersection.js';
|
||||
|
||||
export const entities = [
|
||||
FederationChart.hour, FederationChart.day,
|
||||
NotesChart.hour, NotesChart.day,
|
||||
@ -24,4 +29,11 @@ export const entities = [
|
||||
PerUserFollowingChart.hour, PerUserFollowingChart.day,
|
||||
PerUserDriveChart.hour, PerUserDriveChart.day,
|
||||
ApRequestChart.hour, ApRequestChart.day,
|
||||
|
||||
...(process.env.NODE_ENV === 'test' ? [
|
||||
TestChart.hour, TestChart.day,
|
||||
TestGroupedChart.hour, TestGroupedChart.day,
|
||||
TestUniqueChart.hour, TestUniqueChart.day,
|
||||
TestIntersectionChart.hour, TestIntersectionChart.day,
|
||||
] : []),
|
||||
];
|
||||
|
@ -1,38 +1,31 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as tmp from 'tmp';
|
||||
import * as path from 'node:path';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { IImage, convertToJpeg } from './image-processor.js';
|
||||
import * as FFmpeg from 'fluent-ffmpeg';
|
||||
import FFmpeg from 'fluent-ffmpeg';
|
||||
|
||||
export async function GenerateVideoThumbnail(path: string): Promise<IImage> {
|
||||
const [outDir, cleanup] = await new Promise<[string, any]>((res, rej) => {
|
||||
tmp.dir((e, path, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
export async function GenerateVideoThumbnail(source: string): Promise<IImage> {
|
||||
const [file, cleanup] = await createTemp();
|
||||
const parsed = path.parse(file);
|
||||
|
||||
try {
|
||||
await new Promise((res, rej) => {
|
||||
FFmpeg({
|
||||
source,
|
||||
})
|
||||
.on('end', res)
|
||||
.on('error', rej)
|
||||
.screenshot({
|
||||
folder: parsed.dir,
|
||||
filename: parsed.base,
|
||||
count: 1,
|
||||
timestamps: ['5%'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
FFmpeg({
|
||||
source: path,
|
||||
})
|
||||
.on('end', res)
|
||||
.on('error', rej)
|
||||
.screenshot({
|
||||
folder: outDir,
|
||||
filename: 'output.png',
|
||||
count: 1,
|
||||
timestamps: ['5%'],
|
||||
});
|
||||
});
|
||||
|
||||
const outPath = `${outDir}/output.png`;
|
||||
|
||||
// JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる)
|
||||
const thumbnail = await convertToJpeg(outPath, 498, 280);
|
||||
|
||||
// cleanup
|
||||
await fs.promises.unlink(outPath);
|
||||
cleanup();
|
||||
|
||||
return thumbnail;
|
||||
// JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる)
|
||||
return await convertToJpeg(498, 280);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
@ -45,29 +45,20 @@ export async function uploadFromUrl({
|
||||
// Create temp file
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
||||
// write content at URL to temp file
|
||||
await downloadUrl(url, path);
|
||||
|
||||
let driveFile: DriveFile;
|
||||
let error;
|
||||
|
||||
try {
|
||||
driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive });
|
||||
// write content at URL to temp file
|
||||
await downloadUrl(url, path);
|
||||
|
||||
const driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive });
|
||||
logger.succ(`Got: ${driveFile.id}`);
|
||||
return driveFile!;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
logger.error(`Failed to create drive file: ${e}`, {
|
||||
url: url,
|
||||
e: e,
|
||||
});
|
||||
}
|
||||
|
||||
// clean-up
|
||||
cleanup();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
} else {
|
||||
return driveFile!;
|
||||
throw e;
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { DOMWindow, JSDOM } from 'jsdom';
|
||||
import fetch from 'node-fetch';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { getJson, getHtml, getAgentByUrl } from '@/misc/fetch.js';
|
||||
import { Instance } from '@/models/entities/instance.js';
|
||||
import { Instances } from '@/models/index.js';
|
||||
@ -208,16 +209,11 @@ async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | nul
|
||||
}
|
||||
|
||||
async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
|
||||
if (doc) {
|
||||
const themeColor = doc.querySelector('meta[name="theme-color"]')?.getAttribute('content');
|
||||
const themeColor = doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') || manifest?.theme_color;
|
||||
|
||||
if (themeColor) {
|
||||
return themeColor;
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest) {
|
||||
return manifest.theme_color;
|
||||
if (themeColor) {
|
||||
const color = new tinycolor(themeColor);
|
||||
if (color.isValid()) return color.toHexString();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -312,7 +312,8 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
||||
endedPollNotificationQueue.add({
|
||||
noteId: note.id,
|
||||
}, {
|
||||
delay
|
||||
delay,
|
||||
removeOnComplete: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,11 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note,
|
||||
}
|
||||
}
|
||||
|
||||
// check visibility
|
||||
if (!await Notes.isVisibleForMe(note, user.id)) {
|
||||
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
||||
}
|
||||
|
||||
// TODO: cache
|
||||
reaction = await toDbReaction(reaction, user.host);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createSystemUser } from './create-system-user.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { renderFollowRelay } from '@/remote/activitypub/renderer/follow-relay.js';
|
||||
import { renderActivity, attachLdSignature } from '@/remote/activitypub/renderer/index.js';
|
||||
import renderUndo from '@/remote/activitypub/renderer/undo.js';
|
||||
@ -8,7 +8,7 @@ import { Users, Relays } from '@/models/index.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { Relay } from '@/models/entities/relay.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { createSystemUser } from './create-system-user.js';
|
||||
|
||||
const ACTOR_USERNAME = 'relay.actor' as const;
|
||||
|
||||
@ -88,7 +88,9 @@ export async function deliverToRelays(user: { id: User['id']; host: null; }, act
|
||||
}));
|
||||
if (relays.length === 0) return;
|
||||
|
||||
const copy = structuredClone(activity);
|
||||
// TODO
|
||||
//const copy = structuredClone(activity);
|
||||
const copy = JSON.parse(JSON.stringify(activity));
|
||||
if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||
|
||||
const signed = await attachLdSignature(copy, user);
|
||||
|
@ -1,12 +1,14 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import rndstr from 'rndstr';
|
||||
import * as assert from 'assert';
|
||||
import rndstr from 'rndstr';
|
||||
import { initDb } from '../src/db/postgre.js';
|
||||
import { initTestDb } from './utils.js';
|
||||
|
||||
describe('ActivityPub', () => {
|
||||
before(async () => {
|
||||
await initTestDb();
|
||||
//await initTestDb();
|
||||
await initDb();
|
||||
});
|
||||
|
||||
describe('Parse minimum object', () => {
|
||||
@ -57,8 +59,8 @@ describe('ActivityPub', () => {
|
||||
const note = await createNote(post.id, resolver, true);
|
||||
|
||||
assert.deepStrictEqual(note?.uri, post.id);
|
||||
assert.deepStrictEqual(note?.visibility, 'public');
|
||||
assert.deepStrictEqual(note?.text, post.content);
|
||||
assert.deepStrictEqual(note.visibility, 'public');
|
||||
assert.deepStrictEqual(note.text, post.content);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as assert from 'assert';
|
||||
import httpSignature from 'http-signature';
|
||||
import { genRsaKeyPair } from '../src/misc/gen-key-pair.js';
|
||||
import { createSignedPost, createSignedGet } from '../src/remote/activitypub/ap-request.js';
|
||||
import httpSignature from 'http-signature';
|
||||
|
||||
export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
|
||||
return {
|
||||
@ -13,7 +13,7 @@ export const buildParsedSignature = (signingString: string, signature: string, a
|
||||
signature: signature,
|
||||
},
|
||||
signingString: signingString,
|
||||
algorithm: algorithm?.toUpperCase(),
|
||||
algorithm: algorithm.toUpperCase(),
|
||||
keyId: 'KeyID', // dummy, not used for verify
|
||||
};
|
||||
};
|
||||
@ -26,7 +26,7 @@ describe('ap-request', () => {
|
||||
const activity = { a: 1 };
|
||||
const body = JSON.stringify(activity);
|
||||
const headers = {
|
||||
'User-Agent': 'UA'
|
||||
'User-Agent': 'UA',
|
||||
};
|
||||
|
||||
const req = createSignedPost({ key, url, body, additionalHeaders: headers });
|
||||
@ -42,7 +42,7 @@ describe('ap-request', () => {
|
||||
const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
|
||||
const url = 'https://example.com/outbox';
|
||||
const headers = {
|
||||
'User-Agent': 'UA'
|
||||
'User-Agent': 'UA',
|
||||
};
|
||||
|
||||
const req = createSignedGet({ key, url, additionalHeaders: headers });
|
||||
|
@ -61,40 +61,40 @@ describe('API visibility', () => {
|
||||
|
||||
const show = async (noteId: any, by: any) => {
|
||||
return await request('/notes/show', {
|
||||
noteId
|
||||
noteId,
|
||||
}, by);
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
//#region prepare
|
||||
// signup
|
||||
alice = await signup({ username: 'alice' });
|
||||
alice = await signup({ username: 'alice' });
|
||||
follower = await signup({ username: 'follower' });
|
||||
other = await signup({ username: 'other' });
|
||||
target = await signup({ username: 'target' });
|
||||
target2 = await signup({ username: 'target2' });
|
||||
other = await signup({ username: 'other' });
|
||||
target = await signup({ username: 'target' });
|
||||
target2 = await signup({ username: 'target2' });
|
||||
|
||||
// follow alice <= follower
|
||||
await request('/following/create', { userId: alice.id }, follower);
|
||||
|
||||
// normal posts
|
||||
pub = await post(alice, { text: 'x', visibility: 'public' });
|
||||
pub = await post(alice, { text: 'x', visibility: 'public' });
|
||||
home = await post(alice, { text: 'x', visibility: 'home' });
|
||||
fol = await post(alice, { text: 'x', visibility: 'followers' });
|
||||
spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] });
|
||||
fol = await post(alice, { text: 'x', visibility: 'followers' });
|
||||
spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] });
|
||||
|
||||
// replies
|
||||
tgt = await post(target, { text: 'y', visibility: 'public' });
|
||||
pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' });
|
||||
pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' });
|
||||
homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' });
|
||||
folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' });
|
||||
speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' });
|
||||
folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' });
|
||||
speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' });
|
||||
|
||||
// mentions
|
||||
pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' });
|
||||
pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' });
|
||||
homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' });
|
||||
folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' });
|
||||
speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' });
|
||||
folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' });
|
||||
speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' });
|
||||
//#endregion
|
||||
});
|
||||
|
||||
|
@ -25,7 +25,7 @@ describe('Block', () => {
|
||||
|
||||
it('Block作成', async(async () => {
|
||||
const res = await request('/blocking/create', {
|
||||
userId: bob.id
|
||||
userId: bob.id,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
|
@ -2,30 +2,21 @@ process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as lolex from '@sinonjs/fake-timers';
|
||||
import { async, initTestDb } from './utils.js';
|
||||
import TestChart from '../src/services/chart/charts/test.js';
|
||||
import TestGroupedChart from '../src/services/chart/charts/test-grouped.js';
|
||||
import TestUniqueChart from '../src/services/chart/charts/test-unique.js';
|
||||
import TestIntersectionChart from '../src/services/chart/charts/test-intersection.js';
|
||||
import * as _TestChart from '../src/services/chart/charts/entities/test.js';
|
||||
import * as _TestGroupedChart from '../src/services/chart/charts/entities/test-grouped.js';
|
||||
import * as _TestUniqueChart from '../src/services/chart/charts/entities/test-unique.js';
|
||||
import * as _TestIntersectionChart from '../src/services/chart/charts/entities/test-intersection.js';
|
||||
import { initDb } from '../src/db/postgre.js';
|
||||
|
||||
describe('Chart', () => {
|
||||
let testChart: TestChart;
|
||||
let testGroupedChart: TestGroupedChart;
|
||||
let testUniqueChart: TestUniqueChart;
|
||||
let testIntersectionChart: TestIntersectionChart;
|
||||
let clock: lolex.Clock;
|
||||
let clock: lolex.InstalledClock;
|
||||
|
||||
beforeEach(async(async () => {
|
||||
await initTestDb(false, [
|
||||
_TestChart.entity.hour, _TestChart.entity.day,
|
||||
_TestGroupedChart.entity.hour, _TestGroupedChart.entity.day,
|
||||
_TestUniqueChart.entity.hour, _TestUniqueChart.entity.day,
|
||||
_TestIntersectionChart.entity.hour, _TestIntersectionChart.entity.day,
|
||||
]);
|
||||
beforeEach(async () => {
|
||||
await initDb(true);
|
||||
|
||||
testChart = new TestChart();
|
||||
testGroupedChart = new TestGroupedChart();
|
||||
@ -33,15 +24,16 @@ describe('Chart', () => {
|
||||
testIntersectionChart = new TestIntersectionChart();
|
||||
|
||||
clock = lolex.install({
|
||||
now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0))
|
||||
now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
|
||||
shouldClearNativeTimers: true,
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(async(async () => {
|
||||
afterEach(() => {
|
||||
clock.uninstall();
|
||||
}));
|
||||
});
|
||||
|
||||
it('Can updates', async(async () => {
|
||||
it('Can updates', async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
@ -52,7 +44,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0]
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
@ -60,12 +52,12 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0]
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('Can updates (dec)', async(async () => {
|
||||
it('Can updates (dec)', async () => {
|
||||
await testChart.decrement();
|
||||
await testChart.save();
|
||||
|
||||
@ -76,7 +68,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [1, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [-1, 0, 0]
|
||||
total: [-1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
@ -84,12 +76,12 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [1, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [-1, 0, 0]
|
||||
total: [-1, 0, 0],
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('Empty chart', async(async () => {
|
||||
it('Empty chart', async () => {
|
||||
const chartHours = await testChart.getChart('hour', 3, null);
|
||||
const chartDays = await testChart.getChart('day', 3, null);
|
||||
|
||||
@ -97,7 +89,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [0, 0, 0]
|
||||
total: [0, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
@ -105,12 +97,12 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [0, 0, 0]
|
||||
total: [0, 0, 0],
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('Can updates at multiple times at same time', async(async () => {
|
||||
it('Can updates at multiple times at same time', async () => {
|
||||
await testChart.increment();
|
||||
await testChart.increment();
|
||||
await testChart.increment();
|
||||
@ -123,7 +115,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [3, 0, 0],
|
||||
total: [3, 0, 0]
|
||||
total: [3, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
@ -131,12 +123,12 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [3, 0, 0],
|
||||
total: [3, 0, 0]
|
||||
total: [3, 0, 0],
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('複数回saveされてもデータの更新は一度だけ', async(async () => {
|
||||
it('複数回saveされてもデータの更新は一度だけ', async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
await testChart.save();
|
||||
@ -149,7 +141,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0]
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
@ -157,12 +149,12 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0]
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('Can updates at different times', async(async () => {
|
||||
it('Can updates at different times', async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
@ -178,7 +170,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 1, 0],
|
||||
total: [2, 1, 0]
|
||||
total: [2, 1, 0],
|
||||
},
|
||||
});
|
||||
|
||||
@ -186,14 +178,14 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [2, 0, 0],
|
||||
total: [2, 0, 0]
|
||||
total: [2, 0, 0],
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
// 仕様上はこうなってほしいけど、実装は難しそうなのでskip
|
||||
/*
|
||||
it('Can updates at different times without save', async(async () => {
|
||||
it('Can updates at different times without save', async () => {
|
||||
await testChart.increment();
|
||||
|
||||
clock.tick('01:00:00');
|
||||
@ -219,10 +211,10 @@ describe('Chart', () => {
|
||||
total: [2, 0, 0]
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
*/
|
||||
|
||||
it('Can padding', async(async () => {
|
||||
it('Can padding', async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
@ -238,7 +230,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 1],
|
||||
total: [2, 1, 1]
|
||||
total: [2, 1, 1],
|
||||
},
|
||||
});
|
||||
|
||||
@ -246,13 +238,13 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [2, 0, 0],
|
||||
total: [2, 0, 0]
|
||||
total: [2, 0, 0],
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
// 要求された範囲にログがひとつもない場合でもパディングできる
|
||||
it('Can padding from past range', async(async () => {
|
||||
it('Can padding from past range', async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
@ -265,7 +257,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [1, 1, 1]
|
||||
total: [1, 1, 1],
|
||||
},
|
||||
});
|
||||
|
||||
@ -273,14 +265,14 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0]
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
// 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる
|
||||
// Issue #3190
|
||||
it('Can padding from past range 2', async(async () => {
|
||||
it('Can padding from past range 2', async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
@ -296,7 +288,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [2, 1, 1]
|
||||
total: [2, 1, 1],
|
||||
},
|
||||
});
|
||||
|
||||
@ -304,12 +296,12 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [2, 0, 0],
|
||||
total: [2, 0, 0]
|
||||
total: [2, 0, 0],
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('Can specify offset', async(async () => {
|
||||
it('Can specify offset', async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
@ -325,7 +317,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0]
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
@ -333,12 +325,12 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [2, 0, 0],
|
||||
total: [2, 0, 0]
|
||||
total: [2, 0, 0],
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('Can specify offset (floor time)', async(async () => {
|
||||
it('Can specify offset (floor time)', async () => {
|
||||
clock.tick('00:30:00');
|
||||
|
||||
await testChart.increment();
|
||||
@ -356,7 +348,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0]
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
@ -364,13 +356,13 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [2, 0, 0],
|
||||
total: [2, 0, 0]
|
||||
total: [2, 0, 0],
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('Grouped', () => {
|
||||
it('Can updates', async(async () => {
|
||||
it('Can updates', async () => {
|
||||
await testGroupedChart.increment('alice');
|
||||
await testGroupedChart.save();
|
||||
|
||||
@ -383,7 +375,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0]
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
@ -391,7 +383,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0]
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
@ -399,7 +391,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [0, 0, 0]
|
||||
total: [0, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
@ -407,14 +399,14 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [0, 0, 0]
|
||||
total: [0, 0, 0],
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unique increment', () => {
|
||||
it('Can updates', async(async () => {
|
||||
it('Can updates', async () => {
|
||||
await testUniqueChart.uniqueIncrement('alice');
|
||||
await testUniqueChart.uniqueIncrement('alice');
|
||||
await testUniqueChart.uniqueIncrement('bob');
|
||||
@ -430,10 +422,10 @@ describe('Chart', () => {
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: [2, 0, 0],
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('Intersection', () => {
|
||||
it('条件が満たされていない場合はカウントされない', async(async () => {
|
||||
it('条件が満たされていない場合はカウントされない', async () => {
|
||||
await testIntersectionChart.addA('alice');
|
||||
await testIntersectionChart.addA('bob');
|
||||
await testIntersectionChart.addB('carol');
|
||||
@ -453,9 +445,9 @@ describe('Chart', () => {
|
||||
b: [1, 0, 0],
|
||||
aAndB: [0, 0, 0],
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('条件が満たされている場合にカウントされる', async(async () => {
|
||||
it('条件が満たされている場合にカウントされる', async () => {
|
||||
await testIntersectionChart.addA('alice');
|
||||
await testIntersectionChart.addA('bob');
|
||||
await testIntersectionChart.addB('carol');
|
||||
@ -476,12 +468,12 @@ describe('Chart', () => {
|
||||
b: [2, 0, 0],
|
||||
aAndB: [1, 0, 0],
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resync', () => {
|
||||
it('Can resync', async(async () => {
|
||||
it('Can resync', async () => {
|
||||
testChart.total = 1;
|
||||
|
||||
await testChart.resync();
|
||||
@ -493,7 +485,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [1, 0, 0]
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
@ -501,12 +493,12 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [1, 0, 0]
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('Can resync (2)', async(async () => {
|
||||
it('Can resync (2)', async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
@ -523,7 +515,7 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 1, 0],
|
||||
total: [100, 1, 0]
|
||||
total: [100, 1, 0],
|
||||
},
|
||||
});
|
||||
|
||||
@ -531,9 +523,9 @@ describe('Chart', () => {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [100, 0, 0]
|
||||
total: [100, 0, 0],
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as assert from 'assert';
|
||||
|
||||
import { extractMentions } from '../src/misc/extract-mentions.js';
|
||||
import { parse } from 'mfm-js';
|
||||
import { extractMentions } from '../src/misc/extract-mentions.js';
|
||||
|
||||
describe('Extract mentions', () => {
|
||||
it('simple', () => {
|
||||
@ -10,15 +10,15 @@ describe('Extract mentions', () => {
|
||||
assert.deepStrictEqual(mentions, [{
|
||||
username: 'foo',
|
||||
acct: '@foo',
|
||||
host: null
|
||||
host: null,
|
||||
}, {
|
||||
username: 'bar',
|
||||
acct: '@bar',
|
||||
host: null
|
||||
host: null,
|
||||
}, {
|
||||
username: 'baz',
|
||||
acct: '@baz',
|
||||
host: null
|
||||
host: null,
|
||||
}]);
|
||||
});
|
||||
|
||||
@ -28,15 +28,15 @@ describe('Extract mentions', () => {
|
||||
assert.deepStrictEqual(mentions, [{
|
||||
username: 'foo',
|
||||
acct: '@foo',
|
||||
host: null
|
||||
host: null,
|
||||
}, {
|
||||
username: 'bar',
|
||||
acct: '@bar',
|
||||
host: null
|
||||
host: null,
|
||||
}, {
|
||||
username: 'baz',
|
||||
acct: '@baz',
|
||||
host: null
|
||||
host: null,
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
@ -2,8 +2,8 @@ process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils.js';
|
||||
import * as openapi from '@redocly/openapi-core';
|
||||
import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils.js';
|
||||
|
||||
// Request Accept
|
||||
const ONLY_AP = 'application/activity+json';
|
||||
@ -26,7 +26,7 @@ describe('Fetch resource', () => {
|
||||
p = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
alicesPost = await post(alice, {
|
||||
text: 'test'
|
||||
text: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
@ -70,7 +70,7 @@ describe('Fetch resource', () => {
|
||||
const config = await openapi.loadConfig();
|
||||
const result = await openapi.bundle({
|
||||
config,
|
||||
ref: `http://localhost:${port}/api.json`
|
||||
ref: `http://localhost:${port}/api.json`,
|
||||
});
|
||||
|
||||
for (const problem of result.problems) {
|
||||
|
@ -18,7 +18,7 @@ describe('Get file info', () => {
|
||||
md5: 'd41d8cd98f00b204e9800998ecf8427e',
|
||||
type: {
|
||||
mime: 'application/octet-stream',
|
||||
ext: null
|
||||
ext: null,
|
||||
},
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
@ -36,7 +36,7 @@ describe('Get file info', () => {
|
||||
md5: '091b3f259662aa31e2ffef4519951168',
|
||||
type: {
|
||||
mime: 'image/jpeg',
|
||||
ext: 'jpg'
|
||||
ext: 'jpg',
|
||||
},
|
||||
width: 512,
|
||||
height: 512,
|
||||
@ -54,7 +54,7 @@ describe('Get file info', () => {
|
||||
md5: '08189c607bea3b952704676bb3c979e0',
|
||||
type: {
|
||||
mime: 'image/apng',
|
||||
ext: 'apng'
|
||||
ext: 'apng',
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
@ -72,7 +72,7 @@ describe('Get file info', () => {
|
||||
md5: '32c47a11555675d9267aee1a86571e7e',
|
||||
type: {
|
||||
mime: 'image/gif',
|
||||
ext: 'gif'
|
||||
ext: 'gif',
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
@ -90,7 +90,7 @@ describe('Get file info', () => {
|
||||
md5: 'f73535c3e1e27508885b69b10cf6e991',
|
||||
type: {
|
||||
mime: 'image/png',
|
||||
ext: 'png'
|
||||
ext: 'png',
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
@ -108,7 +108,7 @@ describe('Get file info', () => {
|
||||
md5: 'b6f52b4b021e7b92cdd04509c7267965',
|
||||
type: {
|
||||
mime: 'image/svg+xml',
|
||||
ext: 'svg'
|
||||
ext: 'svg',
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
@ -127,7 +127,7 @@ describe('Get file info', () => {
|
||||
md5: '4b7a346cde9ccbeb267e812567e33397',
|
||||
type: {
|
||||
mime: 'image/svg+xml',
|
||||
ext: 'svg'
|
||||
ext: 'svg',
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
@ -145,7 +145,7 @@ describe('Get file info', () => {
|
||||
md5: '268c5dde99e17cf8fe09f1ab3f97df56',
|
||||
type: {
|
||||
mime: 'application/octet-stream', // do not treat as image
|
||||
ext: null
|
||||
ext: null,
|
||||
},
|
||||
width: 25000,
|
||||
height: 25000,
|
||||
@ -163,7 +163,7 @@ describe('Get file info', () => {
|
||||
md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
|
||||
type: {
|
||||
mime: 'image/jpeg',
|
||||
ext: 'jpg'
|
||||
ext: 'jpg',
|
||||
},
|
||||
width: 512,
|
||||
height: 256,
|
||||
|
@ -11,7 +11,7 @@ export class MockResolver extends Resolver {
|
||||
public async _register(uri: string, content: string | Record<string, any>, type = 'application/activity+json') {
|
||||
this._rs.set(uri, {
|
||||
type,
|
||||
content: typeof content === 'string' ? content : JSON.stringify(content)
|
||||
content: typeof content === 'string' ? content : JSON.stringify(content),
|
||||
});
|
||||
}
|
||||
|
||||
@ -22,9 +22,9 @@ export class MockResolver extends Resolver {
|
||||
|
||||
if (!r) {
|
||||
throw {
|
||||
name: `StatusError`,
|
||||
name: 'StatusError',
|
||||
statusCode: 404,
|
||||
message: `Not registed for mock`
|
||||
message: 'Not registed for mock',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ describe('Mute', () => {
|
||||
|
||||
it('ミュート作成', async(async () => {
|
||||
const res = await request('/mute/create', {
|
||||
userId: carol.id
|
||||
userId: carol.id,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 204);
|
||||
@ -117,7 +117,7 @@ describe('Mute', () => {
|
||||
const aliceNote = await post(alice);
|
||||
const carolNote = await post(carol);
|
||||
const bobNote = await post(bob, {
|
||||
renoteId: carolNote.id
|
||||
renoteId: carolNote.id,
|
||||
});
|
||||
|
||||
const res = await request('/notes/local-timeline', {}, alice);
|
||||
|
@ -2,8 +2,8 @@ process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import { async, signup, request, post, uploadFile, startServer, shutdownServer, initTestDb } from './utils.js';
|
||||
import { Note } from '../src/models/entities/note.js';
|
||||
import { async, signup, request, post, uploadFile, startServer, shutdownServer, initTestDb } from './utils.js';
|
||||
|
||||
describe('Note', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
@ -26,7 +26,7 @@ describe('Note', () => {
|
||||
|
||||
it('投稿できる', async(async () => {
|
||||
const post = {
|
||||
text: 'test'
|
||||
text: 'test',
|
||||
};
|
||||
|
||||
const res = await request('/notes/create', post, alice);
|
||||
@ -40,7 +40,7 @@ describe('Note', () => {
|
||||
const file = await uploadFile(alice);
|
||||
|
||||
const res = await request('/notes/create', {
|
||||
fileIds: [file.id]
|
||||
fileIds: [file.id],
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
@ -53,7 +53,7 @@ describe('Note', () => {
|
||||
|
||||
const res = await request('/notes/create', {
|
||||
text: 'test',
|
||||
fileIds: [file.id]
|
||||
fileIds: [file.id],
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
@ -64,7 +64,7 @@ describe('Note', () => {
|
||||
it('存在しないファイルは無視', async(async () => {
|
||||
const res = await request('/notes/create', {
|
||||
text: 'test',
|
||||
fileIds: ['000000000000000000000000']
|
||||
fileIds: ['000000000000000000000000'],
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
@ -74,19 +74,19 @@ describe('Note', () => {
|
||||
|
||||
it('不正なファイルIDで怒られる', async(async () => {
|
||||
const res = await request('/notes/create', {
|
||||
fileIds: ['kyoppie']
|
||||
fileIds: ['kyoppie'],
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('返信できる', async(async () => {
|
||||
const bobPost = await post(bob, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
|
||||
const alicePost = {
|
||||
text: 'bar',
|
||||
replyId: bobPost.id
|
||||
replyId: bobPost.id,
|
||||
};
|
||||
|
||||
const res = await request('/notes/create', alicePost, alice);
|
||||
@ -100,11 +100,11 @@ describe('Note', () => {
|
||||
|
||||
it('renoteできる', async(async () => {
|
||||
const bobPost = await post(bob, {
|
||||
text: 'test'
|
||||
text: 'test',
|
||||
});
|
||||
|
||||
const alicePost = {
|
||||
renoteId: bobPost.id
|
||||
renoteId: bobPost.id,
|
||||
};
|
||||
|
||||
const res = await request('/notes/create', alicePost, alice);
|
||||
@ -117,12 +117,12 @@ describe('Note', () => {
|
||||
|
||||
it('引用renoteできる', async(async () => {
|
||||
const bobPost = await post(bob, {
|
||||
text: 'test'
|
||||
text: 'test',
|
||||
});
|
||||
|
||||
const alicePost = {
|
||||
text: 'test',
|
||||
renoteId: bobPost.id
|
||||
renoteId: bobPost.id,
|
||||
};
|
||||
|
||||
const res = await request('/notes/create', alicePost, alice);
|
||||
@ -136,7 +136,7 @@ describe('Note', () => {
|
||||
|
||||
it('文字数ぎりぎりで怒られない', async(async () => {
|
||||
const post = {
|
||||
text: '!'.repeat(500)
|
||||
text: '!'.repeat(500),
|
||||
};
|
||||
const res = await request('/notes/create', post, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
@ -144,7 +144,7 @@ describe('Note', () => {
|
||||
|
||||
it('文字数オーバーで怒られる', async(async () => {
|
||||
const post = {
|
||||
text: '!'.repeat(501)
|
||||
text: '!'.repeat(501),
|
||||
};
|
||||
const res = await request('/notes/create', post, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
@ -153,7 +153,7 @@ describe('Note', () => {
|
||||
it('存在しないリプライ先で怒られる', async(async () => {
|
||||
const post = {
|
||||
text: 'test',
|
||||
replyId: '000000000000000000000000'
|
||||
replyId: '000000000000000000000000',
|
||||
};
|
||||
const res = await request('/notes/create', post, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
@ -161,7 +161,7 @@ describe('Note', () => {
|
||||
|
||||
it('存在しないrenote対象で怒られる', async(async () => {
|
||||
const post = {
|
||||
renoteId: '000000000000000000000000'
|
||||
renoteId: '000000000000000000000000',
|
||||
};
|
||||
const res = await request('/notes/create', post, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
@ -170,7 +170,7 @@ describe('Note', () => {
|
||||
it('不正なリプライ先IDで怒られる', async(async () => {
|
||||
const post = {
|
||||
text: 'test',
|
||||
replyId: 'foo'
|
||||
replyId: 'foo',
|
||||
};
|
||||
const res = await request('/notes/create', post, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
@ -178,7 +178,7 @@ describe('Note', () => {
|
||||
|
||||
it('不正なrenote対象IDで怒られる', async(async () => {
|
||||
const post = {
|
||||
renoteId: 'foo'
|
||||
renoteId: 'foo',
|
||||
};
|
||||
const res = await request('/notes/create', post, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
@ -186,7 +186,7 @@ describe('Note', () => {
|
||||
|
||||
it('存在しないユーザーにメンションできる', async(async () => {
|
||||
const post = {
|
||||
text: '@ghost yo'
|
||||
text: '@ghost yo',
|
||||
};
|
||||
|
||||
const res = await request('/notes/create', post, alice);
|
||||
@ -198,7 +198,7 @@ describe('Note', () => {
|
||||
|
||||
it('同じユーザーに複数メンションしても内部的にまとめられる', async(async () => {
|
||||
const post = {
|
||||
text: '@bob @bob @bob yo'
|
||||
text: '@bob @bob @bob yo',
|
||||
};
|
||||
|
||||
const res = await request('/notes/create', post, alice);
|
||||
@ -216,8 +216,8 @@ describe('Note', () => {
|
||||
const res = await request('/notes/create', {
|
||||
text: 'test',
|
||||
poll: {
|
||||
choices: ['foo', 'bar']
|
||||
}
|
||||
choices: ['foo', 'bar'],
|
||||
},
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
@ -227,7 +227,7 @@ describe('Note', () => {
|
||||
|
||||
it('投票の選択肢が無くて怒られる', async(async () => {
|
||||
const res = await request('/notes/create', {
|
||||
poll: {}
|
||||
poll: {},
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
@ -235,8 +235,8 @@ describe('Note', () => {
|
||||
it('投票の選択肢が無くて怒られる (空の配列)', async(async () => {
|
||||
const res = await request('/notes/create', {
|
||||
poll: {
|
||||
choices: []
|
||||
}
|
||||
choices: [],
|
||||
},
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
@ -244,8 +244,8 @@ describe('Note', () => {
|
||||
it('投票の選択肢が1つで怒られる', async(async () => {
|
||||
const res = await request('/notes/create', {
|
||||
poll: {
|
||||
choices: ['Strawberry Pasta']
|
||||
}
|
||||
choices: ['Strawberry Pasta'],
|
||||
},
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
@ -254,13 +254,13 @@ describe('Note', () => {
|
||||
const { body } = await request('/notes/create', {
|
||||
text: 'test',
|
||||
poll: {
|
||||
choices: ['sakura', 'izumi', 'ako']
|
||||
}
|
||||
choices: ['sakura', 'izumi', 'ako'],
|
||||
},
|
||||
}, alice);
|
||||
|
||||
const res = await request('/notes/polls/vote', {
|
||||
noteId: body.createdNote.id,
|
||||
choice: 1
|
||||
choice: 1,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 204);
|
||||
@ -270,18 +270,18 @@ describe('Note', () => {
|
||||
const { body } = await request('/notes/create', {
|
||||
text: 'test',
|
||||
poll: {
|
||||
choices: ['sakura', 'izumi', 'ako']
|
||||
}
|
||||
choices: ['sakura', 'izumi', 'ako'],
|
||||
},
|
||||
}, alice);
|
||||
|
||||
await request('/notes/polls/vote', {
|
||||
noteId: body.createdNote.id,
|
||||
choice: 0
|
||||
choice: 0,
|
||||
}, alice);
|
||||
|
||||
const res = await request('/notes/polls/vote', {
|
||||
noteId: body.createdNote.id,
|
||||
choice: 2
|
||||
choice: 2,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
@ -292,23 +292,23 @@ describe('Note', () => {
|
||||
text: 'test',
|
||||
poll: {
|
||||
choices: ['sakura', 'izumi', 'ako'],
|
||||
multiple: true
|
||||
}
|
||||
multiple: true,
|
||||
},
|
||||
}, alice);
|
||||
|
||||
await request('/notes/polls/vote', {
|
||||
noteId: body.createdNote.id,
|
||||
choice: 0
|
||||
choice: 0,
|
||||
}, alice);
|
||||
|
||||
await request('/notes/polls/vote', {
|
||||
noteId: body.createdNote.id,
|
||||
choice: 1
|
||||
choice: 1,
|
||||
}, alice);
|
||||
|
||||
const res = await request('/notes/polls/vote', {
|
||||
noteId: body.createdNote.id,
|
||||
choice: 2
|
||||
choice: 2,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 204);
|
||||
@ -319,15 +319,15 @@ describe('Note', () => {
|
||||
text: 'test',
|
||||
poll: {
|
||||
choices: ['sakura', 'izumi', 'ako'],
|
||||
expiredAfter: 1
|
||||
}
|
||||
expiredAfter: 1,
|
||||
},
|
||||
}, alice);
|
||||
|
||||
await new Promise(x => setTimeout(x, 2));
|
||||
|
||||
const res = await request('/notes/polls/vote', {
|
||||
noteId: body.createdNote.id,
|
||||
choice: 1
|
||||
choice: 1,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
@ -341,11 +341,11 @@ describe('Note', () => {
|
||||
}, alice);
|
||||
const replyOneRes = await request('/notes/create', {
|
||||
text: 'reply one',
|
||||
replyId: mainNoteRes.body.createdNote.id
|
||||
replyId: mainNoteRes.body.createdNote.id,
|
||||
}, alice);
|
||||
const replyTwoRes = await request('/notes/create', {
|
||||
text: 'reply two',
|
||||
replyId: mainNoteRes.body.createdNote.id
|
||||
replyId: mainNoteRes.body.createdNote.id,
|
||||
}, alice);
|
||||
|
||||
const deleteOneRes = await request('/notes/delete', {
|
||||
@ -353,7 +353,7 @@ describe('Note', () => {
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(deleteOneRes.status, 204);
|
||||
let mainNote = await Notes.findOne({id: mainNoteRes.body.createdNote.id});
|
||||
let mainNote = await Notes.findOne({ id: mainNoteRes.body.createdNote.id });
|
||||
assert.strictEqual(mainNote.repliesCount, 1);
|
||||
|
||||
const deleteTwoRes = await request('/notes/delete', {
|
||||
@ -361,7 +361,7 @@ describe('Note', () => {
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(deleteTwoRes.status, 204);
|
||||
mainNote = await Notes.findOne({id: mainNoteRes.body.createdNote.id});
|
||||
mainNote = await Notes.findOne({ id: mainNoteRes.body.createdNote.id });
|
||||
assert.strictEqual(mainNote.repliesCount, 0);
|
||||
}));
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ describe('url', () => {
|
||||
const s = query({
|
||||
foo: 'ふぅ',
|
||||
bar: 'b a r',
|
||||
baz: undefined
|
||||
baz: undefined,
|
||||
});
|
||||
assert.deepStrictEqual(s, 'foo=%E3%81%B5%E3%81%85&bar=b%20a%20r');
|
||||
});
|
||||
|
@ -2,8 +2,8 @@ process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import { connectStream, signup, request, post, startServer, shutdownServer, initTestDb } from './utils.js';
|
||||
import { Following } from '../src/models/entities/following.js';
|
||||
import { connectStream, signup, request, post, startServer, shutdownServer, initTestDb } from './utils.js';
|
||||
|
||||
describe('Streaming', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
@ -30,7 +30,7 @@ describe('Streaming', () => {
|
||||
followerSharedInbox: null,
|
||||
followeeHost: followee.host,
|
||||
followeeInbox: null,
|
||||
followeeSharedInbox: null
|
||||
followeeSharedInbox: null,
|
||||
});
|
||||
};
|
||||
|
||||
@ -47,7 +47,7 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
post(alice, {
|
||||
text: 'foo @bob bar'
|
||||
text: 'foo @bob bar',
|
||||
});
|
||||
}));
|
||||
|
||||
@ -55,7 +55,7 @@ describe('Streaming', () => {
|
||||
const alice = await signup({ username: 'alice' });
|
||||
const bob = await signup({ username: 'bob' });
|
||||
const bobNote = await post(bob, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
|
||||
const ws = await connectStream(bob, 'main', ({ type, body }) => {
|
||||
@ -67,14 +67,14 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
post(alice, {
|
||||
renoteId: bobNote.id
|
||||
renoteId: bobNote.id,
|
||||
});
|
||||
}));
|
||||
|
||||
describe('Home Timeline', () => {
|
||||
it('自分の投稿が流れる', () => new Promise(async done => {
|
||||
const post = {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
};
|
||||
|
||||
const me = await signup();
|
||||
@ -96,7 +96,7 @@ describe('Streaming', () => {
|
||||
|
||||
// Alice が Bob をフォロー
|
||||
await request('/following/create', {
|
||||
userId: bob.id
|
||||
userId: bob.id,
|
||||
}, alice);
|
||||
|
||||
const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
|
||||
@ -108,7 +108,7 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
post(bob, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
}));
|
||||
|
||||
@ -125,7 +125,7 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
post(bob, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -141,7 +141,7 @@ describe('Streaming', () => {
|
||||
|
||||
// Alice が Bob をフォロー
|
||||
await request('/following/create', {
|
||||
userId: bob.id
|
||||
userId: bob.id,
|
||||
}, alice);
|
||||
|
||||
const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
|
||||
@ -157,7 +157,7 @@ describe('Streaming', () => {
|
||||
post(bob, {
|
||||
text: 'foo',
|
||||
visibility: 'specified',
|
||||
visibleUserIds: [alice.id]
|
||||
visibleUserIds: [alice.id],
|
||||
});
|
||||
}));
|
||||
|
||||
@ -168,7 +168,7 @@ describe('Streaming', () => {
|
||||
|
||||
// Alice が Bob をフォロー
|
||||
await request('/following/create', {
|
||||
userId: bob.id
|
||||
userId: bob.id,
|
||||
}, alice);
|
||||
|
||||
let fired = false;
|
||||
@ -183,7 +183,7 @@ describe('Streaming', () => {
|
||||
post(bob, {
|
||||
text: 'foo',
|
||||
visibility: 'specified',
|
||||
visibleUserIds: [carol.id]
|
||||
visibleUserIds: [carol.id],
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -207,7 +207,7 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
}));
|
||||
|
||||
@ -224,7 +224,7 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
post(bob, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
}));
|
||||
|
||||
@ -241,7 +241,7 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
post(bob, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -257,7 +257,7 @@ describe('Streaming', () => {
|
||||
|
||||
// Alice が Bob をフォロー
|
||||
await request('/following/create', {
|
||||
userId: bob.id
|
||||
userId: bob.id,
|
||||
}, alice);
|
||||
|
||||
let fired = false;
|
||||
@ -269,7 +269,7 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
post(bob, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -294,7 +294,7 @@ describe('Streaming', () => {
|
||||
// ホーム指定
|
||||
post(bob, {
|
||||
text: 'foo',
|
||||
visibility: 'home'
|
||||
visibility: 'home',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -310,7 +310,7 @@ describe('Streaming', () => {
|
||||
|
||||
// Alice が Bob をフォロー
|
||||
await request('/following/create', {
|
||||
userId: bob.id
|
||||
userId: bob.id,
|
||||
}, alice);
|
||||
|
||||
let fired = false;
|
||||
@ -325,7 +325,7 @@ describe('Streaming', () => {
|
||||
post(bob, {
|
||||
text: 'foo',
|
||||
visibility: 'specified',
|
||||
visibleUserIds: [alice.id]
|
||||
visibleUserIds: [alice.id],
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -350,7 +350,7 @@ describe('Streaming', () => {
|
||||
// フォロワー宛て投稿
|
||||
post(bob, {
|
||||
text: 'foo',
|
||||
visibility: 'followers'
|
||||
visibility: 'followers',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -374,7 +374,7 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
}));
|
||||
|
||||
@ -391,7 +391,7 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
post(bob, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
}));
|
||||
|
||||
@ -411,7 +411,7 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
post(bob, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
}));
|
||||
|
||||
@ -428,7 +428,7 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
post(bob, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -444,7 +444,7 @@ describe('Streaming', () => {
|
||||
|
||||
// Alice が Bob をフォロー
|
||||
await request('/following/create', {
|
||||
userId: bob.id
|
||||
userId: bob.id,
|
||||
}, alice);
|
||||
|
||||
const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
|
||||
@ -460,7 +460,7 @@ describe('Streaming', () => {
|
||||
post(bob, {
|
||||
text: 'foo',
|
||||
visibility: 'specified',
|
||||
visibleUserIds: [alice.id]
|
||||
visibleUserIds: [alice.id],
|
||||
});
|
||||
}));
|
||||
|
||||
@ -470,7 +470,7 @@ describe('Streaming', () => {
|
||||
|
||||
// Alice が Bob をフォロー
|
||||
await request('/following/create', {
|
||||
userId: bob.id
|
||||
userId: bob.id,
|
||||
}, alice);
|
||||
|
||||
const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
|
||||
@ -485,7 +485,7 @@ describe('Streaming', () => {
|
||||
// ホーム投稿
|
||||
post(bob, {
|
||||
text: 'foo',
|
||||
visibility: 'home'
|
||||
visibility: 'home',
|
||||
});
|
||||
}));
|
||||
|
||||
@ -504,7 +504,7 @@ describe('Streaming', () => {
|
||||
// ホーム投稿
|
||||
post(bob, {
|
||||
text: 'foo',
|
||||
visibility: 'home'
|
||||
visibility: 'home',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -529,7 +529,7 @@ describe('Streaming', () => {
|
||||
// フォロワー宛て投稿
|
||||
post(bob, {
|
||||
text: 'foo',
|
||||
visibility: 'followers'
|
||||
visibility: 'followers',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -554,7 +554,7 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
post(bob, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
}));
|
||||
|
||||
@ -571,7 +571,7 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
post(bob, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
}));
|
||||
|
||||
@ -590,7 +590,7 @@ describe('Streaming', () => {
|
||||
// ホーム投稿
|
||||
post(bob, {
|
||||
text: 'foo',
|
||||
visibility: 'home'
|
||||
visibility: 'home',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -608,13 +608,13 @@ describe('Streaming', () => {
|
||||
|
||||
// リスト作成
|
||||
const list = await request('/users/lists/create', {
|
||||
name: 'my list'
|
||||
name: 'my list',
|
||||
}, alice).then(x => x.body);
|
||||
|
||||
// Alice が Bob をリスイン
|
||||
await request('/users/lists/push', {
|
||||
listId: list.id,
|
||||
userId: bob.id
|
||||
userId: bob.id,
|
||||
}, alice);
|
||||
|
||||
const ws = await connectStream(alice, 'userList', ({ type, body }) => {
|
||||
@ -624,11 +624,11 @@ describe('Streaming', () => {
|
||||
done();
|
||||
}
|
||||
}, {
|
||||
listId: list.id
|
||||
listId: list.id,
|
||||
});
|
||||
|
||||
post(bob, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
}));
|
||||
|
||||
@ -638,7 +638,7 @@ describe('Streaming', () => {
|
||||
|
||||
// リスト作成
|
||||
const list = await request('/users/lists/create', {
|
||||
name: 'my list'
|
||||
name: 'my list',
|
||||
}, alice).then(x => x.body);
|
||||
|
||||
let fired = false;
|
||||
@ -648,11 +648,11 @@ describe('Streaming', () => {
|
||||
fired = true;
|
||||
}
|
||||
}, {
|
||||
listId: list.id
|
||||
listId: list.id,
|
||||
});
|
||||
|
||||
post(bob, {
|
||||
text: 'foo'
|
||||
text: 'foo',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -669,13 +669,13 @@ describe('Streaming', () => {
|
||||
|
||||
// リスト作成
|
||||
const list = await request('/users/lists/create', {
|
||||
name: 'my list'
|
||||
name: 'my list',
|
||||
}, alice).then(x => x.body);
|
||||
|
||||
// Alice が Bob をリスイン
|
||||
await request('/users/lists/push', {
|
||||
listId: list.id,
|
||||
userId: bob.id
|
||||
userId: bob.id,
|
||||
}, alice);
|
||||
|
||||
const ws = await connectStream(alice, 'userList', ({ type, body }) => {
|
||||
@ -686,14 +686,14 @@ describe('Streaming', () => {
|
||||
done();
|
||||
}
|
||||
}, {
|
||||
listId: list.id
|
||||
listId: list.id,
|
||||
});
|
||||
|
||||
// Bob が Alice 宛てのダイレクト投稿
|
||||
post(bob, {
|
||||
text: 'foo',
|
||||
visibility: 'specified',
|
||||
visibleUserIds: [alice.id]
|
||||
visibleUserIds: [alice.id],
|
||||
});
|
||||
}));
|
||||
|
||||
@ -704,13 +704,13 @@ describe('Streaming', () => {
|
||||
|
||||
// リスト作成
|
||||
const list = await request('/users/lists/create', {
|
||||
name: 'my list'
|
||||
name: 'my list',
|
||||
}, alice).then(x => x.body);
|
||||
|
||||
// Alice が Bob をリスイン
|
||||
await request('/users/lists/push', {
|
||||
listId: list.id,
|
||||
userId: bob.id
|
||||
userId: bob.id,
|
||||
}, alice);
|
||||
|
||||
let fired = false;
|
||||
@ -720,13 +720,13 @@ describe('Streaming', () => {
|
||||
fired = true;
|
||||
}
|
||||
}, {
|
||||
listId: list.id
|
||||
listId: list.id,
|
||||
});
|
||||
|
||||
// フォロワー宛て投稿
|
||||
post(bob, {
|
||||
text: 'foo',
|
||||
visibility: 'followers'
|
||||
visibility: 'followers',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -749,12 +749,12 @@ describe('Streaming', () => {
|
||||
}
|
||||
}, {
|
||||
q: [
|
||||
['foo']
|
||||
]
|
||||
['foo'],
|
||||
],
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: '#foo'
|
||||
text: '#foo',
|
||||
});
|
||||
}));
|
||||
|
||||
@ -773,20 +773,20 @@ describe('Streaming', () => {
|
||||
}
|
||||
}, {
|
||||
q: [
|
||||
['foo', 'bar']
|
||||
]
|
||||
['foo', 'bar'],
|
||||
],
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: '#foo'
|
||||
text: '#foo',
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: '#bar'
|
||||
text: '#bar',
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: '#foo #bar'
|
||||
text: '#foo #bar',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -816,24 +816,24 @@ describe('Streaming', () => {
|
||||
}, {
|
||||
q: [
|
||||
['foo'],
|
||||
['bar']
|
||||
]
|
||||
['bar'],
|
||||
],
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: '#foo'
|
||||
text: '#foo',
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: '#bar'
|
||||
text: '#bar',
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: '#foo #bar'
|
||||
text: '#foo #bar',
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: '#piyo'
|
||||
text: '#piyo',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@ -866,28 +866,28 @@ describe('Streaming', () => {
|
||||
}, {
|
||||
q: [
|
||||
['foo', 'bar'],
|
||||
['piyo']
|
||||
]
|
||||
['piyo'],
|
||||
],
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: '#foo'
|
||||
text: '#foo',
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: '#bar'
|
||||
text: '#bar',
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: '#foo #bar'
|
||||
text: '#foo #bar',
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: '#piyo'
|
||||
text: '#piyo',
|
||||
});
|
||||
|
||||
post(me, {
|
||||
text: '#waaa'
|
||||
text: '#waaa',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
|
@ -23,13 +23,13 @@ describe('users/notes', () => {
|
||||
const jpg = await uploadFile(alice, _dirname + '/resources/Lenna.jpg');
|
||||
const png = await uploadFile(alice, _dirname + '/resources/Lenna.png');
|
||||
jpgNote = await post(alice, {
|
||||
fileIds: [jpg.id]
|
||||
fileIds: [jpg.id],
|
||||
});
|
||||
pngNote = await post(alice, {
|
||||
fileIds: [png.id]
|
||||
fileIds: [png.id],
|
||||
});
|
||||
jpgPngNote = await post(alice, {
|
||||
fileIds: [jpg.id, png.id]
|
||||
fileIds: [jpg.id, png.id],
|
||||
});
|
||||
});
|
||||
|
||||
@ -40,7 +40,7 @@ describe('users/notes', () => {
|
||||
it('ファイルタイプ指定 (jpg)', async(async () => {
|
||||
const res = await request('/users/notes', {
|
||||
userId: alice.id,
|
||||
fileType: ['image/jpeg']
|
||||
fileType: ['image/jpeg'],
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
@ -53,7 +53,7 @@ describe('users/notes', () => {
|
||||
it('ファイルタイプ指定 (jpg or png)', async(async () => {
|
||||
const res = await request('/users/notes', {
|
||||
userId: alice.id,
|
||||
fileType: ['image/jpeg', 'image/png']
|
||||
fileType: ['image/jpeg', 'image/png'],
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
|
@ -1,14 +1,15 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import * as childProcess from 'child_process';
|
||||
import * as http from 'node:http';
|
||||
import { SIGKILL } from 'constants';
|
||||
import * as WebSocket from 'ws';
|
||||
import * as misskey from 'misskey-js';
|
||||
import fetch from 'node-fetch';
|
||||
import FormData from 'form-data';
|
||||
import * as childProcess from 'child_process';
|
||||
import * as http from 'node:http';
|
||||
import { DataSource } from 'typeorm';
|
||||
import loadConfig from '../src/config/load.js';
|
||||
import { SIGKILL } from 'constants';
|
||||
import { entities } from '../src/db/postgre.js';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
@ -27,29 +28,29 @@ export const async = (fn: Function) => (done: Function) => {
|
||||
|
||||
export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
|
||||
const auth = me ? {
|
||||
i: me.token
|
||||
i: me.token,
|
||||
} : {};
|
||||
|
||||
const res = await fetch(`http://localhost:${port}/api${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(Object.assign(auth, params))
|
||||
body: JSON.stringify(Object.assign(auth, params)),
|
||||
});
|
||||
|
||||
const status = res.status;
|
||||
const body = res.status !== 204 ? await res.json().catch() : null;
|
||||
|
||||
return {
|
||||
body, status
|
||||
body, status,
|
||||
};
|
||||
};
|
||||
|
||||
export const signup = async (params?: any): Promise<any> => {
|
||||
const q = Object.assign({
|
||||
username: 'test',
|
||||
password: 'test'
|
||||
password: 'test',
|
||||
}, params);
|
||||
|
||||
const res = await request('/signup', q);
|
||||
@ -59,7 +60,7 @@ export const signup = async (params?: any): Promise<any> => {
|
||||
|
||||
export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
|
||||
const q = Object.assign({
|
||||
text: 'test'
|
||||
text: 'test',
|
||||
}, params);
|
||||
|
||||
const res = await request('/notes/create', q, user);
|
||||
@ -70,26 +71,26 @@ export const post = async (user: any, params?: misskey.Endpoints['notes/create']
|
||||
export const react = async (user: any, note: any, reaction: string): Promise<any> => {
|
||||
await request('/notes/reactions/create', {
|
||||
noteId: note.id,
|
||||
reaction: reaction
|
||||
reaction: reaction,
|
||||
}, user);
|
||||
};
|
||||
|
||||
export const uploadFile = (user: any, path?: string): Promise<any> => {
|
||||
const formData = new FormData();
|
||||
formData.append('i', user.token);
|
||||
formData.append('file', fs.createReadStream(path || _dirname + '/resources/Lenna.png'));
|
||||
const formData = new FormData();
|
||||
formData.append('i', user.token);
|
||||
formData.append('file', fs.createReadStream(path || _dirname + '/resources/Lenna.png'));
|
||||
|
||||
return fetch(`http://localhost:${port}/api/drive/files/create`, {
|
||||
method: 'post',
|
||||
body: formData,
|
||||
timeout: 30 * 1000,
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
throw `${res.status} ${res.statusText}`;
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
});
|
||||
return fetch(`http://localhost:${port}/api/drive/files/create`, {
|
||||
method: 'post',
|
||||
body: formData,
|
||||
timeout: 30 * 1000,
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
throw `${res.status} ${res.statusText}`;
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
|
||||
@ -99,9 +100,9 @@ export function connectStream(user: any, channel: string, listener: (message: Re
|
||||
ws.on('open', () => {
|
||||
ws.on('message', data => {
|
||||
const msg = JSON.parse(data.toString());
|
||||
if (msg.type == 'channel' && msg.body.id == 'a') {
|
||||
if (msg.type === 'channel' && msg.body.id === 'a') {
|
||||
listener(msg.body);
|
||||
} else if (msg.type == 'connected' && msg.body.id == 'a') {
|
||||
} else if (msg.type === 'connected' && msg.body.id === 'a') {
|
||||
res(ws);
|
||||
}
|
||||
});
|
||||
@ -112,8 +113,8 @@ export function connectStream(user: any, channel: string, listener: (message: Re
|
||||
channel: channel,
|
||||
id: 'a',
|
||||
pong: true,
|
||||
params: params
|
||||
}
|
||||
params: params,
|
||||
},
|
||||
}));
|
||||
});
|
||||
});
|
||||
@ -124,8 +125,8 @@ export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?
|
||||
return await new Promise((resolve, reject) => {
|
||||
const req = http.request(`http://localhost:${port}${path}`, {
|
||||
headers: {
|
||||
Accept: accept
|
||||
}
|
||||
Accept: accept,
|
||||
},
|
||||
}, res => {
|
||||
if (res.statusCode! >= 400) {
|
||||
reject(res);
|
||||
@ -146,7 +147,7 @@ export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProce
|
||||
return (done: (err?: Error) => any) => {
|
||||
const p = childProcess.spawn('node', [_dirname + '/../index.js'], {
|
||||
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
||||
env: { NODE_ENV: 'test', PATH: process.env.PATH }
|
||||
env: { NODE_ENV: 'test', PATH: process.env.PATH },
|
||||
});
|
||||
callbackSpawnedProcess(p);
|
||||
p.on('message', message => {
|
||||
@ -158,12 +159,7 @@ export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProce
|
||||
export async function initTestDb(justBorrow = false, initEntities?: any[]) {
|
||||
if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
|
||||
|
||||
try {
|
||||
const conn = await getConnection();
|
||||
await conn.close();
|
||||
} catch (e) {}
|
||||
|
||||
return await createConnection({
|
||||
const db = new DataSource({
|
||||
type: 'postgres',
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
@ -172,8 +168,12 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) {
|
||||
database: config.db.db,
|
||||
synchronize: true && !justBorrow,
|
||||
dropSchema: true && !justBorrow,
|
||||
entities: initEntities || entities
|
||||
entities: initEntities || entities,
|
||||
});
|
||||
|
||||
await db.initialize();
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
export function startServer(timeout = 30 * 1000): Promise<childProcess.ChildProcess> {
|
||||
@ -185,7 +185,7 @@ export function startServer(timeout = 30 * 1000): Promise<childProcess.ChildProc
|
||||
|
||||
const p = childProcess.spawn('node', [_dirname + '/../built/index.js'], {
|
||||
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
||||
env: { NODE_ENV: 'test', PATH: process.env.PATH }
|
||||
env: { NODE_ENV: 'test', PATH: process.env.PATH },
|
||||
});
|
||||
|
||||
p.on('error', e => rej(e));
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,68 +1,79 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
"node": false
|
||||
'node': false,
|
||||
},
|
||||
parser: "vue-eslint-parser",
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
'parser': '@typescript-eslint/parser',
|
||||
tsconfigRootDir: __dirname,
|
||||
//project: ['./tsconfig.json'],
|
||||
project: ['./tsconfig.json'],
|
||||
extraFileExtensions: ['.vue'],
|
||||
},
|
||||
extends: [
|
||||
//"../shared/.eslintrc.js",
|
||||
"plugin:vue/vue3-recommended"
|
||||
'../shared/.eslintrc.js',
|
||||
'plugin:vue/vue3-recommended',
|
||||
],
|
||||
rules: {
|
||||
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||
// data の禁止理由: 抽象的すぎるため
|
||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||
"id-denylist": ["error", "window", "data", "e"],
|
||||
'id-denylist': ['error', 'window', 'data', 'e'],
|
||||
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
|
||||
"no-shadow": ["warn"],
|
||||
"vue/attributes-order": ["error", {
|
||||
"alphabetical": false
|
||||
'no-shadow': ['warn'],
|
||||
'vue/attributes-order': ['error', {
|
||||
'alphabetical': false,
|
||||
}],
|
||||
"vue/no-use-v-if-with-v-for": ["error", {
|
||||
"allowUsingIterationVar": false
|
||||
'vue/no-use-v-if-with-v-for': ['error', {
|
||||
'allowUsingIterationVar': false,
|
||||
}],
|
||||
"vue/no-ref-as-operand": "error",
|
||||
"vue/no-multi-spaces": ["error", {
|
||||
"ignoreProperties": false
|
||||
'vue/no-ref-as-operand': 'error',
|
||||
'vue/no-multi-spaces': ['error', {
|
||||
'ignoreProperties': false,
|
||||
}],
|
||||
"vue/no-v-html": "error",
|
||||
"vue/order-in-components": "error",
|
||||
"vue/html-indent": ["warn", "tab", {
|
||||
"attribute": 1,
|
||||
"baseIndent": 0,
|
||||
"closeBracket": 0,
|
||||
"alignAttributesVertically": true,
|
||||
"ignores": []
|
||||
'vue/no-v-html': 'error',
|
||||
'vue/order-in-components': 'error',
|
||||
'vue/html-indent': ['warn', 'tab', {
|
||||
'attribute': 1,
|
||||
'baseIndent': 0,
|
||||
'closeBracket': 0,
|
||||
'alignAttributesVertically': true,
|
||||
'ignores': [],
|
||||
}],
|
||||
"vue/html-closing-bracket-spacing": ["warn", {
|
||||
"startTag": "never",
|
||||
"endTag": "never",
|
||||
"selfClosingTag": "never"
|
||||
'vue/html-closing-bracket-spacing': ['warn', {
|
||||
'startTag': 'never',
|
||||
'endTag': 'never',
|
||||
'selfClosingTag': 'never',
|
||||
}],
|
||||
"vue/multi-word-component-names": "warn",
|
||||
"vue/require-v-for-key": "warn",
|
||||
"vue/no-unused-components": "warn",
|
||||
"vue/valid-v-for": "warn",
|
||||
"vue/return-in-computed-property": "warn",
|
||||
"vue/no-setup-props-destructure": "warn",
|
||||
"vue/max-attributes-per-line": "off",
|
||||
"vue/html-self-closing": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
'vue/multi-word-component-names': 'warn',
|
||||
'vue/require-v-for-key': 'warn',
|
||||
'vue/no-unused-components': 'warn',
|
||||
'vue/valid-v-for': 'warn',
|
||||
'vue/return-in-computed-property': 'warn',
|
||||
'vue/no-setup-props-destructure': 'warn',
|
||||
'vue/max-attributes-per-line': 'off',
|
||||
'vue/html-self-closing': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
},
|
||||
globals: {
|
||||
"require": false,
|
||||
"_DEV_": false,
|
||||
"_LANGS_": false,
|
||||
"_VERSION_": false,
|
||||
"_ENV_": false,
|
||||
"_PERF_PREFIX_": false,
|
||||
"_DATA_TRANSFER_DRIVE_FILE_": false,
|
||||
"_DATA_TRANSFER_DRIVE_FOLDER_": false,
|
||||
"_DATA_TRANSFER_DECK_COLUMN_": false
|
||||
}
|
||||
}
|
||||
// Node.js
|
||||
'module': false,
|
||||
'require': false,
|
||||
'__dirname': false,
|
||||
|
||||
// Vue
|
||||
'$$': false,
|
||||
'$ref': false,
|
||||
'$computed': false,
|
||||
|
||||
// Misskey
|
||||
'_DEV_': false,
|
||||
'_LANGS_': false,
|
||||
'_VERSION_': false,
|
||||
'_ENV_': false,
|
||||
'_PERF_PREFIX_': false,
|
||||
'_DATA_TRANSFER_DRIVE_FILE_': false,
|
||||
'_DATA_TRANSFER_DRIVE_FOLDER_': false,
|
||||
'_DATA_TRANSFER_DECK_COLUMN_': false,
|
||||
},
|
||||
};
|
||||
|
8
packages/client/@types/theme.d.ts
vendored
8
packages/client/@types/theme.d.ts
vendored
@ -1,5 +1,7 @@
|
||||
import { Theme } from '../src/scripts/theme';
|
||||
|
||||
declare module '@/themes/*.json5' {
|
||||
export = Theme;
|
||||
import { Theme } from "@/scripts/theme";
|
||||
|
||||
const theme: Theme;
|
||||
|
||||
export default theme;
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordapp/twemoji": "13.1.1",
|
||||
"@discordapp/twemoji": "14.0.2",
|
||||
"@fortawesome/fontawesome-free": "6.1.1",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"abort-controller": "3.0.0",
|
||||
@ -18,11 +18,11 @@
|
||||
"autosize": "5.0.1",
|
||||
"autwh": "0.1.0",
|
||||
"blurhash": "1.1.5",
|
||||
"broadcast-channel": "4.11.0",
|
||||
"broadcast-channel": "4.12.0",
|
||||
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2",
|
||||
"chart.js": "3.7.1",
|
||||
"chart.js": "3.8.0",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
"chartjs-plugin-gradient": "0.2.2",
|
||||
"chartjs-plugin-gradient": "0.5.0",
|
||||
"chartjs-plugin-zoom": "1.2.1",
|
||||
"compare-versions": "4.1.3",
|
||||
"content-disposition": "0.5.4",
|
||||
@ -33,16 +33,14 @@
|
||||
"idb-keyval": "6.1.0",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"json5": "2.2.1",
|
||||
"katex": "0.15.3",
|
||||
"katex": "0.15.6",
|
||||
"matter-js": "0.18.0",
|
||||
"mfm-js": "0.21.0",
|
||||
"mfm-js": "0.22.1",
|
||||
"misskey-js": "0.0.14",
|
||||
"mocha": "9.2.2",
|
||||
"mocha": "10.0.0",
|
||||
"ms": "2.1.3",
|
||||
"nested-property": "4.0.0",
|
||||
"parse5": "6.0.1",
|
||||
"photoswipe": "5.2.7",
|
||||
"portscanner": "2.2.0",
|
||||
"prismjs": "1.28.0",
|
||||
"private-ip": "2.3.3",
|
||||
"promise-limit": "2.7.0",
|
||||
@ -54,32 +52,32 @@
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.51.0",
|
||||
"sass": "1.52.1",
|
||||
"seedrandom": "3.0.5",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.140.2",
|
||||
"throttle-debounce": "4.0.1",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tsc-alias": "1.5.0",
|
||||
"tsconfig-paths": "3.14.1",
|
||||
"tsc-alias": "1.6.7",
|
||||
"tsconfig-paths": "4.0.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"uuid": "8.3.2",
|
||||
"v-debounce": "0.1.2",
|
||||
"vanilla-tilt": "1.7.2",
|
||||
"vue": "3.2.33",
|
||||
"vue": "3.2.36",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vue-router": "4.0.15",
|
||||
"vuedraggable": "4.0.1",
|
||||
"websocket": "1.0.34",
|
||||
"@vitejs/plugin-vue": "2.3.3",
|
||||
"@vue/compiler-sfc": "3.2.33",
|
||||
"@vue/compiler-sfc": "3.2.36",
|
||||
"@rollup/plugin-alias": "3.1.9",
|
||||
"@rollup/plugin-json": "4.1.0",
|
||||
"rollup": "2.73.0",
|
||||
"typescript": "4.6.4",
|
||||
"rollup": "2.74.1",
|
||||
"typescript": "4.7.2",
|
||||
"vite": "2.9.9",
|
||||
"ws": "8.6.0"
|
||||
},
|
||||
@ -93,7 +91,6 @@
|
||||
"@types/matter-js": "0.17.7",
|
||||
"@types/mocha": "9.1.1",
|
||||
"@types/oauth": "0.9.1",
|
||||
"@types/parse5": "6.0.3",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/qrcode": "1.4.2",
|
||||
"@types/random-seed": "0.3.3",
|
||||
@ -103,12 +100,12 @@
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.23.0",
|
||||
"@typescript-eslint/parser": "5.23.0",
|
||||
"eslint": "8.15.0",
|
||||
"eslint-plugin-vue": "8.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.26.0",
|
||||
"@typescript-eslint/parser": "5.26.0",
|
||||
"eslint": "8.16.0",
|
||||
"eslint-plugin-vue": "9.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "9.6.1",
|
||||
"cypress": "9.7.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"start-server-and-test": "1.14.0"
|
||||
}
|
||||
|
@ -11,10 +11,10 @@ import { i18n } from './i18n';
|
||||
|
||||
type Account = misskey.entities.MeDetailed;
|
||||
|
||||
const data = localStorage.getItem('account');
|
||||
const accountData = localStorage.getItem('account');
|
||||
|
||||
// TODO: 外部からはreadonlyに
|
||||
export const $i = data ? reactive(JSON.parse(data) as Account) : null;
|
||||
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
||||
|
||||
export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator);
|
||||
|
||||
@ -52,7 +52,7 @@ export async function signout() {
|
||||
return Promise.all(registrations.map(registration => registration.unregister()));
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (err) {}
|
||||
//#endregion
|
||||
|
||||
document.cookie = `igi=; path=/`;
|
||||
@ -104,8 +104,8 @@ function fetchAccount(token: string): Promise<Account> {
|
||||
});
|
||||
}
|
||||
|
||||
export function updateAccount(data) {
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
export function updateAccount(accountData) {
|
||||
for (const [key, value] of Object.entries(accountData)) {
|
||||
$i[key] = value;
|
||||
}
|
||||
localStorage.setItem('account', JSON.stringify($i));
|
||||
|
@ -37,7 +37,7 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'closed'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const window = ref<InstanceType<typeof XWindow>>();
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="bcekxzvu _card _gap">
|
||||
<div class="_content target">
|
||||
<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
|
||||
<MkA class="info" :to="userPage(report.targetUser)" v-user-preview="report.targetUserId">
|
||||
<MkA v-user-preview="report.targetUserId" class="info" :to="userPage(report.targetUser)">
|
||||
<MkUserName class="name" :user="report.targetUser"/>
|
||||
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
|
||||
</MkA>
|
||||
|
@ -48,8 +48,8 @@ async function onClick() {
|
||||
});
|
||||
isFollowing.value = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
wait.value = false;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user