Merge branch 'develop' into fix-msg-room
This commit is contained in:
commit
312e2a022c
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
|
## 🤬 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
|
## 📝 Steps to Reproduce
|
||||||
|
|
||||||
|
4
.github/labeler.yml
vendored
4
.github/labeler.yml
vendored
@ -4,5 +4,9 @@
|
|||||||
'🖥️Client':
|
'🖥️Client':
|
||||||
- packages/client/**/*
|
- packages/client/**/*
|
||||||
|
|
||||||
|
'🧪Test':
|
||||||
|
- cypress/**/*
|
||||||
|
- packages/backend/test/**/*
|
||||||
|
|
||||||
'‼️ wrong locales':
|
'‼️ wrong locales':
|
||||||
- any: ['locales/*.yml', '!locales/ja-JP.yml']
|
- any: ['locales/*.yml', '!locales/ja-JP.yml']
|
||||||
|
4
.github/workflows/labeler.yml
vendored
4
.github/workflows/labeler.yml
vendored
@ -1,6 +1,8 @@
|
|||||||
name: "Pull Request Labeler"
|
name: "Pull Request Labeler"
|
||||||
on:
|
on:
|
||||||
- pull_request_target
|
pull_request_target:
|
||||||
|
branches-ignore:
|
||||||
|
- 'l10n_develop'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
triage:
|
triage:
|
||||||
|
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.DEPLOYBOT_APP_ID }}
|
||||||
|
private_key: ${{ secrets.DEPLOYBOT_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: deploy
|
||||||
|
named-args: true
|
||||||
|
permission: write
|
95
.github/workflows/pr-preview-deploy.yml
vendored
Normal file
95
.github/workflows/pr-preview-deploy.yml
vendored
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# Run secret-dependent integration tests only after /deploy approval
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, synchronize]
|
||||||
|
repository_dispatch:
|
||||||
|
types: [deploy-command]
|
||||||
|
|
||||||
|
name: Deploy preview environment
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Repo owner has commented /deploy 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:
|
||||||
|
- uses: actions/github-script@v5
|
||||||
|
id: check-id
|
||||||
|
env:
|
||||||
|
number: ${{ github.event.client_payload.pull_request.number }}
|
||||||
|
job: ${{ github.job }}
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
result-encoding: string
|
||||||
|
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);
|
||||||
|
|
||||||
|
return check[0].id;
|
||||||
|
|
||||||
|
- uses: actions/github-script@v5
|
||||||
|
env:
|
||||||
|
check_id: ${{ steps.check-id.outputs.result }}
|
||||||
|
details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }}
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
await github.rest.checks.update({
|
||||||
|
...context.repo,
|
||||||
|
check_run_id: process.env.check_id,
|
||||||
|
status: 'in_progress',
|
||||||
|
details_url: process.env.details_url
|
||||||
|
});
|
||||||
|
|
||||||
|
# Check out merge commit
|
||||||
|
- name: Fork based /deploy 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 }}-syuilo
|
||||||
|
timeout: 15m
|
||||||
|
|
||||||
|
# Update check run called "integration-fork"
|
||||||
|
- uses: actions/github-script@v5
|
||||||
|
id: update-check-run
|
||||||
|
if: ${{ always() }}
|
||||||
|
env:
|
||||||
|
# Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run
|
||||||
|
conclusion: ${{ job.status }}
|
||||||
|
check_id: ${{ steps.check-id.outputs.result }}
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const { data: result } = await github.rest.checks.update({
|
||||||
|
...context.repo,
|
||||||
|
check_run_id: process.env.check_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 }}-syuilo
|
@ -1 +1 @@
|
|||||||
v18.0.0
|
v16.15.0
|
||||||
|
52
CHANGELOG.md
52
CHANGELOG.md
@ -10,38 +10,52 @@ You should also include the user name that made the change.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
## 12.x.x (unreleased)
|
## 12.x.x (unreleased)
|
||||||
### NOTE
|
|
||||||
- From this version, Node 18.0.0 or later is required.
|
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
- enhance: ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina
|
- Supports Unicode Emoji 14.0 @mei23
|
||||||
- enhance: API: notifications/readは配列でも受け付けるように #7667 @tamaina
|
- プッシュ通知を複数アカウント対応に #7667 @tamaina
|
||||||
- enhance: プッシュ通知を複数アカウント対応に #7667 @tamaina
|
- プッシュ通知にクリックやactionを設定 #7667 @tamaina
|
||||||
- enhance: プッシュ通知にクリックやactionを設定 #7667 @tamaina
|
- ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina
|
||||||
- replaced webpack with Vite @tamaina
|
- Server: always remove completed tasks of job queue @Johann150
|
||||||
- update dependencies @syuilo
|
- Client: アバターの設定で画像をクロップできるように @syuilo
|
||||||
- enhance: display URL of QR code for TOTP registration @syuilo
|
- Client: make emoji stand out more on reaction button @Johann150
|
||||||
- make CAPTCHA required for signin to improve security @syuilo
|
- Client: display URL of QR code for TOTP registration @tamaina
|
||||||
- enhance: Supports Unicode Emoji 14.0 @mei23
|
- Client: render quote renote CWs as MFM @pixeldesu
|
||||||
|
- 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
|
- The theme color is now better validated. @Johann150
|
||||||
Your own theme color may be unset if it was in an invalid format.
|
Your own theme color may be unset if it was in an invalid format.
|
||||||
Admins should check their instance settings if in doubt.
|
Admins should check their instance settings if in doubt.
|
||||||
- Perform port diagnosis at startup only when Listen fails @mei23
|
- 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
|
### Bugfixes
|
||||||
- Client: fix settings page @tamaina
|
- Server: keep file order of note attachement @Johann150
|
||||||
- Client: fix profile tabs @futchitwo
|
- Server: fix caching @Johann150
|
||||||
- Server: await promises when following or unfollowing users @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 missing foreign key for reports leading to reports page being unusable @Johann150
|
||||||
- Server: fix internal in-memory caching @Johann150
|
- Server: fix internal in-memory caching @Johann150
|
||||||
- Server: use correct order of attachments on notes @Johann150
|
- Server: use correct order of attachments on notes @Johann150
|
||||||
- Server: prevent crash when processing certain PNGs @syuilo
|
- Server: prevent crash when processing certain PNGs @syuilo
|
||||||
- Server: Fix unable to generate video thumbnails @mei23
|
- 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)
|
## 12.110.1 (2022/04/23)
|
||||||
|
|
||||||
|
@ -66,20 +66,29 @@ Be willing to comment on the good points and not just the things you want fixed
|
|||||||
- Are there any omissions or gaps?
|
- Are there any omissions or gaps?
|
||||||
- Does it check for anomalies?
|
- Does it check for anomalies?
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
The `/deploy` command by issue comment can be used to deploy the contents of a PR to the preview environment.
|
||||||
|
```
|
||||||
|
/deploy sha=<commit hash>
|
||||||
|
```
|
||||||
|
An actual domain will be assigned so you can test the federation.
|
||||||
|
|
||||||
## Merge
|
## Merge
|
||||||
For now, basically only @syuilo has the authority to merge PRs into develop because he is most familiar with the codebase.
|
For now, basically only @syuilo has the authority to merge PRs into develop because he is most familiar with the codebase.
|
||||||
However, minor fixes, refactoring, and urgent changes may be merged at the discretion of a contributor.
|
However, minor fixes, refactoring, and urgent changes may be merged at the discretion of a contributor.
|
||||||
|
|
||||||
## Release
|
## 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
|
### Release Instructions
|
||||||
1. commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json))
|
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.
|
2. Create a release PR.
|
||||||
3. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases)
|
- Into `master` from `develop` branch.
|
||||||
- The target branch must be `master`
|
- The title must be in the format `Release: x.y.z`.
|
||||||
- The tag name must be the version
|
- `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.
|
||||||
|
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)
|
## Localization (l10n)
|
||||||
Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management.
|
Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management.
|
||||||
|
@ -47,6 +47,10 @@ With Misskey's built in drive, you get cloud storage right in your social media,
|
|||||||
|
|
||||||
<div style="clear: both;"></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
|
## Sponsors
|
||||||
<div align="center">
|
<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>
|
<a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a>
|
||||||
|
12
cypress.config.ts
Normal file
12
cypress.config.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from 'cypress'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
// We've imported your old cypress plugins here.
|
||||||
|
// You may want to clean this up later by importing these.
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
return require('./cypress/plugins/index.js')(on, config)
|
||||||
|
},
|
||||||
|
baseUrl: 'http://localhost:61812',
|
||||||
|
},
|
||||||
|
})
|
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"baseUrl": "http://localhost:61812"
|
|
||||||
}
|
|
@ -1,11 +1,6 @@
|
|||||||
describe('Before setup instance', () => {
|
describe('Before setup instance', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.window(win => {
|
cy.resetState();
|
||||||
win.indexedDB.deleteDatabase('keyval-store');
|
|
||||||
});
|
|
||||||
cy.request('POST', '/api/reset-db').as('reset');
|
|
||||||
cy.get('@reset').its('status').should('equal', 204);
|
|
||||||
cy.reload(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -35,18 +30,10 @@ describe('Before setup instance', () => {
|
|||||||
|
|
||||||
describe('After setup instance', () => {
|
describe('After setup instance', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.window(win => {
|
cy.resetState();
|
||||||
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.request('POST', '/api/admin/accounts/create', {
|
cy.registerUser('admin', 'pass', true);
|
||||||
username: 'admin',
|
|
||||||
password: 'pass',
|
|
||||||
}).its('body').as('admin');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -76,24 +63,13 @@ describe('After setup instance', () => {
|
|||||||
|
|
||||||
describe('After user signup', () => {
|
describe('After user signup', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.window(win => {
|
cy.resetState();
|
||||||
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.request('POST', '/api/admin/accounts/create', {
|
cy.registerUser('admin', 'pass', true);
|
||||||
username: 'admin',
|
|
||||||
password: 'pass',
|
|
||||||
}).its('body').as('admin');
|
|
||||||
|
|
||||||
// ユーザー作成
|
// ユーザー作成
|
||||||
cy.request('POST', '/api/signup', {
|
cy.registerUser('alice', 'alice1234');
|
||||||
username: 'alice',
|
|
||||||
password: 'alice1234',
|
|
||||||
}).its('body').as('alice');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -138,34 +114,15 @@ describe('After user signup', () => {
|
|||||||
|
|
||||||
describe('After user singed in', () => {
|
describe('After user singed in', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.window(win => {
|
cy.resetState();
|
||||||
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.request('POST', '/api/admin/accounts/create', {
|
cy.registerUser('admin', 'pass', true);
|
||||||
username: 'admin',
|
|
||||||
password: 'pass',
|
|
||||||
}).its('body').as('admin');
|
|
||||||
|
|
||||||
// ユーザー作成
|
// ユーザー作成
|
||||||
cy.request('POST', '/api/signup', {
|
cy.registerUser('alice', 'alice1234');
|
||||||
username: 'alice',
|
|
||||||
password: 'alice1234',
|
|
||||||
}).its('body').as('alice');
|
|
||||||
|
|
||||||
cy.visit('/');
|
cy.login('alice', 'alice1234');
|
||||||
|
|
||||||
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');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
65
cypress/e2e/widgets.cy.js
Normal file
65
cypress/e2e/widgets.cy.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 --
|
// -- This will overwrite an existing command --
|
||||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
// 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');
|
||||||
|
});
|
||||||
|
@ -842,6 +842,9 @@ oneDay: "1日"
|
|||||||
oneWeek: "1週間"
|
oneWeek: "1週間"
|
||||||
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
|
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
|
||||||
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
|
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
|
||||||
|
rateLimitExceeded: "レート制限を超えました"
|
||||||
|
cropImage: "画像のクロップ"
|
||||||
|
cropImageAsk: "画像をクロップしますか?"
|
||||||
|
|
||||||
_emailUnavailable:
|
_emailUnavailable:
|
||||||
used: "既に使用されています"
|
used: "既に使用されています"
|
||||||
@ -1110,7 +1113,6 @@ _sfx:
|
|||||||
channel: "チャンネル通知"
|
channel: "チャンネル通知"
|
||||||
|
|
||||||
_ago:
|
_ago:
|
||||||
unknown: "謎"
|
|
||||||
future: "未来"
|
future: "未来"
|
||||||
justNow: "たった今"
|
justNow: "たった今"
|
||||||
secondsAgo: "{n}秒前"
|
secondsAgo: "{n}秒前"
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
"watch": "npm run dev",
|
"watch": "npm run dev",
|
||||||
"dev": "node ./scripts/dev.js",
|
"dev": "node ./scripts/dev.js",
|
||||||
"lint": "node ./scripts/lint.js",
|
"lint": "node ./scripts/lint.js",
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts",
|
||||||
"cy:run": "cypress run",
|
"cy:run": "cypress run",
|
||||||
"e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
|
"e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
|
||||||
"mocha": "cd packages/backend && cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" npx mocha",
|
"mocha": "cd packages/backend && cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" npx mocha",
|
||||||
@ -41,10 +41,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/gulp": "4.0.9",
|
"@types/gulp": "4.0.9",
|
||||||
"@types/gulp-rename": "2.0.1",
|
"@types/gulp-rename": "2.0.1",
|
||||||
"@typescript-eslint/parser": "5.18.0",
|
"@typescript-eslint/parser": "5.27.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "9.5.3",
|
"cypress": "10.0.3",
|
||||||
"start-server-and-test": "1.14.0",
|
"start-server-and-test": "1.14.0",
|
||||||
"typescript": "4.6.3"
|
"typescript": "4.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,6 @@
|
|||||||
"loader=./test/loader.js"
|
"loader=./test/loader.js"
|
||||||
],
|
],
|
||||||
"slow": 1000,
|
"slow": 1000,
|
||||||
"timeout": 3000,
|
"timeout": 10000,
|
||||||
"exit": true
|
"exit": true
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
"lodash": "^4.17.21"
|
"lodash": "^4.17.21"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/koa": "3.10.4",
|
"@bull-board/koa": "3.11.1",
|
||||||
"@discordapp/twemoji": "14.0.2",
|
"@discordapp/twemoji": "14.0.2",
|
||||||
"@elastic/elasticsearch": "7.11.0",
|
"@elastic/elasticsearch": "7.11.0",
|
||||||
"@koa/cors": "3.1.0",
|
"@koa/cors": "3.1.0",
|
||||||
@ -28,10 +28,9 @@
|
|||||||
"archiver": "5.3.1",
|
"archiver": "5.3.1",
|
||||||
"autobind-decorator": "2.4.0",
|
"autobind-decorator": "2.4.0",
|
||||||
"autwh": "0.1.0",
|
"autwh": "0.1.0",
|
||||||
"aws-sdk": "2.1135.0",
|
"aws-sdk": "2.1152.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "1.1.5",
|
"blurhash": "1.1.5",
|
||||||
"broadcast-channel": "4.12.0",
|
|
||||||
"bull": "4.8.3",
|
"bull": "4.8.3",
|
||||||
"cacheable-lookup": "6.0.4",
|
"cacheable-lookup": "6.0.4",
|
||||||
"cbor": "8.1.0",
|
"cbor": "8.1.0",
|
||||||
@ -44,18 +43,18 @@
|
|||||||
"deep-email-validator": "0.1.21",
|
"deep-email-validator": "0.1.21",
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "17.1.1",
|
"file-type": "17.1.2",
|
||||||
"fluent-ffmpeg": "2.1.2",
|
"fluent-ffmpeg": "2.1.2",
|
||||||
"got": "12.0.4",
|
"got": "12.1.0",
|
||||||
"hpagent": "0.1.2",
|
"hpagent": "0.1.2",
|
||||||
"ip-cidr": "3.0.8",
|
"ip-cidr": "3.0.10",
|
||||||
"is-svg": "4.3.2",
|
"is-svg": "4.3.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "19.0.0",
|
"jsdom": "19.0.0",
|
||||||
"json5": "2.2.1",
|
"json5": "2.2.1",
|
||||||
"json5-loader": "4.0.1",
|
"json5-loader": "4.0.1",
|
||||||
"jsonld": "5.2.0",
|
"jsonld": "6.0.0",
|
||||||
"jsrsasign": "10.5.22",
|
"jsrsasign": "10.5.24",
|
||||||
"koa": "2.13.4",
|
"koa": "2.13.4",
|
||||||
"koa-bodyparser": "4.3.0",
|
"koa-bodyparser": "4.3.0",
|
||||||
"koa-favicon": "2.1.0",
|
"koa-favicon": "2.1.0",
|
||||||
@ -72,7 +71,7 @@
|
|||||||
"ms": "3.0.0-canary.1",
|
"ms": "3.0.0-canary.1",
|
||||||
"multer": "1.4.4",
|
"multer": "1.4.4",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.2.4",
|
"node-fetch": "3.2.6",
|
||||||
"nodemailer": "6.7.5",
|
"nodemailer": "6.7.5",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
"parse5": "6.0.1",
|
"parse5": "6.0.1",
|
||||||
@ -101,14 +100,14 @@
|
|||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"style-loader": "3.3.1",
|
"style-loader": "3.3.1",
|
||||||
"summaly": "2.5.0",
|
"summaly": "2.5.1",
|
||||||
"syslog-pro": "1.0.0",
|
"syslog-pro": "1.0.0",
|
||||||
"systeminformation": "5.11.15",
|
"systeminformation": "5.11.16",
|
||||||
"tinycolor2": "1.4.2",
|
"tinycolor2": "1.4.2",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"ts-loader": "9.3.0",
|
"ts-loader": "9.3.0",
|
||||||
"ts-node": "10.8.0",
|
"ts-node": "10.8.1",
|
||||||
"tsc-alias": "1.6.7",
|
"tsc-alias": "1.6.9",
|
||||||
"tsconfig-paths": "4.0.0",
|
"tsconfig-paths": "4.0.0",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typeorm": "0.3.6",
|
"typeorm": "0.3.6",
|
||||||
@ -117,7 +116,7 @@
|
|||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"web-push": "3.5.0",
|
"web-push": "3.5.0",
|
||||||
"websocket": "1.0.34",
|
"websocket": "1.0.34",
|
||||||
"ws": "8.6.0",
|
"ws": "8.8.0",
|
||||||
"xev": "3.0.2"
|
"xev": "3.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -145,7 +144,7 @@
|
|||||||
"@types/koa__multer": "2.0.4",
|
"@types/koa__multer": "2.0.4",
|
||||||
"@types/koa__router": "8.0.11",
|
"@types/koa__router": "8.0.11",
|
||||||
"@types/mocha": "9.1.1",
|
"@types/mocha": "9.1.1",
|
||||||
"@types/node": "17.0.35",
|
"@types/node": "17.0.41",
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
"@types/nodemailer": "6.4.4",
|
"@types/nodemailer": "6.4.4",
|
||||||
"@types/oauth": "0.9.1",
|
"@types/oauth": "0.9.1",
|
||||||
@ -167,12 +166,11 @@
|
|||||||
"@types/web-push": "3.3.2",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
"@types/ws": "8.5.3",
|
"@types/ws": "8.5.3",
|
||||||
"@typescript-eslint/eslint-plugin": "5.26.0",
|
"@typescript-eslint/eslint-plugin": "5.27.1",
|
||||||
"@typescript-eslint/parser": "5.26.0",
|
"@typescript-eslint/parser": "5.27.1",
|
||||||
"typescript": "4.7.2",
|
"typescript": "4.7.3",
|
||||||
"eslint": "8.16.0",
|
"eslint": "8.17.0",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
|
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"execa": "6.1.0"
|
"execa": "6.1.0"
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ export default function load() {
|
|||||||
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
|
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
|
||||||
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
|
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
|
||||||
mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
|
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;
|
if (!config.redis.prefix) config.redis.prefix = mixin.host;
|
||||||
|
|
||||||
|
@ -73,6 +73,7 @@ import { entities as charts } from '@/services/chart/entities.js';
|
|||||||
import { Webhook } from '@/models/entities/webhook.js';
|
import { Webhook } from '@/models/entities/webhook.js';
|
||||||
import { envOption } from '../env.js';
|
import { envOption } from '../env.js';
|
||||||
import { dbLogger } from './logger.js';
|
import { dbLogger } from './logger.js';
|
||||||
|
import { redisClient } from './redis.js';
|
||||||
|
|
||||||
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
|
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
|
||||||
|
|
||||||
@ -207,7 +208,15 @@ export const db = new DataSource({
|
|||||||
migrations: ['../../migration/*.js'],
|
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) {
|
if (db.isInitialized) {
|
||||||
// nop
|
// nop
|
||||||
} else {
|
} else {
|
||||||
@ -217,6 +226,7 @@ export async function initDb() {
|
|||||||
|
|
||||||
export async function resetDb() {
|
export async function resetDb() {
|
||||||
const reset = async () => {
|
const reset = async () => {
|
||||||
|
await redisClient.FLUSHDB();
|
||||||
const tables = await db.query(`SELECT relname AS "table"
|
const tables = await db.query(`SELECT relname AS "table"
|
||||||
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
|
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
|
||||||
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
|
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
|
||||||
|
@ -6,6 +6,9 @@ const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
|
|||||||
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
|
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
|
||||||
|
|
||||||
export function fromHtml(html: string, hashtagNames?: string[]): string {
|
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);
|
const dom = parse5.parseFragment(html);
|
||||||
|
|
||||||
let text = '';
|
let text = '';
|
||||||
|
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'] {
|
getPublicProperties(file: DriveFile): DriveFile['properties'] {
|
||||||
if (file.properties.orientation != null) {
|
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) {
|
if (file.properties.orientation >= 5) {
|
||||||
[properties.width, properties.height] = [properties.height, properties.width];
|
[properties.width, properties.height] = [properties.height, properties.width];
|
||||||
}
|
}
|
||||||
|
@ -144,13 +144,7 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// 指定されているかどうか
|
// 指定されているかどうか
|
||||||
const specified = note.visibleUserIds.some((id: any) => meId === id);
|
return note.visibleUserIds.some((id: any) => meId === id);
|
||||||
|
|
||||||
if (specified) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,9 +163,12 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||||||
} else {
|
} else {
|
||||||
// フォロワーかどうか
|
// フォロワーかどうか
|
||||||
const [following, user] = await Promise.all([
|
const [following, user] = await Promise.all([
|
||||||
Followings.findOneBy({
|
Followings.count({
|
||||||
followeeId: note.userId,
|
where: {
|
||||||
followerId: meId,
|
followeeId: note.userId,
|
||||||
|
followerId: meId,
|
||||||
|
},
|
||||||
|
take: 1,
|
||||||
}),
|
}),
|
||||||
Users.findOneByOrFail({ id: meId }),
|
Users.findOneByOrFail({ id: meId }),
|
||||||
]);
|
]);
|
||||||
@ -183,7 +180,7 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||||||
in which case we can never know the following. Instead we have
|
in which case we can never know the following. Instead we have
|
||||||
to assume that the users are following each other.
|
to assume that the users are following each other.
|
||||||
*/
|
*/
|
||||||
return following != null || (note.userHost != null && user.host != null);
|
return following > 0 || (note.userHost != null && user.host != null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,47 +61,58 @@ export const UserRepository = db.getRepository(User).extend({
|
|||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
async getRelation(me: User['id'], target: User['id']) {
|
async getRelation(me: User['id'], target: User['id']) {
|
||||||
const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
|
return awaitAll({
|
||||||
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 {
|
|
||||||
id: target,
|
id: target,
|
||||||
isFollowing: following1 != null,
|
isFollowing: Followings.count({
|
||||||
hasPendingFollowRequestFromYou: followReq1 != null,
|
where: {
|
||||||
hasPendingFollowRequestToYou: followReq2 != null,
|
followerId: me,
|
||||||
isFollowed: following2 != null,
|
followeeId: target,
|
||||||
isBlocking: toBlocking != null,
|
},
|
||||||
isBlocked: fromBlocked != null,
|
take: 1,
|
||||||
isMuted: mute != null,
|
}).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> {
|
async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
|
||||||
|
@ -305,11 +305,13 @@ export default function() {
|
|||||||
systemQueue.add('resyncCharts', {
|
systemQueue.add('resyncCharts', {
|
||||||
}, {
|
}, {
|
||||||
repeat: { cron: '0 0 * * *' },
|
repeat: { cron: '0 0 * * *' },
|
||||||
|
removeOnComplete: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
systemQueue.add('cleanCharts', {
|
systemQueue.add('cleanCharts', {
|
||||||
}, {
|
}, {
|
||||||
repeat: { cron: '0 0 * * *' },
|
repeat: { cron: '0 0 * * *' },
|
||||||
|
removeOnComplete: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
systemQueue.add('checkExpiredMutings', {
|
systemQueue.add('checkExpiredMutings', {
|
||||||
|
@ -9,7 +9,7 @@ import { addFile } from '@/services/drive/add-file.js';
|
|||||||
import { format as dateFormat } from 'date-fns';
|
import { format as dateFormat } from 'date-fns';
|
||||||
import { Users, Emojis } from '@/models/index.js';
|
import { Users, Emojis } from '@/models/index.js';
|
||||||
import { } from '@/queue/types.js';
|
import { } from '@/queue/types.js';
|
||||||
import { createTempDir } from '@/misc/create-temp.js';
|
import { createTemp, createTempDir } from '@/misc/create-temp.js';
|
||||||
import { downloadUrl } from '@/misc/download-url.js';
|
import { downloadUrl } from '@/misc/download-url.js';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
|
@ -5,14 +5,52 @@ import { User, IRemoteUser, CacheableRemoteUser, CacheableUser } from '@/models/
|
|||||||
import { UserPublickey } from '@/models/entities/user-publickey.js';
|
import { UserPublickey } from '@/models/entities/user-publickey.js';
|
||||||
import { MessagingMessage } from '@/models/entities/messaging-message.js';
|
import { MessagingMessage } from '@/models/entities/messaging-message.js';
|
||||||
import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.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 { Cache } from '@/misc/cache.js';
|
||||||
import { uriPersonCache, userByIdCache } from '@/services/user-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 publicKeyCache = new Cache<UserPublickey | null>(Infinity);
|
||||||
const publicKeyByUserIdCache = 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 {
|
export default class DbResolver {
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
@ -21,60 +59,54 @@ export default class DbResolver {
|
|||||||
* AP Note => Misskey Note in DB
|
* AP Note => Misskey Note in DB
|
||||||
*/
|
*/
|
||||||
public async getNoteFromApId(value: string | IObject): Promise<Note | null> {
|
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({
|
return await Notes.findOneBy({
|
||||||
id: parsed.id,
|
id: parsed.id,
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if (parsed.uri) {
|
|
||||||
return await Notes.findOneBy({
|
return await Notes.findOneBy({
|
||||||
uri: parsed.uri,
|
uri: parsed.uri,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMessageFromApId(value: string | IObject): Promise<MessagingMessage | 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({
|
return await MessagingMessages.findOneBy({
|
||||||
id: parsed.id,
|
id: parsed.id,
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if (parsed.uri) {
|
|
||||||
return await MessagingMessages.findOneBy({
|
return await MessagingMessages.findOneBy({
|
||||||
uri: parsed.uri,
|
uri: parsed.uri,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AP Person => Misskey User in DB
|
* AP Person => Misskey User in DB
|
||||||
*/
|
*/
|
||||||
public async getUserFromApId(value: string | IObject): Promise<CacheableUser | null> {
|
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({
|
return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({
|
||||||
id: parsed.id,
|
id: parsed.id,
|
||||||
}).then(x => x ?? undefined)) ?? null;
|
}).then(x => x ?? undefined)) ?? null;
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if (parsed.uri) {
|
|
||||||
return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({
|
return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({
|
||||||
uri: parsed.uri,
|
uri: parsed.uri,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -120,31 +152,4 @@ export default class DbResolver {
|
|||||||
key,
|
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
|
|
||||||
};
|
|
||||||
|
@ -3,8 +3,6 @@ import { Note } from '@/models/entities/note.js';
|
|||||||
import { toHtml } from '../../../mfm/to-html.js';
|
import { toHtml } from '../../../mfm/to-html.js';
|
||||||
|
|
||||||
export default function(note: Note) {
|
export default function(note: Note) {
|
||||||
let html = note.text ? toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)) : null;
|
if (!note.text) return '';
|
||||||
if (html == null) html = '<p>.</p>';
|
return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,9 @@ import promiseLimit from 'promise-limit';
|
|||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import Resolver from '../resolver.js';
|
import Resolver from '../resolver.js';
|
||||||
import post from '@/services/note/create.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 { 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 { htmlToMfm } from '../misc/html-to-mfm.js';
|
||||||
import { extractApHashtags } from './tag.js';
|
import { extractApHashtags } from './tag.js';
|
||||||
import { unique, toArray, toSingle } from '@/prelude/array.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 { DriveFile } from '@/models/entities/drive-file.js';
|
||||||
import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
|
import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
|
||||||
import { extractDbHost, toPuny } from '@/misc/convert-host.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 { Note } from '@/models/entities/note.js';
|
||||||
import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js';
|
import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js';
|
||||||
import { Emoji } from '@/models/entities/emoji.js';
|
import { Emoji } from '@/models/entities/emoji.js';
|
||||||
@ -197,7 +197,14 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
|
|||||||
const cw = note.summary === '' ? null : note.summary;
|
const cw = note.summary === '' ? null : note.summary;
|
||||||
|
|
||||||
// テキストのパース
|
// テキストのパース
|
||||||
const text = typeof note._misskey_content !== 'undefined' ? note._misskey_content : (note.content ? htmlToMfm(note.content, note.tag) : null);
|
let text: string | null = null;
|
||||||
|
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source?.content === 'string') {
|
||||||
|
text = note.source.content;
|
||||||
|
} else if (typeof note._misskey_content === 'string') {
|
||||||
|
text = note._misskey_content;
|
||||||
|
} else if (typeof note.content === 'string') {
|
||||||
|
text = htmlToMfm(note.content, note.tag);
|
||||||
|
}
|
||||||
|
|
||||||
// vote
|
// vote
|
||||||
if (reply && reply.hasPoll) {
|
if (reply && reply.hasPoll) {
|
||||||
|
@ -1,8 +1,20 @@
|
|||||||
import config from '@/config/index.js';
|
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',
|
* Renders a block into its ActivityPub representation.
|
||||||
actor: `${config.url}/users/${blocker.id}`,
|
*
|
||||||
object: blockee.uri,
|
* @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) => {
|
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 = {
|
const follow = {
|
||||||
|
id: requestId ?? `${config.url}/follows/${follower.id}/${followee.id}`,
|
||||||
type: 'Follow',
|
type: 'Follow',
|
||||||
actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri,
|
actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri,
|
||||||
object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri,
|
object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri,
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
if (requestId) follow.id = requestId;
|
|
||||||
|
|
||||||
return follow;
|
return follow;
|
||||||
};
|
};
|
||||||
|
@ -8,7 +8,7 @@ import { User } from '@/models/entities/user.js';
|
|||||||
export const renderActivity = (x: any): IActivity | null => {
|
export const renderActivity = (x: any): IActivity | null => {
|
||||||
if (x == null) return 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()}`;
|
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 files = await getPromisedFiles(note.fileIds);
|
||||||
|
|
||||||
const text = note.text;
|
// text should never be undefined
|
||||||
|
const text = note.text ?? null;
|
||||||
let poll: Poll | null = null;
|
let poll: Poll | null = null;
|
||||||
|
|
||||||
if (note.hasPoll) {
|
if (note.hasPoll) {
|
||||||
poll = await Polls.findOneBy({ noteId: note.id });
|
poll = await Polls.findOneBy({ noteId: note.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
let apText = text;
|
let apText = text ?? '';
|
||||||
if (apText == null) apText = '';
|
|
||||||
|
|
||||||
if (quote) {
|
if (quote) {
|
||||||
apText += `\n\nRE: ${quote}`;
|
apText += `\n\nRE: ${quote}`;
|
||||||
@ -138,6 +138,10 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
|
|||||||
summary,
|
summary,
|
||||||
content,
|
content,
|
||||||
_misskey_content: text,
|
_misskey_content: text,
|
||||||
|
source: {
|
||||||
|
content: text,
|
||||||
|
mediaType: "text/x.misskeymarkdown",
|
||||||
|
},
|
||||||
_misskey_quote: quote,
|
_misskey_quote: quote,
|
||||||
quoteUrl: quote,
|
quoteUrl: quote,
|
||||||
published: note.createdAt.toISOString(),
|
published: note.createdAt.toISOString(),
|
||||||
|
@ -3,9 +3,18 @@ import { getJson } from '@/misc/fetch.js';
|
|||||||
import { ILocalUser } from '@/models/entities/user.js';
|
import { ILocalUser } from '@/models/entities/user.js';
|
||||||
import { getInstanceActor } from '@/services/instance-actor.js';
|
import { getInstanceActor } from '@/services/instance-actor.js';
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.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 { signedGet } from './request.js';
|
||||||
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.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 {
|
export default class Resolver {
|
||||||
private history: Set<string>;
|
private history: Set<string>;
|
||||||
@ -40,14 +49,25 @@ export default class Resolver {
|
|||||||
return value;
|
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)) {
|
if (this.history.has(value)) {
|
||||||
throw new Error('cannot resolve already resolved one');
|
throw new Error('cannot resolve already resolved one');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.history.add(value);
|
this.history.add(value);
|
||||||
|
|
||||||
const meta = await fetchMeta();
|
|
||||||
const host = extractDbHost(value);
|
const host = extractDbHost(value);
|
||||||
|
if (isSelfHost(host)) {
|
||||||
|
return await this.resolveLocal(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = await fetchMeta();
|
||||||
if (meta.blockedHosts.includes(host)) {
|
if (meta.blockedHosts.includes(host)) {
|
||||||
throw new Error('Instance is blocked');
|
throw new Error('Instance is blocked');
|
||||||
}
|
}
|
||||||
@ -70,4 +90,44 @@ export default class Resolver {
|
|||||||
|
|
||||||
return object;
|
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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,7 +106,10 @@ export const isPost = (object: IObject): object is IPost =>
|
|||||||
|
|
||||||
export interface IPost extends IObject {
|
export interface IPost extends IObject {
|
||||||
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
|
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
|
||||||
_misskey_content?: string;
|
source?: {
|
||||||
|
content: string;
|
||||||
|
mediaType: string;
|
||||||
|
};
|
||||||
_misskey_quote?: string;
|
_misskey_quote?: string;
|
||||||
quoteUrl?: string;
|
quoteUrl?: string;
|
||||||
_misskey_talk: boolean;
|
_misskey_talk: boolean;
|
||||||
@ -114,7 +117,10 @@ export interface IPost extends IObject {
|
|||||||
|
|
||||||
export interface IQuestion extends IObject {
|
export interface IQuestion extends IObject {
|
||||||
type: 'Note' | 'Question';
|
type: 'Note' | 'Question';
|
||||||
_misskey_content?: string;
|
source?: {
|
||||||
|
content: string;
|
||||||
|
mediaType: string;
|
||||||
|
};
|
||||||
_misskey_quote?: string;
|
_misskey_quote?: string;
|
||||||
quoteUrl?: string;
|
quoteUrl?: string;
|
||||||
oneOf?: IQuestionChoice[];
|
oneOf?: IQuestionChoice[];
|
||||||
|
@ -15,9 +15,10 @@ import { inbox as processInbox } from '@/queue/index.js';
|
|||||||
import { isSelfHost } from '@/misc/convert-host.js';
|
import { isSelfHost } from '@/misc/convert-host.js';
|
||||||
import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js';
|
import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js';
|
||||||
import { ILocalUser, User } from '@/models/entities/user.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 { renderLike } from '@/remote/activitypub/renderer/like.js';
|
||||||
import { getUserKeypair } from '@/misc/keypair-store.js';
|
import { getUserKeypair } from '@/misc/keypair-store.js';
|
||||||
|
import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
||||||
|
|
||||||
// Init router
|
// Init router
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
@ -224,4 +225,30 @@ router.get('/likes/:like', async ctx => {
|
|||||||
setResponseType(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;
|
export default router;
|
||||||
|
@ -2,10 +2,11 @@ import Koa from 'koa';
|
|||||||
import { performance } from 'perf_hooks';
|
import { performance } from 'perf_hooks';
|
||||||
import { limiter } from './limiter.js';
|
import { limiter } from './limiter.js';
|
||||||
import { CacheableLocalUser, User } from '@/models/entities/user.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 { ApiError } from './error.js';
|
||||||
import { apiLogger } from './logger.js';
|
import { apiLogger } from './logger.js';
|
||||||
import { AccessToken } from '@/models/entities/access-token.js';
|
import { AccessToken } from '@/models/entities/access-token.js';
|
||||||
|
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||||
|
|
||||||
const accessDenied = {
|
const accessDenied = {
|
||||||
message: 'Access denied.',
|
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) => {
|
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 isSecure = user != null && token == null;
|
||||||
|
const isModerator = user != null && (user.isModerator || user.isAdmin);
|
||||||
|
|
||||||
const ep = endpoints.find(e => e.name === endpoint);
|
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);
|
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) {
|
if (ep.meta.requireCredential && user == null) {
|
||||||
throw new ApiError({
|
throw new ApiError({
|
||||||
message: 'Credential required.',
|
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.' });
|
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.' });
|
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
|
// Cast non JSON input
|
||||||
if (ep.meta.requireFile && ep.params.properties) {
|
if (ep.meta.requireFile && ep.params.properties) {
|
||||||
for (const k of Object.keys(ep.params.properties)) {
|
for (const k of Object.keys(ep.params.properties)) {
|
||||||
|
@ -654,7 +654,6 @@ export interface IEndpointMeta {
|
|||||||
/**
|
/**
|
||||||
* エンドポイントのリミテーションに関するやつ
|
* エンドポイントのリミテーションに関するやつ
|
||||||
* 省略した場合はリミテーションは無いものとして解釈されます。
|
* 省略した場合はリミテーションは無いものとして解釈されます。
|
||||||
* また、withCredential が false の場合はリミテーションを行うことはできません。
|
|
||||||
*/
|
*/
|
||||||
readonly limit?: {
|
readonly limit?: {
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
import { Signins, UserProfiles, Users } from '@/models/index.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
import { Users } from '@/models/index.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@ -23,9 +23,12 @@ export const paramDef = {
|
|||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
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');
|
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');
|
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 {
|
return {
|
||||||
...user,
|
email: profile.email,
|
||||||
token: user.token != null ? '<MASKED>' : user.token,
|
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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -9,6 +9,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'read:drive',
|
kind: 'read:drive',
|
||||||
|
|
||||||
|
description: 'Find the notes to which the given file is attached.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -8,6 +8,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'read:drive',
|
kind: 'read:drive',
|
||||||
|
|
||||||
|
description: 'Check if a given file exists.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -20,6 +20,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:drive',
|
kind: 'write:drive',
|
||||||
|
|
||||||
|
description: 'Upload a new drive file.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -11,6 +11,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:drive',
|
kind: 'write:drive',
|
||||||
|
|
||||||
|
description: 'Delete an existing drive file.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
noSuchFile: {
|
noSuchFile: {
|
||||||
message: 'No such file.',
|
message: 'No such file.',
|
||||||
|
@ -8,6 +8,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'read:drive',
|
kind: 'read:drive',
|
||||||
|
|
||||||
|
description: 'Search for a drive file by a hash of the contents.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -9,6 +9,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'read:drive',
|
kind: 'read:drive',
|
||||||
|
|
||||||
|
description: 'Search for a drive file by the given parameters.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -10,6 +10,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'read:drive',
|
kind: 'read:drive',
|
||||||
|
|
||||||
|
description: 'Show the properties of a drive file.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -11,6 +11,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:drive',
|
kind: 'write:drive',
|
||||||
|
|
||||||
|
description: 'Update the properties of a drive file.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
invalidFileName: {
|
invalidFileName: {
|
||||||
message: 'Invalid file name.',
|
message: 'Invalid file name.',
|
||||||
|
@ -13,6 +13,8 @@ export const meta = {
|
|||||||
max: 60,
|
max: 60,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
description: 'Request the server to download a new drive file from the specified URL.',
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
kind: 'write:drive',
|
kind: 'write:drive',
|
||||||
|
@ -134,7 +134,7 @@ export const paramDef = {
|
|||||||
{
|
{
|
||||||
// (re)note with text, files and poll are optional
|
// (re)note with text, files and poll are optional
|
||||||
properties: {
|
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'],
|
required: ['text'],
|
||||||
},
|
},
|
||||||
|
@ -10,8 +10,12 @@ import { genId } from '@/misc/gen-id.js';
|
|||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
tags: ['reset password'],
|
||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
|
description: 'Request a users password to be reset.',
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
duration: ms('1hour'),
|
duration: ms('1hour'),
|
||||||
max: 3,
|
max: 3,
|
||||||
|
@ -3,8 +3,12 @@ import { ApiError } from '../error.js';
|
|||||||
import { resetDb } from '@/db/postgre.js';
|
import { resetDb } from '@/db/postgre.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
tags: ['non-productive'],
|
||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
|
description: 'Only available when running with <code>NODE_ENV=testing</code>. Reset the database and flush Redis.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
@ -5,8 +5,12 @@ import { Users, UserProfiles, PasswordResetRequests } from '@/models/index.js';
|
|||||||
import { ApiError } from '../error.js';
|
import { ApiError } from '../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
tags: ['reset password'],
|
||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
|
description: 'Complete the password reset that was previously requested.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
@ -8,6 +8,8 @@ export const meta = {
|
|||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
description: 'Register to receive push notifications.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -5,6 +5,8 @@ export const meta = {
|
|||||||
tags: ['account'],
|
tags: ['account'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
description: 'Unregister from receiving push notifications.',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import define from '../define.js';
|
import define from '../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
tags: ['non-productive'],
|
||||||
|
|
||||||
|
description: 'Endpoint for testing input validation.',
|
||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -4,6 +4,18 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
|||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users', 'clips'],
|
tags: ['users', 'clips'],
|
||||||
|
|
||||||
|
description: 'Show all clips this user owns.',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'Clip',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -10,6 +10,8 @@ export const meta = {
|
|||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
|
description: 'Show everyone that follows this user.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -10,6 +10,8 @@ export const meta = {
|
|||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
|
description: 'Show everyone that this user is following.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -4,6 +4,18 @@ import { makePaginationQuery } from '../../../common/make-pagination-query.js';
|
|||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users', 'gallery'],
|
tags: ['users', 'gallery'],
|
||||||
|
|
||||||
|
description: 'Show all gallery posts by the given user.',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'GalleryPost',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -10,6 +10,8 @@ export const meta = {
|
|||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
|
description: 'Get a list of other users that the specified user frequently replies to.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -11,6 +11,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:user-groups',
|
kind: 'write:user-groups',
|
||||||
|
|
||||||
|
description: 'Create a new group.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -9,6 +9,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:user-groups',
|
kind: 'write:user-groups',
|
||||||
|
|
||||||
|
description: 'Delete an existing group.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
noSuchGroup: {
|
noSuchGroup: {
|
||||||
message: 'No such group.',
|
message: 'No such group.',
|
||||||
|
@ -11,6 +11,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:user-groups',
|
kind: 'write:user-groups',
|
||||||
|
|
||||||
|
description: 'Join a group the authenticated user has been invited to.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
noSuchInvitation: {
|
noSuchInvitation: {
|
||||||
message: 'No such invitation.',
|
message: 'No such invitation.',
|
||||||
|
@ -9,6 +9,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:user-groups',
|
kind: 'write:user-groups',
|
||||||
|
|
||||||
|
description: 'Delete an existing group invitation for the authenticated user without joining the group.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
noSuchInvitation: {
|
noSuchInvitation: {
|
||||||
message: 'No such invitation.',
|
message: 'No such invitation.',
|
||||||
|
@ -13,6 +13,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:user-groups',
|
kind: 'write:user-groups',
|
||||||
|
|
||||||
|
description: 'Invite a user to an existing group.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
noSuchGroup: {
|
noSuchGroup: {
|
||||||
message: 'No such group.',
|
message: 'No such group.',
|
||||||
|
@ -9,6 +9,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'read:user-groups',
|
kind: 'read:user-groups',
|
||||||
|
|
||||||
|
description: 'List the groups that the authenticated user is a member of.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -9,6 +9,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:user-groups',
|
kind: 'write:user-groups',
|
||||||
|
|
||||||
|
description: 'Leave a group. The owner of a group can not leave. They must transfer ownership or delete the group instead.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
noSuchGroup: {
|
noSuchGroup: {
|
||||||
message: 'No such group.',
|
message: 'No such group.',
|
||||||
|
@ -8,6 +8,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'read:user-groups',
|
kind: 'read:user-groups',
|
||||||
|
|
||||||
|
description: 'List the groups that the authenticated user is the owner of.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -10,6 +10,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:user-groups',
|
kind: 'write:user-groups',
|
||||||
|
|
||||||
|
description: 'Removes a specified user from a group. The owner can not be removed.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
noSuchGroup: {
|
noSuchGroup: {
|
||||||
message: 'No such group.',
|
message: 'No such group.',
|
||||||
|
@ -9,6 +9,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'read:user-groups',
|
kind: 'read:user-groups',
|
||||||
|
|
||||||
|
description: 'Show the properties of a group.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -10,6 +10,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:user-groups',
|
kind: 'write:user-groups',
|
||||||
|
|
||||||
|
description: 'Transfer ownership of a group from the authenticated user to another user.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -9,6 +9,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:user-groups',
|
kind: 'write:user-groups',
|
||||||
|
|
||||||
|
description: 'Update the properties of a group.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -10,6 +10,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
|
description: 'Create a new list of users.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -9,6 +9,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
|
description: 'Delete an existing list of users.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
noSuchList: {
|
noSuchList: {
|
||||||
message: 'No such list.',
|
message: 'No such list.',
|
||||||
|
@ -8,6 +8,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'read:account',
|
kind: 'read:account',
|
||||||
|
|
||||||
|
description: 'Show all lists that the authenticated user has created.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -11,6 +11,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
|
description: 'Remove a user from a list.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
noSuchList: {
|
noSuchList: {
|
||||||
message: 'No such list.',
|
message: 'No such list.',
|
||||||
|
@ -11,6 +11,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
|
description: 'Add a user to an existing list.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
noSuchList: {
|
noSuchList: {
|
||||||
message: 'No such list.',
|
message: 'No such list.',
|
||||||
|
@ -9,6 +9,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'read:account',
|
kind: 'read:account',
|
||||||
|
|
||||||
|
description: 'Show the properties of a list.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -9,6 +9,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
|
description: 'Update the properties of a list.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -12,6 +12,8 @@ import { generateMutedInstanceQuery } from '../../common/generate-muted-instance
|
|||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users', 'notes'],
|
tags: ['users', 'notes'],
|
||||||
|
|
||||||
|
description: 'Show all notes that this user created.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -4,6 +4,18 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
|||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users', 'pages'],
|
tags: ['users', 'pages'],
|
||||||
|
|
||||||
|
description: 'Show all pages this user created.',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'Page',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -9,6 +9,8 @@ export const meta = {
|
|||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
|
description: 'Show all reactions this user made.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -11,6 +11,8 @@ export const meta = {
|
|||||||
|
|
||||||
kind: 'read:account',
|
kind: 'read:account',
|
||||||
|
|
||||||
|
description: 'Show users that the authenticated user might be interested to follow.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -6,6 +6,8 @@ export const meta = {
|
|||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
description: 'Show the different kinds of relations between the authenticated user and the specified user(s).',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
oneOf: [
|
oneOf: [
|
||||||
|
@ -13,6 +13,8 @@ export const meta = {
|
|||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
description: 'File a report.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
noSuchUser: {
|
noSuchUser: {
|
||||||
message: 'No such user.',
|
message: 'No such user.',
|
||||||
|
@ -9,6 +9,8 @@ export const meta = {
|
|||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
|
description: 'Search for a user by username and/or host.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -8,6 +8,8 @@ export const meta = {
|
|||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
|
description: 'Search for users.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -11,6 +11,8 @@ export const meta = {
|
|||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
|
description: 'Show the properties of a user.',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
oneOf: [
|
oneOf: [
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index.js';
|
import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index.js';
|
||||||
|
import { awaitAll } from '@/prelude/await-all.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users'],
|
tags: ['users'],
|
||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
|
description: 'Show statistics about a user.',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
noSuchUser: {
|
noSuchUser: {
|
||||||
message: 'No such user.',
|
message: 'No such user.',
|
||||||
@ -14,6 +17,94 @@ export const meta = {
|
|||||||
id: '9e638e45-3b25-4ef7-8f95-07e8498f1819',
|
id: '9e638e45-3b25-4ef7-8f95-07e8498f1819',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
notesCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
repliesCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
renotesCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
repliedCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
renotedCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
pollVotesCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
pollVotedCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
localFollowingCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
remoteFollowingCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
localFollowersCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
remoteFollowersCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
followingCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
followersCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
sentReactionsCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
receivedReactionsCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
noteFavoritesCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
pageLikesCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
pageLikedCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
driveFilesCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
driveUsage: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
description: 'Drive usage in bytes',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
@ -31,109 +122,72 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||||||
throw new ApiError(meta.errors.noSuchUser);
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [
|
const result = await awaitAll({
|
||||||
notesCount,
|
notesCount: Notes.createQueryBuilder('note')
|
||||||
repliesCount,
|
|
||||||
renotesCount,
|
|
||||||
repliedCount,
|
|
||||||
renotedCount,
|
|
||||||
pollVotesCount,
|
|
||||||
pollVotedCount,
|
|
||||||
localFollowingCount,
|
|
||||||
remoteFollowingCount,
|
|
||||||
localFollowersCount,
|
|
||||||
remoteFollowersCount,
|
|
||||||
sentReactionsCount,
|
|
||||||
receivedReactionsCount,
|
|
||||||
noteFavoritesCount,
|
|
||||||
pageLikesCount,
|
|
||||||
pageLikedCount,
|
|
||||||
driveFilesCount,
|
|
||||||
driveUsage,
|
|
||||||
] = await Promise.all([
|
|
||||||
Notes.createQueryBuilder('note')
|
|
||||||
.where('note.userId = :userId', { userId: user.id })
|
.where('note.userId = :userId', { userId: user.id })
|
||||||
.getCount(),
|
.getCount(),
|
||||||
Notes.createQueryBuilder('note')
|
repliesCount: Notes.createQueryBuilder('note')
|
||||||
.where('note.userId = :userId', { userId: user.id })
|
.where('note.userId = :userId', { userId: user.id })
|
||||||
.andWhere('note.replyId IS NOT NULL')
|
.andWhere('note.replyId IS NOT NULL')
|
||||||
.getCount(),
|
.getCount(),
|
||||||
Notes.createQueryBuilder('note')
|
renotesCount: Notes.createQueryBuilder('note')
|
||||||
.where('note.userId = :userId', { userId: user.id })
|
.where('note.userId = :userId', { userId: user.id })
|
||||||
.andWhere('note.renoteId IS NOT NULL')
|
.andWhere('note.renoteId IS NOT NULL')
|
||||||
.getCount(),
|
.getCount(),
|
||||||
Notes.createQueryBuilder('note')
|
repliedCount: Notes.createQueryBuilder('note')
|
||||||
.where('note.replyUserId = :userId', { userId: user.id })
|
.where('note.replyUserId = :userId', { userId: user.id })
|
||||||
.getCount(),
|
.getCount(),
|
||||||
Notes.createQueryBuilder('note')
|
renotedCount: Notes.createQueryBuilder('note')
|
||||||
.where('note.renoteUserId = :userId', { userId: user.id })
|
.where('note.renoteUserId = :userId', { userId: user.id })
|
||||||
.getCount(),
|
.getCount(),
|
||||||
PollVotes.createQueryBuilder('vote')
|
pollVotesCount: PollVotes.createQueryBuilder('vote')
|
||||||
.where('vote.userId = :userId', { userId: user.id })
|
.where('vote.userId = :userId', { userId: user.id })
|
||||||
.getCount(),
|
.getCount(),
|
||||||
PollVotes.createQueryBuilder('vote')
|
pollVotedCount: PollVotes.createQueryBuilder('vote')
|
||||||
.innerJoin('vote.note', 'note')
|
.innerJoin('vote.note', 'note')
|
||||||
.where('note.userId = :userId', { userId: user.id })
|
.where('note.userId = :userId', { userId: user.id })
|
||||||
.getCount(),
|
.getCount(),
|
||||||
Followings.createQueryBuilder('following')
|
localFollowingCount: Followings.createQueryBuilder('following')
|
||||||
.where('following.followerId = :userId', { userId: user.id })
|
.where('following.followerId = :userId', { userId: user.id })
|
||||||
.andWhere('following.followeeHost IS NULL')
|
.andWhere('following.followeeHost IS NULL')
|
||||||
.getCount(),
|
.getCount(),
|
||||||
Followings.createQueryBuilder('following')
|
remoteFollowingCount: Followings.createQueryBuilder('following')
|
||||||
.where('following.followerId = :userId', { userId: user.id })
|
.where('following.followerId = :userId', { userId: user.id })
|
||||||
.andWhere('following.followeeHost IS NOT NULL')
|
.andWhere('following.followeeHost IS NOT NULL')
|
||||||
.getCount(),
|
.getCount(),
|
||||||
Followings.createQueryBuilder('following')
|
localFollowersCount: Followings.createQueryBuilder('following')
|
||||||
.where('following.followeeId = :userId', { userId: user.id })
|
.where('following.followeeId = :userId', { userId: user.id })
|
||||||
.andWhere('following.followerHost IS NULL')
|
.andWhere('following.followerHost IS NULL')
|
||||||
.getCount(),
|
.getCount(),
|
||||||
Followings.createQueryBuilder('following')
|
remoteFollowersCount: Followings.createQueryBuilder('following')
|
||||||
.where('following.followeeId = :userId', { userId: user.id })
|
.where('following.followeeId = :userId', { userId: user.id })
|
||||||
.andWhere('following.followerHost IS NOT NULL')
|
.andWhere('following.followerHost IS NOT NULL')
|
||||||
.getCount(),
|
.getCount(),
|
||||||
NoteReactions.createQueryBuilder('reaction')
|
sentReactionsCount: NoteReactions.createQueryBuilder('reaction')
|
||||||
.where('reaction.userId = :userId', { userId: user.id })
|
.where('reaction.userId = :userId', { userId: user.id })
|
||||||
.getCount(),
|
.getCount(),
|
||||||
NoteReactions.createQueryBuilder('reaction')
|
receivedReactionsCount: NoteReactions.createQueryBuilder('reaction')
|
||||||
.innerJoin('reaction.note', 'note')
|
.innerJoin('reaction.note', 'note')
|
||||||
.where('note.userId = :userId', { userId: user.id })
|
.where('note.userId = :userId', { userId: user.id })
|
||||||
.getCount(),
|
.getCount(),
|
||||||
NoteFavorites.createQueryBuilder('favorite')
|
noteFavoritesCount: NoteFavorites.createQueryBuilder('favorite')
|
||||||
.where('favorite.userId = :userId', { userId: user.id })
|
.where('favorite.userId = :userId', { userId: user.id })
|
||||||
.getCount(),
|
.getCount(),
|
||||||
PageLikes.createQueryBuilder('like')
|
pageLikesCount: PageLikes.createQueryBuilder('like')
|
||||||
.where('like.userId = :userId', { userId: user.id })
|
.where('like.userId = :userId', { userId: user.id })
|
||||||
.getCount(),
|
.getCount(),
|
||||||
PageLikes.createQueryBuilder('like')
|
pageLikedCount: PageLikes.createQueryBuilder('like')
|
||||||
.innerJoin('like.page', 'page')
|
.innerJoin('like.page', 'page')
|
||||||
.where('page.userId = :userId', { userId: user.id })
|
.where('page.userId = :userId', { userId: user.id })
|
||||||
.getCount(),
|
.getCount(),
|
||||||
DriveFiles.createQueryBuilder('file')
|
driveFilesCount: DriveFiles.createQueryBuilder('file')
|
||||||
.where('file.userId = :userId', { userId: user.id })
|
.where('file.userId = :userId', { userId: user.id })
|
||||||
.getCount(),
|
.getCount(),
|
||||||
DriveFiles.calcDriveUsageOf(user),
|
driveUsage: DriveFiles.calcDriveUsageOf(user),
|
||||||
]);
|
});
|
||||||
|
|
||||||
return {
|
result.followingCount = result.localFollowingCount + result.remoteFollowingCount;
|
||||||
notesCount,
|
result.followersCount = result.localFollowersCount + result.remoteFollowersCount;
|
||||||
repliesCount,
|
|
||||||
renotesCount,
|
return result;
|
||||||
repliedCount,
|
|
||||||
renotedCount,
|
|
||||||
pollVotesCount,
|
|
||||||
pollVotedCount,
|
|
||||||
localFollowingCount,
|
|
||||||
remoteFollowingCount,
|
|
||||||
localFollowersCount,
|
|
||||||
remoteFollowersCount,
|
|
||||||
followingCount: localFollowingCount + remoteFollowingCount,
|
|
||||||
followersCount: localFollowersCount + remoteFollowersCount,
|
|
||||||
sentReactionsCount,
|
|
||||||
receivedReactionsCount,
|
|
||||||
noteFavoritesCount,
|
|
||||||
pageLikesCount,
|
|
||||||
pageLikedCount,
|
|
||||||
driveFilesCount,
|
|
||||||
driveUsage,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
@ -1,25 +1,17 @@
|
|||||||
import Limiter from 'ratelimiter';
|
import Limiter from 'ratelimiter';
|
||||||
import { redisClient } from '../../db/redis.js';
|
import { redisClient } from '../../db/redis.js';
|
||||||
import { IEndpoint } from './endpoints.js';
|
import { IEndpointMeta } from './endpoints.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
|
||||||
import { CacheableLocalUser, User } from '@/models/entities/user.js';
|
import { CacheableLocalUser, User } from '@/models/entities/user.js';
|
||||||
import Logger from '@/services/logger.js';
|
import Logger from '@/services/logger.js';
|
||||||
|
|
||||||
const logger = new Logger('limiter');
|
const logger = new Logger('limiter');
|
||||||
|
|
||||||
export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user: CacheableLocalUser) => new Promise<void>((ok, reject) => {
|
export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string) => new Promise<void>((ok, reject) => {
|
||||||
const limitation = endpoint.meta.limit;
|
const hasShortTermLimit = typeof limitation.minInterval === 'number';
|
||||||
|
|
||||||
const key = Object.prototype.hasOwnProperty.call(limitation, 'key')
|
|
||||||
? limitation.key
|
|
||||||
: endpoint.name;
|
|
||||||
|
|
||||||
const hasShortTermLimit =
|
|
||||||
Object.prototype.hasOwnProperty.call(limitation, 'minInterval');
|
|
||||||
|
|
||||||
const hasLongTermLimit =
|
const hasLongTermLimit =
|
||||||
Object.prototype.hasOwnProperty.call(limitation, 'duration') &&
|
typeof limitation.duration === 'number' &&
|
||||||
Object.prototype.hasOwnProperty.call(limitation, 'max');
|
typeof limitation.max === 'number';
|
||||||
|
|
||||||
if (hasShortTermLimit) {
|
if (hasShortTermLimit) {
|
||||||
min();
|
min();
|
||||||
@ -32,7 +24,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
|
|||||||
// Short-term limit
|
// Short-term limit
|
||||||
function min(): void {
|
function min(): void {
|
||||||
const minIntervalLimiter = new Limiter({
|
const minIntervalLimiter = new Limiter({
|
||||||
id: `${user.id}:${key}:min`,
|
id: `${actor}:${limitation.key}:min`,
|
||||||
duration: limitation.minInterval,
|
duration: limitation.minInterval,
|
||||||
max: 1,
|
max: 1,
|
||||||
db: redisClient,
|
db: redisClient,
|
||||||
@ -43,7 +35,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
|
|||||||
return reject('ERR');
|
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) {
|
if (info.remaining === 0) {
|
||||||
reject('BRIEF_REQUEST_INTERVAL');
|
reject('BRIEF_REQUEST_INTERVAL');
|
||||||
@ -60,7 +52,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
|
|||||||
// Long term limit
|
// Long term limit
|
||||||
function max(): void {
|
function max(): void {
|
||||||
const limiter = new Limiter({
|
const limiter = new Limiter({
|
||||||
id: `${user.id}:${key}`,
|
id: `${actor}:${limitation.key}`,
|
||||||
duration: limitation.duration,
|
duration: limitation.duration,
|
||||||
max: limitation.max,
|
max: limitation.max,
|
||||||
db: redisClient,
|
db: redisClient,
|
||||||
@ -71,7 +63,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
|
|||||||
return reject('ERR');
|
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) {
|
if (info.remaining === 0) {
|
||||||
reject('RATE_LIMIT_EXCEEDED');
|
reject('RATE_LIMIT_EXCEEDED');
|
||||||
|
@ -1,25 +1,22 @@
|
|||||||
import { randomBytes } from 'node:crypto';
|
|
||||||
import Koa from 'koa';
|
import Koa from 'koa';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import * as speakeasy from 'speakeasy';
|
import * as speakeasy from 'speakeasy';
|
||||||
import { IsNull } from 'typeorm';
|
import signin from '../common/signin.js';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index.js';
|
import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index.js';
|
||||||
import { ILocalUser } from '@/models/entities/user.js';
|
import { ILocalUser } from '@/models/entities/user.js';
|
||||||
import { genId } from '@/misc/gen-id.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 { 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) => {
|
export default async (ctx: Koa.Context) => {
|
||||||
ctx.set('Access-Control-Allow-Origin', config.url);
|
ctx.set('Access-Control-Allow-Origin', config.url);
|
||||||
ctx.set('Access-Control-Allow-Credentials', 'true');
|
ctx.set('Access-Control-Allow-Credentials', 'true');
|
||||||
|
|
||||||
const body = ctx.request.body as any;
|
const body = ctx.request.body as any;
|
||||||
|
|
||||||
const instance = await fetchMeta(true);
|
|
||||||
|
|
||||||
const username = body['username'];
|
const username = body['username'];
|
||||||
const password = body['password'];
|
const password = body['password'];
|
||||||
const token = body['token'];
|
const token = body['token'];
|
||||||
@ -29,6 +26,21 @@ export default async (ctx: Koa.Context) => {
|
|||||||
ctx.body = { error };
|
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') {
|
if (typeof username !== 'string') {
|
||||||
ctx.status = 400;
|
ctx.status = 400;
|
||||||
return;
|
return;
|
||||||
@ -84,18 +96,6 @@ export default async (ctx: Koa.Context) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!profile.twoFactorEnabled) {
|
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) {
|
if (same) {
|
||||||
signin(ctx, user);
|
signin(ctx, user);
|
||||||
return;
|
return;
|
||||||
@ -172,7 +172,7 @@ export default async (ctx: Koa.Context) => {
|
|||||||
body.credentialId
|
body.credentialId
|
||||||
.replace(/-/g, '+')
|
.replace(/-/g, '+')
|
||||||
.replace(/_/g, '/'),
|
.replace(/_/g, '/'),
|
||||||
'base64',
|
'base64'
|
||||||
).toString('hex'),
|
).toString('hex'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,14 +54,10 @@
|
|||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Script
|
//#region Script
|
||||||
const salt = localStorage.getItem('salt')
|
import(`/assets/${CLIENT_ENTRY}`)
|
||||||
? `?salt=${localStorage.getItem('salt')}`
|
.catch(async e => {
|
||||||
: '';
|
|
||||||
|
|
||||||
import(`/assets/${CLIENT_ENTRY}${salt}`)
|
|
||||||
.catch(async () => {
|
|
||||||
await checkUpdate();
|
await checkUpdate();
|
||||||
renderError('APP_FETCH_FAILED');
|
renderError('APP_FETCH_FAILED', JSON.stringify(e));
|
||||||
})
|
})
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
@ -142,9 +138,6 @@
|
|||||||
|
|
||||||
// eslint-disable-next-line no-inner-declarations
|
// eslint-disable-next-line no-inner-declarations
|
||||||
function refresh() {
|
function refresh() {
|
||||||
// Random
|
|
||||||
localStorage.setItem('salt', Math.random().toString().substr(2, 8));
|
|
||||||
|
|
||||||
// Clear cache (service worker)
|
// Clear cache (service worker)
|
||||||
try {
|
try {
|
||||||
navigator.serviceWorker.controller.postMessage('clear');
|
navigator.serviceWorker.controller.postMessage('clear');
|
||||||
|
@ -74,9 +74,9 @@ app.use(views(_dirname + '/views', {
|
|||||||
extension: 'pug',
|
extension: 'pug',
|
||||||
options: {
|
options: {
|
||||||
version: config.version,
|
version: config.version,
|
||||||
clientEntry: () => process.env.NODE_ENV === 'production' ?
|
getClientEntry: () => process.env.NODE_ENV === 'production' ?
|
||||||
config.clientEntry :
|
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,
|
config,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@ -247,7 +247,7 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
|
|||||||
icon: meta.iconUrl,
|
icon: meta.iconUrl,
|
||||||
themeColor: meta.themeColor,
|
themeColor: meta.themeColor,
|
||||||
});
|
});
|
||||||
ctx.set('Cache-Control', 'public, max-age=30');
|
ctx.set('Cache-Control', 'public, max-age=15');
|
||||||
} else {
|
} else {
|
||||||
// リモートユーザーなので
|
// リモートユーザーなので
|
||||||
// モデレータがAPI経由で参照可能にするために404にはしない
|
// モデレータがAPI経由で参照可能にするために404にはしない
|
||||||
@ -292,7 +292,7 @@ router.get('/notes/:note', async (ctx, next) => {
|
|||||||
themeColor: meta.themeColor,
|
themeColor: meta.themeColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'public, max-age=15');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -329,7 +329,7 @@ router.get('/@:user/pages/:page', async (ctx, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (['public'].includes(page.visibility)) {
|
if (['public'].includes(page.visibility)) {
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'public, max-age=15');
|
||||||
} else {
|
} else {
|
||||||
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
|
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
|
||||||
}
|
}
|
||||||
@ -360,7 +360,7 @@ router.get('/clips/:clip', async (ctx, next) => {
|
|||||||
themeColor: meta.themeColor,
|
themeColor: meta.themeColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'public, max-age=15');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -385,7 +385,7 @@ router.get('/gallery/:post', async (ctx, next) => {
|
|||||||
themeColor: meta.themeColor,
|
themeColor: meta.themeColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'public, max-age=15');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -409,7 +409,7 @@ router.get('/channels/:channel', async (ctx, next) => {
|
|||||||
themeColor: meta.themeColor,
|
themeColor: meta.themeColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'public, max-age=15');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -468,7 +468,7 @@ router.get('(.*)', async ctx => {
|
|||||||
icon: meta.iconUrl,
|
icon: meta.iconUrl,
|
||||||
themeColor: meta.themeColor,
|
themeColor: meta.themeColor,
|
||||||
});
|
});
|
||||||
ctx.set('Cache-Control', 'public, max-age=300');
|
ctx.set('Cache-Control', 'public, max-age=15');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register router
|
// Register router
|
||||||
|
@ -3,7 +3,9 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
|
|||||||
import manifest from './manifest.json' assert { type: 'json' };
|
import manifest from './manifest.json' assert { type: 'json' };
|
||||||
|
|
||||||
export const manifestHandler = async (ctx: Koa.Context) => {
|
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);
|
const instance = await fetchMeta(true);
|
||||||
|
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
block vars
|
block vars
|
||||||
|
|
||||||
|
block loadClientEntry
|
||||||
|
- const clientEntry = getClientEntry();
|
||||||
|
|
||||||
doctype html
|
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'
|
Thank you for using Misskey!
|
||||||
!= ' https://github.com/misskey-dev/misskey'
|
If you are reading this message... how about joining the development?
|
||||||
!= '\n-->\n'
|
https://github.com/misskey-dev/misskey
|
||||||
|
|
||||||
|
|
||||||
html
|
html
|
||||||
|
|
||||||
@ -30,8 +34,14 @@ html
|
|||||||
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
|
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/not-found.jpg')
|
||||||
link(rel='prefetch' href='https://xn--931a.moe/assets/error.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='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
|
title
|
||||||
block title
|
block title
|
||||||
@ -52,7 +62,7 @@ html
|
|||||||
|
|
||||||
script.
|
script.
|
||||||
var VERSION = "#{version}";
|
var VERSION = "#{version}";
|
||||||
var CLIENT_ENTRY = "#{clientEntry()}";
|
var CLIENT_ENTRY = "#{clientEntry.file}";
|
||||||
|
|
||||||
script
|
script
|
||||||
include ../boot.js
|
include ../boot.js
|
||||||
|
@ -2,9 +2,10 @@ import { publishMainStream, publishUserEvent } from '@/services/stream.js';
|
|||||||
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||||
import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
||||||
import renderUndo from '@/remote/activitypub/renderer/undo.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 { deliver } from '@/queue/index.js';
|
||||||
import renderReject from '@/remote/activitypub/renderer/reject.js';
|
import renderReject from '@/remote/activitypub/renderer/reject.js';
|
||||||
|
import { Blocking } from '@/models/entities/blocking.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index.js';
|
import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index.js';
|
||||||
import { perUserFollowingChart } from '@/services/chart/index.js';
|
import { perUserFollowingChart } from '@/services/chart/index.js';
|
||||||
@ -22,15 +23,19 @@ export default async function(blocker: User, blockee: User) {
|
|||||||
removeFromList(blockee, blocker),
|
removeFromList(blockee, blocker),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await Blockings.insert({
|
const blocking = {
|
||||||
id: genId(),
|
id: genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
blocker,
|
||||||
blockerId: blocker.id,
|
blockerId: blocker.id,
|
||||||
|
blockee,
|
||||||
blockeeId: blockee.id,
|
blockeeId: blockee.id,
|
||||||
});
|
} as Blocking;
|
||||||
|
|
||||||
|
await Blockings.insert(blocking);
|
||||||
|
|
||||||
if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
|
if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
|
||||||
const content = renderActivity(renderBlock(blocker, blockee));
|
const content = renderActivity(renderBlock(blocking));
|
||||||
deliver(blocker, content, blockee.inbox);
|
deliver(blocker, content, blockee.inbox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user