diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.md b/.github/ISSUE_TEMPLATE/01_bug-report.md index 8734fc0c3..0fecce2ee 100644 --- a/.github/ISSUE_TEMPLATE/01_bug-report.md +++ b/.github/ISSUE_TEMPLATE/01_bug-report.md @@ -22,7 +22,10 @@ First, in order to avoid duplicate Issues, please search to see if the problem y ## 🤬 Actual Behavior - + ## 📝 Steps to Reproduce diff --git a/.github/labeler.yml b/.github/labeler.yml index dff393557..98f1d2e38 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -4,5 +4,9 @@ '🖥️Client': - packages/client/**/* +'🧪Test': +- cypress/**/* +- packages/backend/test/**/* + '‼️ wrong locales': - any: ['locales/*.yml', '!locales/ja-JP.yml'] diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 057208eda..fa4a58c3a 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,6 +1,8 @@ name: "Pull Request Labeler" on: -- pull_request_target + pull_request_target: + branches-ignore: + - 'l10n_develop' jobs: triage: diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml new file mode 100644 index 000000000..87af3a6ba --- /dev/null +++ b/.github/workflows/ok-to-test.yml @@ -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 diff --git a/.github/workflows/pr-preview-deploy.yml b/.github/workflows/pr-preview-deploy.yml new file mode 100644 index 000000000..fd43bce9e --- /dev/null +++ b/.github/workflows/pr-preview-deploy.yml @@ -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' + + # + - 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; diff --git a/.github/workflows/pr-preview-destroy.yml b/.github/workflows/pr-preview-destroy.yml new file mode 100644 index 000000000..c14c3db5c --- /dev/null +++ b/.github/workflows/pr-preview-destroy.yml @@ -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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d858daa7..c32c82e2a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -103,7 +103,7 @@ jobs: - name: ALSA Env run: echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc - name: Cypress run - uses: cypress-io/github-action@v2 + uses: cypress-io/github-action@v4 with: install: false start: npm run start:test diff --git a/.node-version b/.node-version index 658984787..7fd023741 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -v18.0.0 +v16.15.0 diff --git a/okteto.yml b/.okteto/okteto-pipeline.yml similarity index 100% rename from okteto.yml rename to .okteto/okteto-pipeline.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 433e28fbd..46de744e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,6 @@ You should also include the user name that made the change. ## 12.x.x (unreleased) ### Improvements -- Client: Preferences Registry -======= ### NOTE - From this version, Node 18.0.0 or later is required. @@ -28,25 +26,73 @@ You should also include the user name that made the change. - enhance: display URL of QR code for TOTP registration @syuilo - make CAPTCHA required for signin to improve security @syuilo - enhance: Supports Unicode Emoji 14.0 @mei23 +======= +- Server: Add rate limit to i/notifications @tamaina +- Client: Improve files page of control panel @syuilo +- Improve player detection in URL preview @mei23 +- Add Badge Image to Push Notification #8012 @tamaina + +### Bugfixes +- Server: Fix GenerateVideoThumbnail failed @mei23 +- Server: Ensure temp directory cleanup @Johann150 +- favicons of federated instances not showing @syuilo + +## 12.111.1 (2022/06/13) + +### Bugfixes +- some fixes of multiple notification read @tamaina +- some GenerateVideoThumbnail failed @Johann150 +- Client: デッキでウィジェットの情報が保存されない問題を修正 @syuilo +- Client: ギャラリーの投稿を開こうとすると編集画面が表示される @futchitwo + +## 12.111.0 (2022/06/11) +### Note +- Node.js 16.15.0 or later is required + +### Improvements +- Supports Unicode Emoji 14.0 @mei23 +- プッシュ通知を複数アカウント対応に #7667 @tamaina +- プッシュ通知にクリックやactionを設定 #7667 @tamaina +- ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina +- Server: always remove completed tasks of job queue @Johann150 +- Client: アバターの設定で画像をクロップできるように @syuilo +- Client: make emoji stand out more on reaction button @Johann150 +- Client: display URL of QR code for TOTP registration @tamaina +- 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 Your own theme color may be unset if it was in an invalid format. Admins should check their instance settings if in doubt. - Perform port diagnosis at startup only when Listen fails @mei23 +- Rate limiting is now also usable for non-authenticated users. @Johann150 @mei23 + Admins should make sure the reverse proxy sets the `X-Forwarded-For` header to the original address. ### Bugfixes -- Client: fix settings page @tamaina -- Client: fix profile tabs @futchitwo -- Server: 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: keep file order of note attachement @Johann150 - Server: fix missing foreign key for reports leading to reports page being unusable @Johann150 - Server: fix internal in-memory caching @Johann150 -- Server: use correct order of attachments on notes @Johann150 - Server: prevent crash when processing certain PNGs @syuilo - Server: Fix unable to generate video thumbnails @mei23 +- Server: Fix `Cannot find module` issue @mei23 +- Federation: Add rel attribute to host-meta @mei23 +- Federation: add id for activitypub follows @Johann150 +- Federation: use `source` instead of `_misskey_content` @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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 135a3e140..e4a5d48cf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,20 +66,27 @@ Be willing to comment on the good points and not just the things you want fixed - Are there any omissions or gaps? - 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= +``` +An actual domain will be assigned so you can test the federation. + ## Merge -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. ## Release -For now, basically only @syuilo has the authority to release Misskey. -However, in case of emergency, a release can be made at the discretion of a contributor. - ### Release Instructions -1. commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json)) -2. follow the `master` branch to the `develop` branch. -3. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases) - - The target branch must be `master` - - The tag name must be the version +1. Commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json)) +2. Create a release PR. + - Into `master` from `develop` branch. + - The title must be in the format `Release: x.y.z`. + - `x.y.z` is the new version you are trying to release. +3. Deploy and perform a simple QA check. Also verify that the tests passed. +4. Merge it. +5. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases) + - The target branch must be `master` + - The tag name must be the version ## Localization (l10n) Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management. diff --git a/README.md b/README.md index 4b7b3273d..c27327064 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ With Misskey's built in drive, you get cloud storage right in your social media,
+## 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
RSS3 diff --git a/ROADMAP.md b/ROADMAP.md index 3ccc098d3..bd96b2c50 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -19,6 +19,7 @@ This is the phase we are at now. We need to make a high-maintenance environment ## (2) Improve functionality Once Phase 1 is complete and an environment conducive to the development of a stable system is in place, the implementation of new functions can begin gradually. +- Improve features for moderation - OAuth2 support https://github.com/misskey-dev/misskey/issues/8262 - GraphQL support? diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 000000000..e390c41a5 --- /dev/null +++ b/cypress.config.ts @@ -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', + }, +}) diff --git a/cypress.json b/cypress.json deleted file mode 100644 index e858e480b..000000000 --- a/cypress.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "baseUrl": "http://localhost:61812" -} diff --git a/cypress/integration/basic.js b/cypress/e2e/basic.cy.js similarity index 72% rename from cypress/integration/basic.js rename to cypress/e2e/basic.cy.js index eb15cfe22..eb5195c4b 100644 --- a/cypress/integration/basic.js +++ b/cypress/e2e/basic.cy.js @@ -1,11 +1,6 @@ describe('Before setup instance', () => { beforeEach(() => { - cy.window(win => { - win.indexedDB.deleteDatabase('keyval-store'); - }); - cy.request('POST', '/api/reset-db').as('reset'); - cy.get('@reset').its('status').should('equal', 204); - cy.reload(true); + cy.resetState(); }); afterEach(() => { @@ -35,18 +30,10 @@ describe('Before setup instance', () => { describe('After setup instance', () => { beforeEach(() => { - cy.window(win => { - win.indexedDB.deleteDatabase('keyval-store'); - }); - cy.request('POST', '/api/reset-db').as('reset'); - cy.get('@reset').its('status').should('equal', 204); - cy.reload(true); + cy.resetState(); // インスタンス初期セットアップ - cy.request('POST', '/api/admin/accounts/create', { - username: 'admin', - password: 'pass', - }).its('body').as('admin'); + cy.registerUser('admin', 'pass', true); }); afterEach(() => { @@ -76,24 +63,13 @@ describe('After setup instance', () => { describe('After user signup', () => { beforeEach(() => { - cy.window(win => { - win.indexedDB.deleteDatabase('keyval-store'); - }); - cy.request('POST', '/api/reset-db').as('reset'); - cy.get('@reset').its('status').should('equal', 204); - cy.reload(true); + cy.resetState(); // インスタンス初期セットアップ - cy.request('POST', '/api/admin/accounts/create', { - username: 'admin', - password: 'pass', - }).its('body').as('admin'); + cy.registerUser('admin', 'pass', true); // ユーザー作成 - cy.request('POST', '/api/signup', { - username: 'alice', - password: 'alice1234', - }).its('body').as('alice'); + cy.registerUser('alice', 'alice1234'); }); afterEach(() => { @@ -138,34 +114,15 @@ describe('After user signup', () => { describe('After user singed in', () => { beforeEach(() => { - cy.window(win => { - win.indexedDB.deleteDatabase('keyval-store'); - }); - cy.request('POST', '/api/reset-db').as('reset'); - cy.get('@reset').its('status').should('equal', 204); - cy.reload(true); + cy.resetState(); // インスタンス初期セットアップ - cy.request('POST', '/api/admin/accounts/create', { - username: 'admin', - password: 'pass', - }).its('body').as('admin'); + cy.registerUser('admin', 'pass', true); // ユーザー作成 - cy.request('POST', '/api/signup', { - username: 'alice', - password: 'alice1234', - }).its('body').as('alice'); + cy.registerUser('alice', 'alice1234'); - cy.visit('/'); - - cy.intercept('POST', '/api/signin').as('signin'); - - cy.get('[data-cy-signin]').click(); - cy.get('[data-cy-signin-username] input').type('alice'); - cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); - - cy.wait('@signin').as('signedIn'); + cy.login('alice', 'alice1234'); }); afterEach(() => { diff --git a/cypress/e2e/widgets.cy.js b/cypress/e2e/widgets.cy.js new file mode 100644 index 000000000..56ad95ee9 --- /dev/null +++ b/cypress/e2e/widgets.cy.js @@ -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'); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 119ab03f7..95bfcf685 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -23,3 +23,33 @@ // // -- This will overwrite an existing command -- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) + +Cypress.Commands.add('resetState', () => { + cy.window(win => { + win.indexedDB.deleteDatabase('keyval-store'); + }); + cy.request('POST', '/api/reset-db').as('reset'); + cy.get('@reset').its('status').should('equal', 204); + cy.reload(true); +}); + +Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => { + const route = isAdmin ? '/api/admin/accounts/create' : '/api/signup'; + + cy.request('POST', route, { + username: username, + password: password, + }).its('body').as(username); +}); + +Cypress.Commands.add('login', (username, password) => { + cy.visit('/'); + + cy.intercept('POST', '/api/signin').as('signin'); + + cy.get('[data-cy-signin]').click(); + cy.get('[data-cy-signin-username] input').type(username); + cy.get('[data-cy-signin-password] input').type(`${password}{enter}`); + + cy.wait('@signin').as('signedIn'); +}); diff --git a/cypress/support/index.js b/cypress/support/e2e.js similarity index 100% rename from cypress/support/index.js rename to cypress/support/e2e.js diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index efad7c954..3bd8f1e50 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1007,7 +1007,6 @@ _sfx: antenna: "الهوائيات" channel: "إشعارات القنات" _ago: - unknown: "مجهول" future: "المستقبَل" justNow: "اللحظة" secondsAgo: "منذ {n} ثوانٍ" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index bcecb0625..d7753b6dc 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -831,11 +831,18 @@ themeColor: "থিমের রং" size: "আকার" numberOfColumn: "কলামের সংখ্যা" searchByGoogle: "গুগল" +instanceDefaultLightTheme: "ইন্সট্যান্সের ডিফল্ট লাইট থিম" +instanceDefaultDarkTheme: "ইন্সট্যান্সের ডিফল্ট ডার্ক থিম" +instanceDefaultThemeDescription: "অবজেক্ট ফরম্যাটে থিম কোড লিখুন" +mutePeriod: "মিউটের সময়কাল" indefinitely: "অনির্দিষ্ট" tenMinutes: "১০ মিনিট" oneHour: "১ ঘণ্টা" oneDay: "একদিন" oneWeek: "এক সপ্তাহ" +reflectMayTakeTime: "এটির কাজ দেখা যেতে কিছুটা সময় লাগতে পারে।" +failedToFetchAccountInformation: "অ্যাকাউন্টের তথ্য উদ্ধার করা যায়নি" +rateLimitExceeded: "রেট লিমিট ছাড়িয়ে গেছে " _emailUnavailable: used: "এই ইমেইল ঠিকানাটি ইতোমধ্যে ব্যবহৃত হয়েছে" format: "এই ইমেল ঠিকানাটি সঠিকভাবে লিখা হয়নি" @@ -1081,7 +1088,6 @@ _sfx: antenna: "অ্যান্টেনাগুলি" channel: "চ্যানেলের বিজ্ঞপ্তি" _ago: - unknown: "অজানা" future: "ভবিষ্যৎ" justNow: "এইমাত্র" secondsAgo: "{n} সেকেন্ড আগে" @@ -1125,6 +1131,7 @@ _2fa: registerKey: "সিকিউরিটি কী নিবন্ধন করুন" step1: "প্রথমে, আপনার ডিভাইসে {a} বা {b} এর মতো একটি অথেনটিকেশন অ্যাপ ইনস্টল করুন৷" step2: "এরপরে, অ্যাপের সাহায্যে প্রদর্শিত QR কোডটি স্ক্যান করুন।" + step2Url: "ডেস্কটপ অ্যাপে, নিম্নলিখিত URL লিখুন:" step3: "অ্যাপে প্রদর্শিত টোকেনটি লিখুন এবং আপনার কাজ শেষ।" step4: "আপনাকে এখন থেকে লগ ইন করার সময়, এইভাবে টোকেন লিখতে হবে।" securityKeyInfo: "আপনি একটি হার্ডওয়্যার সিকিউরিটি কী ব্যবহার করে লগ ইন করতে পারেন যা FIDO2 বা ডিভাইসের ফিঙ্গারপ্রিন্ট সেন্সর বা পিন সমর্থন করে৷" @@ -1608,6 +1615,8 @@ _notification: youReceivedFollowRequest: "অনুসরণ করার জন্য অনুরোধ পাওয়া গেছে" yourFollowRequestAccepted: "আপনার অনুসরণ করার অনুরোধ গৃহীত হয়েছে" youWereInvitedToGroup: "আপনি একটি গ্রুপে আমন্ত্রিত হয়েছেন" + pollEnded: "পোলের ফলাফল দেখা যাবে" + emptyPushNotificationMessage: "আপডেট করা পুশ বিজ্ঞপ্তি" _types: all: "সকল" follow: "অনুসরণ করা হচ্ছে" @@ -1617,11 +1626,13 @@ _notification: quote: "উদ্ধৃতি" reaction: "প্রতিক্রিয়া" pollVote: "পোলে ভোট আছে" + pollEnded: "পোল শেষ" receiveFollowRequest: "প্রাপ্ত অনুসরণের অনুরোধসমূহ" followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ" groupInvited: "গ্রুপের আমন্ত্রনসমূহ" app: "লিঙ্ক করা অ্যাপ থেকে বিজ্ঞপ্তি" _actions: + followBack: "ফলো ব্যাক করেছে" reply: "জবাব" renote: "রিনোট" _deck: diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 577fbca2a..74eab3603 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -134,6 +134,8 @@ _theme: _sfx: note: "Notes" notification: "Notificacions" +_2fa: + step2Url: "També pots inserir aquest enllaç i utilitzes una aplicació d'escriptori:" _widgets: notifications: "Notificacions" timeline: "Línia de temps" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index a3ff6c0a6..5dfce2800 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -842,6 +842,9 @@ oneDay: "Einen Tag" oneWeek: "Eine Woche" reflectMayTakeTime: "Es kann etwas dauern, bis sich dies widerspiegelt." failedToFetchAccountInformation: "Benutzerkontoinformationen konnten nicht abgefragt werden" +rateLimitExceeded: "Versuchsanzahl überschritten" +cropImage: "Bild zuschneiden" +cropImageAsk: "Möchtest du das Bild zuschneiden?" _emailUnavailable: used: "Diese Email-Adresse wird bereits verwendet" format: "Das Format dieser Email-Adresse ist ungültig" @@ -1087,7 +1090,6 @@ _sfx: antenna: "Antennen" channel: "Kanalbenachrichtigung" _ago: - unknown: "Unbekannt" future: "Zukunft" justNow: "Gerade eben" secondsAgo: "vor {n} Sekunde(n)" @@ -1131,6 +1133,7 @@ _2fa: registerKey: "Neuen Sicherheitsschlüssel registrieren" step1: "Installiere zuerst eine Authentifizierungsapp (z.B. {a} oder {b}) auf deinem Gerät." step2: "Dann, scanne den angezeigten QR-Code mit deinem Gerät." + step2Url: "Nutzt du ein Desktopprogramm kannst du alternativ diese URL eingeben:" step3: "Gib zum Abschluss den Token ein, der von deiner App angezeigt wird." step4: "Alle folgenden Anmeldungsversuche werden ab sofort die Eingabe eines solchen Tokens benötigen." securityKeyInfo: "Du kannst neben Fingerabdruck- oder PIN-Authentifizierung auf deinem Gerät auch Anmeldung mit Hilfe eines FIDO2-kompatiblen Hardware-Sicherheitsschlüssels einrichten." diff --git a/locales/en-US.yml b/locales/en-US.yml index a7d69d6d4..8bfea26b0 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -842,6 +842,9 @@ oneDay: "One day" oneWeek: "One week" reflectMayTakeTime: "It may take some time for this to be reflected." failedToFetchAccountInformation: "Could not fetch account information" +rateLimitExceeded: "Rate limit exceeded" +cropImage: "Crop image" +cropImageAsk: "Do you want to crop this image?" _emailUnavailable: used: "This email address is already being used" format: "The format of this email address is invalid" @@ -1087,7 +1090,6 @@ _sfx: antenna: "Antennas" channel: "Channel notifications" _ago: - unknown: "Unknown" future: "Future" justNow: "Just now" secondsAgo: "{n} second(s) ago" @@ -1131,6 +1133,7 @@ _2fa: registerKey: "Register a security key" step1: "First, install an authentication app (such as {a} or {b}) on your device." step2: "Then, scan the QR code displayed on this screen." + step2Url: "You can also enter this URL if you're using a desktop program:" step3: "Enter the token provided by your app to finish setup." step4: "From now on, any future login attempts will ask for such a login token." securityKeyInfo: "Besides fingerprint or PIN authentication, you can also setup authentication via hardware security keys that support FIDO2 to further secure your account." diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 8fff5ca4d..6c10942b4 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -989,7 +989,6 @@ _sfx: antenna: "Antena receptora" channel: "Notificaciones del canal" _ago: - unknown: "Desconocido" future: "Futuro" justNow: "Recién ahora" secondsAgo: "Hace {n} segundos" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 3b5dc1b06..7e225c299 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -804,7 +804,7 @@ manageAccounts: "Gérer les comptes" makeReactionsPublic: "Rendre les réactions publiques" makeReactionsPublicDescription: "Ceci rendra la liste de toutes vos réactions données publique." classic: "Classique" -muteThread: "Mettre ce thread en sourdine" +muteThread: "Masquer cette discussion" unmuteThread: "Ne plus masquer le fil" ffVisibility: "Visibilité des abonnés/abonnements" ffVisibilityDescription: "Permet de configurer qui peut voir les personnes que tu suis et les personnes qui te suivent." @@ -1075,7 +1075,6 @@ _sfx: antenna: "Réception de l’antenne" channel: "Notifications de canal" _ago: - unknown: "Inconnu" future: "Futur" justNow: "à l’instant" secondsAgo: "Il y a {n}s" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index a97ac819a..39e2c1f66 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -842,6 +842,9 @@ oneDay: "1 Hari" oneWeek: "1 Bulan" reflectMayTakeTime: "Mungkin perlu beberapa saat untuk dicerminkan." failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun" +rateLimitExceeded: "Batas sudah terlampaui" +cropImage: "potong gambar" +cropImageAsk: "Ingin memotong gambar?" _emailUnavailable: used: "Alamat surel ini telah digunakan" format: "Format tidak valid." @@ -1087,7 +1090,6 @@ _sfx: antenna: "Penerimaan Antenna" channel: "Pemberitahuan saluran" _ago: - unknown: "Tidak diketahui" future: "Masa depan" justNow: "Baru saja" secondsAgo: "{n} detik lalu" @@ -1131,6 +1133,7 @@ _2fa: registerKey: "Daftarkan kunci keamanan baru" step1: "Pertama, pasang aplikasi otentikasi (seperti {a} atau {b}) di perangkat kamu." step2: "Lalu, pindai kode QR yang ada di layar." + step2Url: "Di aplikasi desktop, masukkan URL berikut:" step3: "Masukkan token yang telah disediakan oleh aplikasimu untuk menyelesaikan pemasangan." step4: "Mulai sekarang, upaya login apapun akan meminta token login dari aplikasi otentikasi kamu." securityKeyInfo: "Kamu dapat memasang otentikasi WebAuthN untuk mengamankan proses login lebih lanjut dengan tidak hanya perangkat keras kunci keamanan yang mendukung FIDO2, namun juga sidik jari atau otentikasi PIN pada perangkatmu." diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 4d1035669..8584ed6a8 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -10,7 +10,7 @@ password: "Password" forgotPassword: "Hai dimenticato la tua password?" fetchingAsApObject: "Recuperando dal Fediverso..." ok: "OK" -gotIt: "Capito!" +gotIt: "Ho capito" cancel: "Annulla" enterUsername: "Inserisci un nome utente" renotedBy: "Rinotato da {user}" @@ -767,6 +767,7 @@ customCss: "CSS personalizzato" global: "Federata" squareAvatars: "Mostra l'immagine del profilo come quadrato" sent: "Inviare" +received: "Ricevuto" searchResult: "Risultati della Ricerca" hashtags: "Hashtag" troubleshooting: "Risoluzione problemi" @@ -804,6 +805,10 @@ welcomeBackWithName: "Bentornato/a, {name}" clickToFinishEmailVerification: "Fai click su [{ok}] per completare la verifica dell'indirizzo email." searchByGoogle: "Cerca" indefinitely: "Non scade" +tenMinutes: "10 minuti" +oneHour: "1 ora" +oneDay: "1 giorno" +oneWeek: "1 settimana" _emailUnavailable: used: "Email già in uso" format: "Formato email non valido" @@ -999,7 +1004,6 @@ _sfx: antenna: "Ricezione dell'antenna" channel: "Notifiche di canale" _ago: - unknown: "Sconosciuto" future: "Futuro" justNow: "Ora" secondsAgo: "{n}s fa" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 22ff78039..19e751587 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -645,6 +645,8 @@ clip: "クリップ" createNew: "新規作成" optional: "任意" createNewClip: "新しいクリップを作成" +unclip: "クリップ解除" +confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれています。ノートをこのクリップから除外しますか?" public: "パブリック" i18nInfo: "Misskeyは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。" manageAccessTokens: "アクセストークンの管理" @@ -844,6 +846,10 @@ oneDay: "1日" oneWeek: "1週間" reflectMayTakeTime: "反映されるまで時間がかかる場合があります。" failedToFetchAccountInformation: "アカウント情報の取得に失敗しました" +rateLimitExceeded: "レート制限を超えました" +cropImage: "画像のクロップ" +cropImageAsk: "画像をクロップしますか?" +file: "ファイル" _emailUnavailable: used: "既に使用されています" @@ -1135,7 +1141,6 @@ _sfx: channel: "チャンネル通知" _ago: - unknown: "謎" future: "未来" justNow: "たった今" secondsAgo: "{n}秒前" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index fd6945160..5458152dd 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -799,7 +799,6 @@ _sfx: notification: "通知" chat: "チャット" _ago: - unknown: "わからん" future: "未来" justNow: "たった今" secondsAgo: "{n}秒前" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 74d06185d..e0a839a2c 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -842,6 +842,9 @@ oneDay: "1일" oneWeek: "일주일" reflectMayTakeTime: "반영되기까지 시간이 걸릴 수 있습니다." failedToFetchAccountInformation: "계정 정보를 가져오지 못했습니다" +rateLimitExceeded: "요청 제한 횟수를 초과하였습니다" +cropImage: "이미지 자르기" +cropImageAsk: "이미지를 자르시겠습니까?" _emailUnavailable: used: "이 메일 주소는 사용중입니다" format: "형식이 올바르지 않습니다" @@ -1087,7 +1090,6 @@ _sfx: antenna: "안테나 수신" channel: "채널 알림" _ago: - unknown: "알 수 없음" future: "미래" justNow: "방금 전" secondsAgo: "{n}초 전" @@ -1131,6 +1133,7 @@ _2fa: registerKey: "키를 등록" step1: "먼저, {a}나 {b}등의 인증 앱을 사용 중인 디바이스에 설치합니다." step2: "그 후, 표시되어 있는 QR코드를 앱으로 스캔합니다." + step2Url: "데스크톱 앱에서는 다음 URL을 입력하세요:" step3: "앱에 표시된 토큰을 입력하시면 완료됩니다." step4: "다음 로그인부터는 토큰을 입력해야 합니다." securityKeyInfo: "FIDO2를 지원하는 하드웨어 보안 키 혹은 디바이스의 지문인식이나 화면잠금 PIN을 이용해서 로그인하도록 설정할 수 있습니다." diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index d0a83eb6a..0ded57394 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -303,6 +303,8 @@ muteThread: "Discussies dempen " unmuteThread: "Dempen van discussie ongedaan maken" hide: "Verbergen" searchByGoogle: "Zoeken" +cropImage: "Afbeelding bijsnijden" +cropImageAsk: "Bijsnijdengevraagd" _email: _follow: title: "volgde jou" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 7fabab3b4..fa1dad217 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -946,7 +946,6 @@ _sfx: chatBg: "Rozmowy (tło)" channel: "Powiadomienia kanału" _ago: - unknown: "Nieznane" future: "W przyszłości" justNow: "Przed chwilą" secondsAgo: "{n} sek. temu" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index c1afa5b56..0dc15a27b 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -81,18 +81,67 @@ somethingHappened: "Ocorreu um erro" retry: "Tentar novamente" pageLoadError: "Ocorreu um erro ao carregar a página." pageLoadErrorDescription: "Isto é normalmente causado por erros de rede ou pela cache do browser. Experimenta limpar a cache e tenta novamente após algum tempo." +serverIsDead: "O servidor não está respondendo. Por favor espere um pouco e tente novamente." +youShouldUpgradeClient: "Para visualizar essa página, por favor recarregue-a para atualizar seu cliente." +enterListName: "Insira um nome para a lista" +privacy: "Privacidade" +makeFollowManuallyApprove: "Pedidos de seguimento precisam ser aprovados" +defaultNoteVisibility: "Visibilidade padrão" follow: "Seguindo" +followRequest: "Mandar pedido de seguimento" +followRequests: "Pedidos de seguimento" +unfollow: "Deixar de seguir" +followRequestPending: "Pedido de seguimento pendente" enterEmoji: "Inserir emoji" renote: "Repostar" renoted: "Repostado" cantRenote: "Não pode repostar" cantReRenote: "Não pode repostar este repost" +quote: "Citar" pinnedNote: "Post fixado" pinned: "Afixar no perfil" +you: "Você" +clickToShow: "Clique para ver" sensitive: "Conteúdo sensível" +add: "Adicionar" +reaction: "Reações" +reactionSetting: "Quais reações a mostrar no selecionador de reações" +rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas" +attachCancel: "Remover anexo" +markAsSensitive: "Marcar como sensível" +unmarkAsSensitive: "Desmarcar como sensível" +enterFileName: "Digite o nome do ficheiro" mute: "Silenciar" unmute: "Dessilenciar" +block: "Bloquear" +unblock: "Desbloquear" +suspend: "Suspender" +unsuspend: "Cancelar suspensão" +blockConfirm: "Tem certeza que gostaria de bloquear essa conta?" +unblockConfirm: "Tem certeza que gostaria de desbloquear essa conta?" +suspendConfirm: "Tem certeza que gostaria de suspender essa conta?" +unsuspendConfirm: "Tem certeza que gostaria de cancelar a suspensão dessa conta?" +selectList: "Escolhe uma lista" +selectAntenna: "Escolhe uma antena" +selectWidget: "Escolhe um widget" +editWidgets: "Editar widgets" +editWidgetsExit: "Pronto" +customEmojis: "Emoji personalizado" +emoji: "Emoji" +emojis: "Emojis" +emojiName: "Nome do Emoji" +emojiUrl: "URL do Emoji" +addEmoji: "Adicionar um Emoji" settingGuide: "Guia de configuração" +flagAsBot: "Marcar conta como robô" +flagAsCat: "Marcar conta como gato" +flagAsCatDescription: "Ative essa opção para marcar essa conta como gato." +flagShowTimelineReplies: "Mostrar respostas na linha de tempo" +general: "Geral" +wallpaper: "Papel de parede" +searchWith: "Buscar: {q}" +youHaveNoLists: "Não tem nenhuma lista" +followConfirm: "Tem certeza que quer deixar de seguir {name}?" instances: "Instância" registeredAt: "Registrado em" perHour: "por hora" @@ -107,6 +156,8 @@ nsfw: "Conteúdo sensível" monthX: "mês de {month}" pinnedNotes: "Post fixado" userList: "Listas" +none: "Nenhum" +output: "Resultado" smtpUser: "Nome de usuário" smtpPass: "Senha" user: "Usuários" @@ -116,6 +167,8 @@ _email: title: "Você tem um novo seguidor" _mfm: mention: "Menção" + quote: "Citar" + emoji: "Emoji personalizado" search: "Pesquisar" _theme: keys: @@ -136,38 +189,229 @@ _profile: _exportOrImport: followingList: "Seguindo" muteList: "Silenciar" + blockingList: "Bloquear" userLists: "Listas" _pages: + blocks: + _button: + _action: + _pushEvent: + event: "Nome do evento" + message: "Mostrar mensagem quando ativado" + variable: "Variável a mandar" + no-variable: "Nenhum" + callAiScript: "Invocar AiScript" + _callAiScript: + functionName: "Nome da função" + radioButton: "Escolha" + _radioButton: + values: "Lista de escolhas separadas por quebras de texto" script: categories: + logical: "Operação lógica" + operation: "Cálculos" + comparison: "Comparação" list: "Listas" blocks: + _strReplace: + arg2: "Texto que irá ser substituído" + arg3: "Substituir com" + strReverse: "Virar texto" + join: "Sequência de texto" _join: arg1: "Listas" + arg2: "Separador" + add: "Somar" + _add: + arg1: "A" + arg2: "B" + subtract: "Subtrair" + _subtract: + arg1: "A" + arg2: "B" + multiply: "Multiplicar" + _multiply: + arg1: "A" + arg2: "B" + divide: "Dividir" + _divide: + arg1: "A" + arg2: "B" + mod: "O resto de" + _mod: + arg1: "A" + arg2: "B" + round: "Arredondar decimal" + _round: + arg1: "Numérico" + eq: "A e B são iguais" + _eq: + arg1: "A" + arg2: "B" + notEq: "A e B são diferentes" + _notEq: + arg1: "A" + arg2: "B" + and: "A e B" + _and: + arg1: "A" + arg2: "B" + or: "A OU B" + _or: + arg1: "A" + arg2: "B" + lt: "< A é menor do que B" + _lt: + arg1: "A" + arg2: "B" + gt: "> A é maior do que B" + _gt: + arg1: "A" + arg2: "B" + ltEq: "<= A é maior ou igual a B" + _ltEq: + arg1: "A" + arg2: "B" + gtEq: ">= A é maior ou igual a B" + _gtEq: + arg1: "A" + arg2: "B" + if: "Galho" + _if: + arg1: "Se" + arg2: "Então" + arg3: "Se não" + not: "NÃO" + _not: + arg1: "NÃO" + random: "Aleatório" + _random: + arg1: "Probabilidade" + rannum: "Numeral aleatório" + _rannum: + arg1: "Valor mínimo" + arg2: "Valor máximo" + randomPick: "Escolher aleatoriamente de uma lista" _randomPick: arg1: "Listas" + dailyRandom: "Aleatório (Muda uma vez por dia para cada usuário)" + _dailyRandom: + arg1: "Probabilidade" + dailyRannum: "Numeral aleatório (Muda uma vez por dia para cada usuário)" + _dailyRannum: + arg1: "Valor mínimo" + arg2: "Valor máximo" + dailyRandomPick: "Escolher aleatoriamente de uma lista (Muda uma vez por dia para cada usuário)" _dailyRandomPick: arg1: "Listas" + seedRandom: "Aleatório (com semente)" + _seedRandom: + arg1: "Semente" + arg2: "Probabilidade" + seedRannum: "Número aleatório (com semente)" + _seedRannum: + arg1: "Semente" + arg2: "Valor mínimo" + arg3: "Valor máximo" + seedRandomPick: "Escolher aleatoriamente de uma lista (com uma semente)" _seedRandomPick: + arg1: "Semente" arg2: "Listas" + DRPWPM: "Escolher aleatoriamente de uma lista ponderada (Muda uma vez por dia para cada usuário)" + _DRPWPM: + arg1: "Lista de texto" + pick: "Escolhe a partir da lista" _pick: arg1: "Listas" + arg2: "Posição" + listLen: "Pegar comprimento da lista" _listLen: arg1: "Listas" + number: "Numérico" + stringToNumber: "Texto para numérico" + _stringToNumber: + arg1: "Texto" + numberToString: "Numérico para texto" + _numberToString: + arg1: "Numérico" + splitStrByLine: "Dividir texto por quebras" + _splitStrByLine: + arg1: "Texto" + ref: "Variável" + aiScriptVar: "Variável AiScript" + fn: "Função" + _fn: + slots: "Espaços" + slots-info: "Separar cada espaço com uma quebra de texto" + arg1: "Resultado" + for: "Repetição 'for'" + _for: + arg1: "Número de repetições" + arg2: "Ação" + typeError: "Espaço {slot} aceita valores de tipo \"{expect}\", mas o valor dado é do tipo \"{actual}\"!" + thereIsEmptySlot: "O espaço {slot} está vazio!" types: + string: "Texto" + number: "Numérico" array: "Listas" + stringArray: "Lista de texto" + emptySlot: "Espaço vazio" + enviromentVariables: "Variáveis de ambiente" + pageVariables: "Variáveis de página" +_relayStatus: + requesting: "Pendente" + accepted: "Aprovado" + rejected: "Recusado" _notification: + fileUploaded: "Carregamento de arquivo efetuado com sucesso" + youGotMention: "{name} te mencionou" + youGotReply: "{name} te respondeu" + youGotQuote: "{name} te citou" + youGotPoll: "{name} votou em sua enquete" + youGotMessagingMessageFromUser: "{name} te mandou uma mensagem de bate-papo" + youGotMessagingMessageFromGroup: "Uma mensagem foi mandada para o grupo {name}" youWereFollowed: "Você tem um novo seguidor" + youReceivedFollowRequest: "Você recebeu um pedido de seguimento" + yourFollowRequestAccepted: "Seu pedido de seguimento foi aceito" + youWereInvitedToGroup: "{userName} te convidou para um grupo" + pollEnded: "Os resultados da enquete agora estão disponíveis" + emptyPushNotificationMessage: "As notificações de alerta foram atualizadas" _types: + all: "Todos" follow: "Seguindo" mention: "Menção" + reply: "Respostas" renote: "Repostar" + quote: "Citar" + reaction: "Reações" + pollVote: "Votações em enquetes" + pollEnded: "Enquetes terminando" + receiveFollowRequest: "Recebeu pedidos de seguimento" + followRequestAccepted: "Aceitou pedidos de seguimento" + groupInvited: "Convites de grupo" + app: "Notificações de aplicativos conectados" _actions: + followBack: "te seguiu de volta" reply: "Responder" renote: "Repostar" _deck: + alwaysShowMainColumn: "Sempre mostrar a coluna principal" + columnAlign: "Alinhar colunas" + columnMargin: "Margem entre colunas" + columnHeaderHeight: "Altura do cabeçalho de coluna" + addColumn: "Adicionar coluna" + swapLeft: "Trocar de posição com a coluna à esquerda" + swapRight: "Trocar de posição com a coluna à direita" + swapUp: "Trocar de posição com a coluna acima" + swapDown: "Trocar de posição com a coluna abaixo" + popRight: "Acoplar coluna à direita" + profile: "Perfil" _columns: + main: "Principal" + widgets: "Widgets" notifications: "Notificações" tl: "Timeline" + antenna: "Antenas" list: "Listas" mentions: "Menções" + direct: "Notas diretas" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 7405c07e6..c44589a7e 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -141,6 +141,8 @@ flagAsBot: "Аккаунт бота" flagAsBotDescription: "Включите, если этот аккаунт управляется программой. Это позволит системе Misskey учитывать это, а также поможет разработчикам других ботов предотвратить бесконечные циклы взаимодействия." flagAsCat: "Аккаунт кота" flagAsCatDescription: "Включите, и этот аккаунт будет помечен как кошачий." +flagShowTimelineReplies: "Показывать ответы на заметки в ленте" +flagShowTimelineRepliesDescription: "Если этот параметр включен, то в ленте, в дополнение к заметкам пользователя, отображаются ответы на другие заметки пользователя." autoAcceptFollowed: "Принимать подписчиков автоматически" addAccount: "Добавить учётную запись" loginFailed: "Неудачная попытка входа" @@ -236,6 +238,7 @@ saved: "Сохранено" messaging: "Сообщения" upload: "Загрузить" keepOriginalUploading: "Сохранить исходное изображение" +keepOriginalUploadingDescription: "Сохраняет исходную версию при загрузке изображений. Если выключить, то при загрузке браузер генерирует изображение для публикации." fromDrive: "С «диска»" fromUrl: "По ссылке" uploadFromUrl: "Загрузить по ссылке" @@ -589,6 +592,7 @@ smtpSecure: "Использовать SSL/TLS для SMTP-соединений" smtpSecureInfo: "Выключите при использовании STARTTLS." testEmail: "Проверка доставки электронной почты" wordMute: "Скрытие слов" +regexpError: "Ошибка в регулярном выражении" instanceMute: "Глушение инстансов" userSaysSomething: "{name} что-то сообщает" makeActive: "Активировать" @@ -619,6 +623,8 @@ fillAbuseReportDescription: "Опишите, пожалуйста, причин abuseReported: "Жалоба отправлена. Большое спасибо за информацию." reporteeOrigin: "О ком сообщено" reporterOrigin: "Кто сообщил" +forwardReport: "Перенаправление отчета на инстант." +forwardReportIsAnonymous: "Удаленный инстант не сможет увидеть вашу информацию и будет отображаться как анонимная системная учетная запись." send: "Отправить" abuseMarkAsResolved: "Отметить жалобу как решённую" openInNewTab: "Открыть в новой вкладке" @@ -815,7 +821,16 @@ leaveGroupConfirm: "Покинуть группу «{name}»?" useDrawerReactionPickerForMobile: "Выдвижная палитра на мобильном устройстве" welcomeBackWithName: "С возвращением, {name}!" clickToFinishEmailVerification: "Пожалуйста, нажмите [{ok}], чтобы завершить подтверждение адреса электронной почты." +overridedDeviceKind: "Тип устройства" +smartphone: "Смартфон" +tablet: "Планшет" +auto: "Автоматически" +themeColor: "Цвет темы" +size: "Размер" +numberOfColumn: "Количество столбцов" searchByGoogle: "Поиск" +instanceDefaultLightTheme: "Светлая тема по умолчанию" +instanceDefaultDarkTheme: "Темная тема по умолчанию" indefinitely: "вечно" _emailUnavailable: used: "Уже используется" @@ -1059,7 +1074,6 @@ _sfx: antenna: "Антенна" channel: "Канал" _ago: - unknown: "Когда-то" future: "Из будущего" justNow: "Только что" secondsAgo: "{n} с назад" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 6818a64d3..dc1151522 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -841,6 +841,7 @@ oneDay: "1 deň" oneWeek: "1 týždeň" reflectMayTakeTime: "Zmeny môžu chvíľu trvať kým sa prejavia." failedToFetchAccountInformation: "Nepodarilo sa načítať informácie o účte." +rateLimitExceeded: "Prekročený limit rýchlosti" _emailUnavailable: used: "Táto emailová adresa sa už používa" format: "Formát emailovej adresy je nesprávny" @@ -1086,7 +1087,6 @@ _sfx: antenna: "Antény" channel: "Upozornenia kanála" _ago: - unknown: "Neznáme" future: "Budúcnosť" justNow: "Teraz" secondsAgo: "pred {n} sekundami" @@ -1130,6 +1130,7 @@ _2fa: registerKey: "Registrovať bezpečnostný kľúč" step1: "Najprv si nainštalujte autentifikačnú aplikáciu (napríklad {a} alebo {b}) na svoje zariadenie." step2: "Potom, naskenujte QR kód zobrazený na obrazovke." + step2Url: "Do aplikácie zadajte nasledujúcu URL adresu:" step3: "Nastavenie dokončíte zadaním tokenu z vašej aplikácie." step4: "Od teraz, všetky ďalšie prihlásenia budú vyžadovať prihlasovací token." securityKeyInfo: "Okrem odtlačku prsta alebo PIN autentifikácie si môžete nastaviť autentifikáciu cez hardvérový bezpečnostný kľúč podporujúci FIDO2 a tak ešte viac zabezpečiť svoj účet." diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml new file mode 100644 index 000000000..42bfa45f2 --- /dev/null +++ b/locales/sv-SE.yml @@ -0,0 +1,319 @@ +--- +_lang_: "Svenska" +headlineMisskey: "Ett nätverk kopplat av noter" +introMisskey: "Välkommen! Misskey är en öppen och decentraliserad mikrobloggningstjänst.\nSkapa en \"not\" och dela dina tankar med alla runtomkring dig. 📡\nMed \"reaktioner\" kan du snabbt uttrycka dina känslor kring andras noter.👍\nLåt oss utforska en nya värld!🚀" +monthAndDay: "{day}/{month}" +search: "Sök" +notifications: "Notifikationer" +username: "Användarnamn" +password: "Lösenord" +forgotPassword: "Glömt lösenord" +fetchingAsApObject: "Hämtar från Fediversum..." +ok: "OK" +gotIt: "Uppfattat!" +cancel: "Avbryt" +enterUsername: "Ange användarnamn" +renotedBy: "Omnoterad av {user}" +noNotes: "Inga noteringar" +noNotifications: "Inga aviseringar" +instance: "Instanser" +settings: "Inställningar" +basicSettings: "Basinställningar" +otherSettings: "Andra inställningar" +openInWindow: "Öppna i ett fönster" +profile: "Profil" +timeline: "Tidslinje" +noAccountDescription: "Användaren har inte skrivit en biografi än." +login: "Logga in" +loggingIn: "Loggar in" +logout: "Logga ut" +signup: "Registrera" +uploading: "Uppladdning sker..." +save: "Spara" +users: "Användare" +addUser: "Lägg till användare" +favorite: "Lägg till i favoriter" +favorites: "Favoriter" +unfavorite: "Avfavorisera" +favorited: "Tillagd i favoriter." +alreadyFavorited: "Redan tillagd i favoriter." +cantFavorite: "Gick inte att lägga till i favoriter." +pin: "Fäst till profil" +unpin: "Lossa från profil" +copyContent: "Kopiera innehåll" +copyLink: "Kopiera länk" +delete: "Radera" +deleteAndEdit: "Radera och ändra" +deleteAndEditConfirm: "Är du säker att du vill radera denna not och ändra den? Du kommer förlora alla reaktioner, omnoteringar och svar till den." +addToList: "Lägg till i lista" +sendMessage: "Skicka ett meddelande" +copyUsername: "Kopiera användarnamn" +searchUser: "Sök användare" +reply: "Svara" +loadMore: "Ladda mer" +showMore: "Visa mer" +youGotNewFollower: "följde dig" +receiveFollowRequest: "Följarförfrågan mottagen" +followRequestAccepted: "Följarförfrågan accepterad" +mention: "Nämn" +mentions: "Omnämningar" +directNotes: "Direktnoter" +importAndExport: "Importera / Exportera" +import: "Importera" +export: "Exportera" +files: "Filer" +download: "Nedladdning" +driveFileDeleteConfirm: "Är du säker att du vill radera filen \"{name}\"? Noter med denna fil bifogad kommer också raderas." +unfollowConfirm: "Är du säker att du vill avfölja {name}?" +exportRequested: "Du har begärt en export. Detta kan ta lite tid. Den kommer läggas till i din Drive när den blir klar." +importRequested: "Du har begärt en import. Detta kan ta lite tid." +lists: "Listor" +noLists: "Du har inga listor" +note: "Not" +notes: "Noter" +following: "Följer" +followers: "Följare" +followsYou: "Följer dig" +createList: "Skapa lista" +manageLists: "Hantera lista" +error: "Fel!" +somethingHappened: "Ett fel har uppstått" +retry: "Försök igen" +pageLoadError: "Det gick inte att ladda sidan." +pageLoadErrorDescription: "Detta händer oftast p.g.a. nätverksfel eller din webbläsarcache. Försök tömma din cache och testa sedan igen efter en liten stund." +serverIsDead: "Servern svarar inte. Vänta ett litet tag och försök igen." +youShouldUpgradeClient: "För att kunna se denna sida, vänligen ladda om sidan för att uppdatera din klient." +enterListName: "Skriv ett namn till listan" +privacy: "Integritet" +makeFollowManuallyApprove: "Följarförfrågningar kräver manuellt godkännande" +defaultNoteVisibility: "Standardsynlighet" +follow: "Följ" +followRequest: "Skicka följarförfrågan" +followRequests: "Följarförfrågningar" +unfollow: "Avfölj" +followRequestPending: "Följarförfrågning avvaktar för svar" +enterEmoji: "Skriv en emoji" +renote: "Omnotera" +unrenote: "Ta tillbaka omnotering" +renoted: "Omnoterad." +cantRenote: "Inlägget kunde inte bli omnoterat." +cantReRenote: "En omnotering kan inte bli omnoterad." +quote: "Citat" +pinnedNote: "Fästad not" +pinned: "Fäst till profil" +you: "Du" +clickToShow: "Klicka för att visa" +sensitive: "Känsligt innehåll" +add: "Lägg till" +reaction: "Reaktioner" +reactionSetting: "Reaktioner som ska visas i reaktionsväljaren" +reactionSettingDescription2: "Dra för att omordna, klicka för att radera, tryck \"+\" för att lägga till." +rememberNoteVisibility: "Komihåg notvisningsinställningar" +attachCancel: "Ta bort bilaga" +markAsSensitive: "Markera som känsligt innehåll" +unmarkAsSensitive: "Avmarkera som känsligt innehåll" +enterFileName: "Ange filnamn" +mute: "Tysta" +unmute: "Avtysta" +block: "Blockera" +unblock: "Avblockera" +suspend: "Suspendera" +unsuspend: "Ta bort suspenderingen" +blockConfirm: "Är du säker att du vill blockera kontot?" +unblockConfirm: "Är du säkert att du vill avblockera kontot?" +suspendConfirm: "Är du säker att du vill suspendera detta konto?" +unsuspendConfirm: "Är du säker att du vill avsuspendera detta konto?" +selectList: "Välj lista" +selectAntenna: "Välj en antenn" +selectWidget: "Välj en widget" +editWidgets: "Redigera widgets" +editWidgetsExit: "Avsluta redigering" +customEmojis: "Anpassa emoji" +emoji: "Emoji" +emojis: "Emoji" +emojiName: "Emoji namn" +emojiUrl: "Emoji länk" +addEmoji: "Lägg till emoji" +settingGuide: "Rekommenderade inställningar" +cacheRemoteFiles: "Spara externa filer till cachen" +cacheRemoteFilesDescription: "När denna inställning är avstängd kommer externa filer laddas direkt från den externa instansen. Genom att stänga av detta kommer lagringsutrymme minska i användning men kommer öka datatrafiken eftersom miniatyrer inte kommer genereras." +flagAsBot: "Markera konto som bot" +flagAsBotDescription: "Aktivera det här alternativet om kontot är kontrollerat av ett program. Om aktiverat kommer den fungera som en flagga för andra utvecklare för att hindra ändlösa kedjor med andra bottar. Det kommer också få Misskeys interna system att hantera kontot som en bot." +flagAsCat: "Markera konto som katt" +flagAsCatDescription: "Aktivera denna inställning för att markera kontot som en katt." +flagShowTimelineReplies: "Visa svar i tidslinje" +flagShowTimelineRepliesDescription: "Visar användarsvar till andra användares noter i tidslinjen om påslagen." +autoAcceptFollowed: "Godkänn följarförfrågningar från användare du följer automatiskt" +addAccount: "Lägg till konto" +loginFailed: "Inloggningen misslyckades" +showOnRemote: "Se på extern instans" +general: "Allmänt" +wallpaper: "Bakgrundsbild" +setWallpaper: "Välj bakgrund" +removeWallpaper: "Ta bort bakgrund" +searchWith: "Sök: {q}" +youHaveNoLists: "Du har inga listor" +followConfirm: "Är du säker att du vill följa {name}?" +proxyAccount: "Proxykonto" +proxyAccountDescription: "Ett proxykonto är ett konto som agerar som en extern följare för användare under vissa villkor. Till exempel, när en användare lägger till en extern användare till en lista så kommer den externa användarens aktivitet inte levireras till instansen om ingen lokal användare följer det kontot, så proxykontot används istället." +host: "Värd" +selectUser: "Välj användare" +recipient: "Mottagare" +annotation: "Kommentarer" +federation: "Federation" +instances: "Instanser" +registeredAt: "Registrerad på" +latestRequestSentAt: "Senaste förfrågan skickad" +latestRequestReceivedAt: "Senaste begäran mottagen" +latestStatus: "Senaste status" +storageUsage: "Använt lagringsutrymme" +charts: "Diagram" +perHour: "Per timme" +perDay: "Per dag" +stopActivityDelivery: "Sluta skicka aktiviteter" +blockThisInstance: "Blockera instans" +operations: "Operationer" +software: "Mjukvara" +version: "Version" +metadata: "Metadata" +withNFiles: "{n} fil(er)" +monitor: "Övervakning" +jobQueue: "Jobbkö" +cpuAndMemory: "CPU och minne" +network: "Nätverk" +disk: "Disk" +instanceInfo: "Instansinformation" +statistics: "Statistik" +clearQueue: "Rensa kö" +clearQueueConfirmTitle: "Är du säker att du vill rensa kön?" +clearQueueConfirmText: "Om någon not är olevererad i kön kommer den inte federeras. Vanligtvis behövs inte denna handling." +clearCachedFiles: "Rensa cache" +clearCachedFilesConfirm: "Är du säker att du vill radera alla cachade externa filer?" +blockedInstances: "Blockerade instanser" +blockedInstancesDescription: "Lista adressnamn av instanser som du vill blockera. Listade instanser kommer inte längre kommunicera med denna instans." +muteAndBlock: "Tystningar och blockeringar" +mutedUsers: "Tystade användare" +blockedUsers: "Blockerade användare" +noUsers: "Det finns inga användare" +editProfile: "Redigera profil" +noteDeleteConfirm: "Är du säker på att du vill ta bort denna not?" +pinLimitExceeded: "Du kan inte fästa fler noter" +intro: "Misskey har installerats! Vänligen skapa en adminanvändare." +done: "Klar" +processing: "Bearbetar..." +preview: "Förhandsvisning" +default: "Standard" +noCustomEmojis: "Det finns ingen emoji" +noJobs: "Det finns inga jobb" +federating: "Federerar" +blocked: "Blockerad" +suspended: "Suspenderad" +all: "Allt" +subscribing: "Prenumererar" +publishing: "Publiceras" +notResponding: "Svarar inte" +instanceFollowing: "Följer på instans" +instanceFollowers: "Följare av instans" +instanceUsers: "Användare av denna instans" +changePassword: "Ändra lösenord" +security: "Säkerhet" +retypedNotMatch: "Inmatningen matchar inte" +currentPassword: "Nuvarande lösenord" +newPassword: "Nytt lösenord" +newPasswordRetype: "Bekräfta lösenord" +attachFile: "Bifoga filer" +more: "Mer!" +featured: "Utvalda" +usernameOrUserId: "Användarnamn eller användar-id" +noSuchUser: "Kan inte hitta användaren" +lookup: "Sökning" +announcements: "Nyheter" +imageUrl: "Bild-URL" +remove: "Radera" +removed: "Borttaget" +removeAreYouSure: "Är du säker att du vill radera \"{x}\"?" +deleteAreYouSure: "Är du säker att du vill radera \"{x}\"?" +resetAreYouSure: "Vill du återställa?" +saved: "Sparad" +messaging: "Chatt" +upload: "Ladda upp" +keepOriginalUploading: "Behåll originalbild" +nsfw: "Känsligt innehåll" +pinnedNotes: "Fästad not" +userList: "Listor" +smtpHost: "Värd" +smtpUser: "Användarnamn" +smtpPass: "Lösenord" +clearCache: "Rensa cache" +user: "Användare" +searchByGoogle: "Sök" +_email: + _follow: + title: "följde dig" +_mfm: + mention: "Nämn" + quote: "Citat" + emoji: "Anpassa emoji" + search: "Sök" +_theme: + keys: + mention: "Nämn" + renote: "Omnotera" +_sfx: + note: "Noter" + notification: "Notifikationer" + chat: "Chatt" +_widgets: + notifications: "Notifikationer" + timeline: "Tidslinje" + federation: "Federation" + jobQueue: "Jobbkö" +_cw: + show: "Ladda mer" +_visibility: + followers: "Följare" +_profile: + username: "Användarnamn" +_exportOrImport: + followingList: "Följer" + muteList: "Tysta" + blockingList: "Blockera" + userLists: "Listor" +_charts: + federation: "Federation" +_pages: + script: + categories: + list: "Listor" + blocks: + _join: + arg1: "Listor" + _randomPick: + arg1: "Listor" + _dailyRandomPick: + arg1: "Listor" + _seedRandomPick: + arg2: "Listor" + _pick: + arg1: "Listor" + _listLen: + arg1: "Listor" + types: + array: "Listor" +_notification: + youWereFollowed: "följde dig" + _types: + follow: "Följer" + mention: "Nämn" + renote: "Omnotera" + quote: "Citat" + reaction: "Reaktioner" + _actions: + reply: "Svara" + renote: "Omnotera" +_deck: + _columns: + notifications: "Notifikationer" + tl: "Tidslinje" + list: "Listor" + mentions: "Omnämningar" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 480526c13..7e7ef8685 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -919,7 +919,6 @@ _sfx: antenna: "Прийом антени" channel: "Повідомлення каналу" _ago: - unknown: "Невідомо" future: "Майбутнє" justNow: "Щойно" secondsAgo: "{n}с тому" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index ffe5ba197..9919e0a0a 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -842,6 +842,7 @@ oneDay: "1 ngày" oneWeek: "1 tuần" reflectMayTakeTime: "Có thể mất một thời gian để điều này được áp dụng." failedToFetchAccountInformation: "Không thể lấy thông tin tài khoản" +rateLimitExceeded: "Giới hạn quá mức" _emailUnavailable: used: "Địa chỉ email đã được sử dụng" format: "Địa chỉ email không hợp lệ" @@ -1087,7 +1088,6 @@ _sfx: antenna: "Trạm phát sóng" channel: "Kênh" _ago: - unknown: "Không rõ" future: "Tương lai" justNow: "Vừa xong" secondsAgo: "{n}s trước" @@ -1131,6 +1131,7 @@ _2fa: registerKey: "Đăng ký một mã bảo vệ" step1: "Trước tiên, hãy cài đặt một ứng dụng xác minh (chẳng hạn như {a} hoặc {b}) trên thiết bị của bạn." step2: "Sau đó, quét mã QR hiển thị trên màn hình này." + step2Url: "Bạn cũng có thể nhập URL này nếu sử dụng một chương trình máy tính:" step3: "Nhập mã token do ứng dụng của bạn cung cấp để hoàn tất thiết lập." step4: "Kể từ bây giờ, những lần đăng nhập trong tương lai sẽ yêu cầu mã token đăng nhập đó." securityKeyInfo: "Bên cạnh xác minh bằng vân tay hoặc mã PIN, bạn cũng có thể thiết lập xác minh thông qua khóa bảo mật phần cứng hỗ trợ FIDO2 để bảo mật hơn nữa cho tài khoản của mình." diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index c719dcb76..4953f5528 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1087,7 +1087,6 @@ _sfx: antenna: "天线接收" channel: "频道通知" _ago: - unknown: "未知" future: "未来" justNow: "最近" secondsAgo: "{n}秒前" @@ -1131,6 +1130,7 @@ _2fa: registerKey: "注册密钥" step1: "首先,在您的设备上安装验证应用,例如{a}或{b}。" step2: "然后,扫描屏幕上显示的二维码。" + step2Url: "在桌面应用程序中输入以下URL:" step3: "输入您的应用提供的动态口令以完成设置。" step4: "从现在开始,任何登录操作都将要求您提供动态口令。" securityKeyInfo: "您可以设置使用支持FIDO2的硬件安全密钥、设备上的指纹或PIN来保护您的登录过程。" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index e9b7ab654..f088fdc0e 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1087,7 +1087,6 @@ _sfx: antenna: "天線接收" channel: "頻道通知" _ago: - unknown: "未知" future: "未來" justNow: "剛剛" secondsAgo: "{n}秒前" @@ -1131,6 +1130,7 @@ _2fa: registerKey: "註冊鍵" step1: "首先,在您的設備上安裝二步驗證程式,例如{a}或{b}。" step2: "然後,掃描螢幕上的QR code。" + step2Url: "在桌面版應用中,請輸入以下的URL:" step3: "輸入您的App提供的權杖以完成設定。" step4: "從現在開始,任何登入操作都將要求您提供權杖。" securityKeyInfo: "您可以設定使用支援FIDO2的硬體安全鎖、終端設備的指纹認證或者PIN碼來登入。" diff --git a/package.json b/package.json index 850a52ceb..fd565f7ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "12.110.1", + "version": "12.111.1-test.1", "codename": "indigo", "repository": { "type": "git", @@ -19,7 +19,7 @@ "watch": "npm run dev", "dev": "node ./scripts/dev.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", "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", @@ -41,10 +41,10 @@ "devDependencies": { "@types/gulp": "4.0.9", "@types/gulp-rename": "2.0.1", - "@typescript-eslint/parser": "5.18.0", + "@typescript-eslint/parser": "5.27.1", "cross-env": "7.0.3", - "cypress": "9.5.3", + "cypress": "10.0.3", "start-server-and-test": "1.14.0", - "typescript": "4.6.3" + "typescript": "4.7.3" } } diff --git a/packages/backend/.mocharc.json b/packages/backend/.mocharc.json index 589522216..87c571cfd 100644 --- a/packages/backend/.mocharc.json +++ b/packages/backend/.mocharc.json @@ -5,6 +5,6 @@ "loader=./test/loader.js" ], "slow": 1000, - "timeout": 3000, + "timeout": 10000, "exit": true } diff --git a/packages/backend/assets/notification-badges/LICENSE b/packages/backend/assets/notification-badges/LICENSE new file mode 100644 index 000000000..841c4c682 --- /dev/null +++ b/packages/backend/assets/notification-badges/LICENSE @@ -0,0 +1,5 @@ +Font Awesome Icons +------------------------- + +Ⓒ Font Awesome +CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/) diff --git a/packages/backend/assets/notification-badges/at.png b/packages/backend/assets/notification-badges/at.png new file mode 100644 index 000000000..d1492856d Binary files /dev/null and b/packages/backend/assets/notification-badges/at.png differ diff --git a/packages/backend/assets/notification-badges/check.png b/packages/backend/assets/notification-badges/check.png new file mode 100644 index 000000000..baeb76bab Binary files /dev/null and b/packages/backend/assets/notification-badges/check.png differ diff --git a/packages/backend/assets/notification-badges/clipboard-check-solid.png b/packages/backend/assets/notification-badges/clipboard-check-solid.png new file mode 100644 index 000000000..d8cdfa9da Binary files /dev/null and b/packages/backend/assets/notification-badges/clipboard-check-solid.png differ diff --git a/packages/backend/assets/notification-badges/clock.png b/packages/backend/assets/notification-badges/clock.png new file mode 100644 index 000000000..9323f8f30 Binary files /dev/null and b/packages/backend/assets/notification-badges/clock.png differ diff --git a/packages/backend/assets/notification-badges/comments.png b/packages/backend/assets/notification-badges/comments.png new file mode 100644 index 000000000..bc8a1c35b Binary files /dev/null and b/packages/backend/assets/notification-badges/comments.png differ diff --git a/packages/backend/assets/notification-badges/id-card-alt.png b/packages/backend/assets/notification-badges/id-card-alt.png new file mode 100644 index 000000000..67e1410e3 Binary files /dev/null and b/packages/backend/assets/notification-badges/id-card-alt.png differ diff --git a/packages/backend/assets/notification-badges/null.png b/packages/backend/assets/notification-badges/null.png new file mode 100644 index 000000000..be1384df1 Binary files /dev/null and b/packages/backend/assets/notification-badges/null.png differ diff --git a/packages/backend/assets/notification-badges/plus.png b/packages/backend/assets/notification-badges/plus.png new file mode 100644 index 000000000..05362c122 Binary files /dev/null and b/packages/backend/assets/notification-badges/plus.png differ diff --git a/packages/backend/assets/notification-badges/poll-h.png b/packages/backend/assets/notification-badges/poll-h.png new file mode 100644 index 000000000..3b7ded665 Binary files /dev/null and b/packages/backend/assets/notification-badges/poll-h.png differ diff --git a/packages/backend/assets/notification-badges/quote-right.png b/packages/backend/assets/notification-badges/quote-right.png new file mode 100644 index 000000000..0fa483765 Binary files /dev/null and b/packages/backend/assets/notification-badges/quote-right.png differ diff --git a/packages/backend/assets/notification-badges/reply.png b/packages/backend/assets/notification-badges/reply.png new file mode 100644 index 000000000..77021f71a Binary files /dev/null and b/packages/backend/assets/notification-badges/reply.png differ diff --git a/packages/backend/assets/notification-badges/retweet.png b/packages/backend/assets/notification-badges/retweet.png new file mode 100644 index 000000000..dc6106048 Binary files /dev/null and b/packages/backend/assets/notification-badges/retweet.png differ diff --git a/packages/backend/assets/notification-badges/user-plus.png b/packages/backend/assets/notification-badges/user-plus.png new file mode 100644 index 000000000..9d376d04d Binary files /dev/null and b/packages/backend/assets/notification-badges/user-plus.png differ diff --git a/packages/backend/package.json b/packages/backend/package.json index 4e0d60b74..3ac5b7398 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -14,7 +14,7 @@ "lodash": "^4.17.21" }, "dependencies": { - "@bull-board/koa": "3.10.4", + "@bull-board/koa": "3.11.1", "@discordapp/twemoji": "14.0.2", "@elastic/elasticsearch": "7.11.0", "@koa/cors": "3.1.0", @@ -28,10 +28,9 @@ "archiver": "5.3.1", "autobind-decorator": "2.4.0", "autwh": "0.1.0", - "aws-sdk": "2.1135.0", + "aws-sdk": "2.1152.0", "bcryptjs": "2.4.3", "blurhash": "1.1.5", - "broadcast-channel": "4.12.0", "bull": "4.8.3", "cacheable-lookup": "6.0.4", "cbor": "8.1.0", @@ -44,18 +43,18 @@ "deep-email-validator": "0.1.21", "escape-regexp": "0.0.1", "feed": "4.2.2", - "file-type": "17.1.1", + "file-type": "17.1.2", "fluent-ffmpeg": "2.1.2", - "got": "12.0.4", + "got": "12.1.0", "hpagent": "0.1.2", - "ip-cidr": "3.0.8", + "ip-cidr": "3.0.10", "is-svg": "4.3.2", "js-yaml": "4.1.0", "jsdom": "19.0.0", "json5": "2.2.1", "json5-loader": "4.0.1", - "jsonld": "5.2.0", - "jsrsasign": "10.5.22", + "jsonld": "6.0.0", + "jsrsasign": "10.5.24", "koa": "2.13.4", "koa-bodyparser": "4.3.0", "koa-favicon": "2.1.0", @@ -72,7 +71,7 @@ "ms": "3.0.0-canary.1", "multer": "1.4.4", "nested-property": "4.0.0", - "node-fetch": "3.2.4", + "node-fetch": "3.2.6", "nodemailer": "6.7.5", "os-utils": "0.0.14", "parse5": "6.0.1", @@ -101,14 +100,14 @@ "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "style-loader": "3.3.1", - "summaly": "2.5.0", + "summaly": "2.6.0", "syslog-pro": "1.0.0", - "systeminformation": "5.11.15", + "systeminformation": "5.11.16", "tinycolor2": "1.4.2", "tmp": "0.2.1", "ts-loader": "9.3.0", - "ts-node": "10.8.0", - "tsc-alias": "1.6.7", + "ts-node": "10.8.1", + "tsc-alias": "1.6.9", "tsconfig-paths": "4.0.0", "twemoji-parser": "14.0.0", "typeorm": "0.3.6", @@ -117,7 +116,7 @@ "uuid": "8.3.2", "web-push": "3.5.0", "websocket": "1.0.34", - "ws": "8.6.0", + "ws": "8.8.0", "xev": "3.0.2" }, "devDependencies": { @@ -145,7 +144,7 @@ "@types/koa__multer": "2.0.4", "@types/koa__router": "8.0.11", "@types/mocha": "9.1.1", - "@types/node": "17.0.35", + "@types/node": "17.0.41", "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.4", "@types/oauth": "0.9.1", @@ -167,12 +166,11 @@ "@types/web-push": "3.3.2", "@types/websocket": "1.0.5", "@types/ws": "8.5.3", - "@typescript-eslint/eslint-plugin": "5.26.0", - "@typescript-eslint/parser": "5.26.0", - "typescript": "4.7.2", - "eslint": "8.16.0", + "@typescript-eslint/eslint-plugin": "5.27.1", + "@typescript-eslint/parser": "5.27.1", + "typescript": "4.7.3", + "eslint": "8.17.0", "eslint-plugin-import": "2.26.0", - "cross-env": "7.0.3", "execa": "6.1.0" } diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts index c2e6bea45..9654a4f3b 100644 --- a/packages/backend/src/config/load.ts +++ b/packages/backend/src/config/load.ts @@ -46,7 +46,7 @@ export default function load() { mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; mixin.userAgent = `Misskey/${meta.version} (${config.url})`; - mixin.clientEntry = clientManifest['src/init.ts'].file.replace(/^_client_dist_\//, ''); + mixin.clientEntry = clientManifest['src/init.ts']; if (!config.redis.prefix) config.redis.prefix = mixin.host; diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index e09e93f04..298f6713e 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -73,6 +73,7 @@ import { entities as charts } from '@/services/chart/entities.js'; import { Webhook } from '@/models/entities/webhook.js'; import { envOption } from '../env.js'; import { dbLogger } from './logger.js'; +import { redisClient } from './redis.js'; const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); @@ -207,7 +208,15 @@ export const db = new DataSource({ migrations: ['../../migration/*.js'], }); -export async function initDb() { +export async function initDb(force = false) { + if (force) { + if (db.isInitialized) { + await db.destroy(); + } + await db.initialize(); + return; + } + if (db.isInitialized) { // nop } else { @@ -217,6 +226,7 @@ export async function initDb() { export async function resetDb() { const reset = async () => { + await redisClient.FLUSHDB(); const tables = await db.query(`SELECT relname AS "table" FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) WHERE nspname NOT IN ('pg_catalog', 'information_schema') diff --git a/packages/backend/src/mfm/from-html.ts b/packages/backend/src/mfm/from-html.ts index 623cb0e71..15110b6b7 100644 --- a/packages/backend/src/mfm/from-html.ts +++ b/packages/backend/src/mfm/from-html.ts @@ -6,6 +6,9 @@ const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; export function fromHtml(html: string, hashtagNames?: string[]): string { + // some AP servers like Pixelfed use br tags as well as newlines + html = html.replace(/\r?\n/gi, '\n'); + const dom = parse5.parseFragment(html); let text = ''; diff --git a/packages/backend/src/misc/create-temp.ts b/packages/backend/src/misc/create-temp.ts index f07be634f..fa88769de 100644 --- a/packages/backend/src/misc/create-temp.ts +++ b/packages/backend/src/misc/create-temp.ts @@ -11,9 +11,14 @@ export function createTemp(): Promise<[string, () => void]> { export function createTempDir(): Promise<[string, () => void]> { return new Promise<[string, () => void]>((res, rej) => { - tmp.dir((e, path, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); + tmp.dir( + { + unsafeCleanup: true, + }, + (e, path, cleanup) => { + if (e) return rej(e); + res([path, cleanup]); + } + ); }); } diff --git a/packages/backend/src/misc/get-ip-hash.ts b/packages/backend/src/misc/get-ip-hash.ts new file mode 100644 index 000000000..379325bb1 --- /dev/null +++ b/packages/backend/src/misc/get-ip-hash.ts @@ -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); +} diff --git a/packages/backend/src/misc/is-mime-image.ts b/packages/backend/src/misc/is-mime-image.ts new file mode 100644 index 000000000..8993ede33 --- /dev/null +++ b/packages/backend/src/misc/is-mime-image.ts @@ -0,0 +1,8 @@ +import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; + +const dictionary = { + 'safe-file': FILE_TYPE_BROWSERSAFE, + 'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'], +}; + +export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime); diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts index b626359d9..0d589d4f1 100644 --- a/packages/backend/src/models/repositories/drive-file.ts +++ b/packages/backend/src/models/repositories/drive-file.ts @@ -29,7 +29,9 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ getPublicProperties(file: DriveFile): DriveFile['properties'] { if (file.properties.orientation != null) { - const properties = structuredClone(file.properties); + // TODO + //const properties = structuredClone(file.properties); + const properties = JSON.parse(JSON.stringify(file.properties)); if (file.properties.orientation >= 5) { [properties.width, properties.height] = [properties.height, properties.width]; } diff --git a/packages/backend/src/models/repositories/instance.ts b/packages/backend/src/models/repositories/instance.ts index 4ddf71709..4594d8634 100644 --- a/packages/backend/src/models/repositories/instance.ts +++ b/packages/backend/src/models/repositories/instance.ts @@ -26,6 +26,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({ maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, iconUrl: instance.iconUrl, + faviconUrl: instance.faviconUrl, infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, }; }, diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index 638d78f62..3fefab031 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -136,6 +136,7 @@ async function populateMyReaction(note: Note, meId: User['id'], _hint_?: { export const NoteRepository = db.getRepository(Note).extend({ async isVisibleForMe(note: Note, meId: User['id'] | null): Promise { + // This code must always be synchronized with the checks in generateVisibilityQuery. // visibility が specified かつ自分が指定されていなかったら非表示 if (note.visibility === 'specified') { if (meId == null) { @@ -144,13 +145,7 @@ export const NoteRepository = db.getRepository(Note).extend({ return true; } else { // 指定されているかどうか - const specified = note.visibleUserIds.some((id: any) => meId === id); - - if (specified) { - return true; - } else { - return false; - } + return note.visibleUserIds.some((id: any) => meId === id); } } @@ -169,9 +164,12 @@ export const NoteRepository = db.getRepository(Note).extend({ } else { // フォロワーかどうか const [following, user] = await Promise.all([ - Followings.findOneBy({ - followeeId: note.userId, - followerId: meId, + Followings.count({ + where: { + followeeId: note.userId, + followerId: meId, + }, + take: 1, }), Users.findOneByOrFail({ id: meId }), ]); @@ -183,7 +181,7 @@ export const NoteRepository = db.getRepository(Note).extend({ in which case we can never know the following. Instead we have 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); } } diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 541fbaf00..8a4e48efd 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -61,47 +61,58 @@ export const UserRepository = db.getRepository(User).extend({ //#endregion async getRelation(me: User['id'], target: User['id']) { - const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([ - Followings.findOneBy({ - followerId: me, - followeeId: target, - }), - Followings.findOneBy({ - followerId: target, - followeeId: me, - }), - FollowRequests.findOneBy({ - followerId: me, - followeeId: target, - }), - FollowRequests.findOneBy({ - followerId: target, - followeeId: me, - }), - Blockings.findOneBy({ - blockerId: me, - blockeeId: target, - }), - Blockings.findOneBy({ - blockerId: target, - blockeeId: me, - }), - Mutings.findOneBy({ - muterId: me, - muteeId: target, - }), - ]); - - return { + return awaitAll({ id: target, - isFollowing: following1 != null, - hasPendingFollowRequestFromYou: followReq1 != null, - hasPendingFollowRequestToYou: followReq2 != null, - isFollowed: following2 != null, - isBlocking: toBlocking != null, - isBlocked: fromBlocked != null, - isMuted: mute != null, - }; + isFollowing: Followings.count({ + where: { + followerId: me, + followeeId: target, + }, + take: 1, + }).then(n => n > 0), + isFollowed: Followings.count({ + where: { + followerId: target, + followeeId: me, + }, + take: 1, + }).then(n => n > 0), + hasPendingFollowRequestFromYou: FollowRequests.count({ + where: { + followerId: me, + followeeId: target, + }, + take: 1, + }).then(n => n > 0), + hasPendingFollowRequestToYou: FollowRequests.count({ + where: { + followerId: target, + followeeId: me, + }, + take: 1, + }).then(n => n > 0), + isBlocking: Blockings.count({ + where: { + blockerId: me, + blockeeId: target, + }, + take: 1, + }).then(n => n > 0), + isBlocked: Blockings.count({ + where: { + blockerId: target, + blockeeId: me, + }, + take: 1, + }).then(n => n > 0), + isMuted: Mutings.count({ + where: { + muterId: me, + muteeId: target, + }, + take: 1, + }).then(n => n > 0), + }); }, async getHasUnreadMessagingMessage(userId: User['id']): Promise { diff --git a/packages/backend/src/models/schema/federation-instance.ts b/packages/backend/src/models/schema/federation-instance.ts index c4e7b3f18..9f27aab98 100644 --- a/packages/backend/src/models/schema/federation-instance.ts +++ b/packages/backend/src/models/schema/federation-instance.ts @@ -88,6 +88,11 @@ export const packedFederationInstanceSchema = { optional: false, nullable: true, format: 'url', }, + faviconUrl: { + type: 'string', + optional: false, nullable: true, + format: 'url', + }, infoUpdatedAt: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index 67d5f5d24..c5fd7de1c 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -305,11 +305,13 @@ export default function() { systemQueue.add('resyncCharts', { }, { repeat: { cron: '0 0 * * *' }, + removeOnComplete: true, }); systemQueue.add('cleanCharts', { }, { repeat: { cron: '0 0 * * *' }, + removeOnComplete: true, }); systemQueue.add('checkExpiredMutings', { diff --git a/packages/backend/src/queue/processors/db/export-custom-emojis.ts b/packages/backend/src/queue/processors/db/export-custom-emojis.ts index 97ba62dcf..8ce1d0527 100644 --- a/packages/backend/src/queue/processors/db/export-custom-emojis.ts +++ b/packages/backend/src/queue/processors/db/export-custom-emojis.ts @@ -9,7 +9,7 @@ import { addFile } from '@/services/drive/add-file.js'; import { format as dateFormat } from 'date-fns'; import { Users, Emojis } from '@/models/index.js'; import { } from '@/queue/types.js'; -import { createTempDir } from '@/misc/create-temp.js'; +import { createTemp, createTempDir } from '@/misc/create-temp.js'; import { downloadUrl } from '@/misc/download-url.js'; import config from '@/config/index.js'; import { IsNull } from 'typeorm'; diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts index ef07966e4..1a02f675c 100644 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ b/packages/backend/src/remote/activitypub/db-resolver.ts @@ -5,14 +5,52 @@ import { User, IRemoteUser, CacheableRemoteUser, CacheableUser } from '@/models/ import { UserPublickey } from '@/models/entities/user-publickey.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js'; -import { IObject, getApId } from './type.js'; -import { resolvePerson } from './models/person.js'; import { Cache } from '@/misc/cache.js'; import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; +import { IObject, getApId } from './type.js'; +import { resolvePerson } from './models/person.js'; const publicKeyCache = new Cache(Infinity); const publicKeyByUserIdCache = new Cache(Infinity); +export type UriParseResult = { + /** wether the URI was generated by us */ + local: true; + /** id in DB */ + id: string; + /** hint of type, e.g. "notes", "users" */ + type: string; + /** any remaining text after type and id, not including the slash after id. undefined if empty */ + rest?: string; +} | { + /** wether the URI was generated by us */ + local: false; + /** uri in DB */ + uri: string; +}; + +export function parseUri(value: string | IObject): UriParseResult { + const uri = getApId(value); + + // the host part of a URL is case insensitive, so use the 'i' flag. + const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i'); + const matchLocal = uri.match(localRegex); + + if (matchLocal) { + return { + local: true, + type: matchLocal[1], + id: matchLocal[2], + rest: matchLocal[3], + }; + } else { + return { + local: false, + uri, + }; + } +} + export default class DbResolver { constructor() { } @@ -21,60 +59,54 @@ export default class DbResolver { * AP Note => Misskey Note in DB */ public async getNoteFromApId(value: string | IObject): Promise { - const parsed = this.parseUri(value); + const parsed = parseUri(value); + + if (parsed.local) { + if (parsed.type !== 'notes') return null; - if (parsed.id) { return await Notes.findOneBy({ id: parsed.id, }); - } - - if (parsed.uri) { + } else { return await Notes.findOneBy({ uri: parsed.uri, }); } - - return null; } public async getMessageFromApId(value: string | IObject): Promise { - const parsed = this.parseUri(value); + const parsed = parseUri(value); + + if (parsed.local) { + if (parsed.type !== 'notes') return null; - if (parsed.id) { return await MessagingMessages.findOneBy({ id: parsed.id, }); - } - - if (parsed.uri) { + } else { return await MessagingMessages.findOneBy({ uri: parsed.uri, }); } - - return null; } /** * AP Person => Misskey User in DB */ public async getUserFromApId(value: string | IObject): Promise { - const parsed = this.parseUri(value); + const parsed = parseUri(value); + + if (parsed.local) { + if (parsed.type !== 'users') return null; - if (parsed.id) { return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({ id: parsed.id, }).then(x => x ?? undefined)) ?? null; - } - - if (parsed.uri) { + } else { return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({ uri: parsed.uri, })); } - - return null; } /** @@ -120,31 +152,4 @@ export default class DbResolver { key, }; } - - public parseUri(value: string | IObject): UriParseResult { - const uri = getApId(value); - - const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/' + '(\\w+)' + '/' + '(\\w+)'); - const matchLocal = uri.match(localRegex); - - if (matchLocal) { - return { - type: matchLocal[1], - id: matchLocal[2], - }; - } else { - return { - uri, - }; - } - } } - -type UriParseResult = { - /** id in DB (local object only) */ - id?: string; - /** uri in DB (remote object only) */ - uri?: string; - /** hint of type (local object only, ex: notes, users) */ - type?: string -}; diff --git a/packages/backend/src/remote/activitypub/misc/get-note-html.ts b/packages/backend/src/remote/activitypub/misc/get-note-html.ts index 3800b4060..389039ebe 100644 --- a/packages/backend/src/remote/activitypub/misc/get-note-html.ts +++ b/packages/backend/src/remote/activitypub/misc/get-note-html.ts @@ -3,8 +3,6 @@ import { Note } from '@/models/entities/note.js'; import { toHtml } from '../../../mfm/to-html.js'; export default function(note: Note) { - let html = note.text ? toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)) : null; - if (html == null) html = '

.

'; - - return html; + if (!note.text) return ''; + return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); } diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index 097a71661..5d63f2605 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -3,9 +3,9 @@ import promiseLimit from 'promise-limit'; import config from '@/config/index.js'; import Resolver from '../resolver.js'; import post from '@/services/note/create.js'; -import { resolvePerson, updatePerson } from './person.js'; +import { resolvePerson } from './person.js'; import { resolveImage } from './image.js'; -import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { htmlToMfm } from '../misc/html-to-mfm.js'; import { extractApHashtags } from './tag.js'; import { unique, toArray, toSingle } from '@/prelude/array.js'; @@ -15,7 +15,7 @@ import { apLogger } from '../logger.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { deliverQuestionUpdate } from '@/services/note/polls/update.js'; import { extractDbHost, toPuny } from '@/misc/convert-host.js'; -import { Emojis, Polls, MessagingMessages, Users } from '@/models/index.js'; +import { Emojis, Polls, MessagingMessages } from '@/models/index.js'; import { Note } from '@/models/entities/note.js'; import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js'; import { Emoji } from '@/models/entities/emoji.js'; @@ -197,7 +197,14 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s 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 !== 'undefined') { + text = note._misskey_content; + } else if (typeof note.content === 'string') { + text = htmlToMfm(note.content, note.tag); + } // vote if (reply && reply.hasPoll) { diff --git a/packages/backend/src/remote/activitypub/renderer/block.ts b/packages/backend/src/remote/activitypub/renderer/block.ts index 10a4fde51..13815fb76 100644 --- a/packages/backend/src/remote/activitypub/renderer/block.ts +++ b/packages/backend/src/remote/activitypub/renderer/block.ts @@ -1,8 +1,20 @@ import config from '@/config/index.js'; -import { ILocalUser, IRemoteUser } from '@/models/entities/user.js'; +import { Blocking } from '@/models/entities/blocking.js'; -export default (blocker: ILocalUser, blockee: IRemoteUser) => ({ - type: 'Block', - actor: `${config.url}/users/${blocker.id}`, - object: blockee.uri, -}); +/** + * Renders a block into its ActivityPub representation. + * + * @param block The block to be rendered. The blockee relation must be loaded. + */ +export function renderBlock(block: Blocking) { + if (block.blockee?.url == null) { + throw new Error('renderBlock: missing blockee uri'); + } + + return { + type: 'Block', + id: `${config.url}/blocks/${block.id}`, + actor: `${config.url}/users/${block.blockerId}`, + object: block.blockee.uri, + }; +} diff --git a/packages/backend/src/remote/activitypub/renderer/follow.ts b/packages/backend/src/remote/activitypub/renderer/follow.ts index 9e9692b77..00fac18ad 100644 --- a/packages/backend/src/remote/activitypub/renderer/follow.ts +++ b/packages/backend/src/remote/activitypub/renderer/follow.ts @@ -4,12 +4,11 @@ import { Users } from '@/models/index.js'; export default (follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string) => { const follow = { + id: requestId ?? `${config.url}/follows/${follower.id}/${followee.id}`, type: 'Follow', actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri, object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri, } as any; - if (requestId) follow.id = requestId; - return follow; }; diff --git a/packages/backend/src/remote/activitypub/renderer/index.ts b/packages/backend/src/remote/activitypub/renderer/index.ts index 5f6933226..f100b77ce 100644 --- a/packages/backend/src/remote/activitypub/renderer/index.ts +++ b/packages/backend/src/remote/activitypub/renderer/index.ts @@ -8,7 +8,7 @@ import { User } from '@/models/entities/user.js'; export const renderActivity = (x: any): IActivity | null => { if (x == null) return null; - if (x !== null && typeof x === 'object' && x.id == null) { + if (typeof x === 'object' && x.id == null) { x.id = `${config.url}/${uuid()}`; } diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index e8d429e5d..b3bafaa3a 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -82,7 +82,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false const files = await getPromisedFiles(note.fileIds); - const text = note.text; + const text = note.text ?? ''; let poll: Poll | null = null; if (note.hasPoll) { @@ -90,7 +90,6 @@ export default async function renderNote(note: Note, dive = true, isTalk = false } let apText = text; - if (apText == null) apText = ''; if (quote) { apText += `\n\nRE: ${quote}`; @@ -138,6 +137,10 @@ export default async function renderNote(note: Note, dive = true, isTalk = false summary, content, _misskey_content: text, + source: { + content: text, + mediaType: "text/x.misskeymarkdown", + }, _misskey_quote: quote, quoteUrl: quote, published: note.createdAt.toISOString(), diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index 334eae984..2f9af43c0 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -3,9 +3,18 @@ import { getJson } from '@/misc/fetch.js'; import { ILocalUser } from '@/models/entities/user.js'; import { getInstanceActor } from '@/services/instance-actor.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; -import { extractDbHost } from '@/misc/convert-host.js'; +import { extractDbHost, isSelfHost } from '@/misc/convert-host.js'; import { signedGet } from './request.js'; import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js'; +import { FollowRequests, Notes, NoteReactions, Polls, Users } from '@/models/index.js'; +import { parseUri } from './db-resolver.js'; +import renderNote from '@/remote/activitypub/renderer/note.js'; +import { renderLike } from '@/remote/activitypub/renderer/like.js'; +import { renderPerson } from '@/remote/activitypub/renderer/person.js'; +import renderQuestion from '@/remote/activitypub/renderer/question.js'; +import renderCreate from '@/remote/activitypub/renderer/create.js'; +import { renderActivity } from '@/remote/activitypub/renderer/index.js'; +import renderFollow from '@/remote/activitypub/renderer/follow.js'; export default class Resolver { private history: Set; @@ -40,14 +49,25 @@ export default class Resolver { return value; } + if (value.includes('#')) { + // URLs with fragment parts cannot be resolved correctly because + // the fragment part does not get transmitted over HTTP(S). + // Avoid strange behaviour by not trying to resolve these at all. + throw new Error(`cannot resolve URL with fragment: ${value}`); + } + if (this.history.has(value)) { throw new Error('cannot resolve already resolved one'); } this.history.add(value); - const meta = await fetchMeta(); const host = extractDbHost(value); + if (isSelfHost(host)) { + return await this.resolveLocal(value); + } + + const meta = await fetchMeta(); if (meta.blockedHosts.includes(host)) { throw new Error('Instance is blocked'); } @@ -70,4 +90,44 @@ export default class Resolver { return object; } + + private resolveLocal(url: string): Promise { + 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 + 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`); + } + } } diff --git a/packages/backend/src/remote/activitypub/type.ts b/packages/backend/src/remote/activitypub/type.ts index ef5b98b59..5d00481b7 100644 --- a/packages/backend/src/remote/activitypub/type.ts +++ b/packages/backend/src/remote/activitypub/type.ts @@ -106,7 +106,10 @@ export const isPost = (object: IObject): object is IPost => export interface IPost extends IObject { type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event'; - _misskey_content?: string; + source?: { + content: string; + mediaType: string; + }; _misskey_quote?: string; quoteUrl?: string; _misskey_talk: boolean; @@ -114,7 +117,10 @@ export interface IPost extends IObject { export interface IQuestion extends IObject { type: 'Note' | 'Question'; - _misskey_content?: string; + source?: { + content: string; + mediaType: string; + }; _misskey_quote?: string; quoteUrl?: string; oneOf?: IQuestionChoice[]; diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index a48c2d412..cd5f917c4 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -15,9 +15,10 @@ import { inbox as processInbox } from '@/queue/index.js'; import { isSelfHost } from '@/misc/convert-host.js'; import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js'; import { ILocalUser, User } from '@/models/entities/user.js'; -import { In, IsNull } from 'typeorm'; +import { In, IsNull, Not } from 'typeorm'; import { renderLike } from '@/remote/activitypub/renderer/like.js'; import { getUserKeypair } from '@/misc/keypair-store.js'; +import renderFollow from '@/remote/activitypub/renderer/follow.js'; // Init router const router = new Router(); @@ -224,4 +225,30 @@ router.get('/likes/:like', async ctx => { setResponseType(ctx); }); +// follow +router.get('/follows/:follower/:followee', async ctx => { + // This may be used before the follow is completed, so we do not + // check if the following exists. + + const [follower, followee] = await Promise.all([ + Users.findOneBy({ + id: ctx.params.follower, + host: IsNull(), + }), + Users.findOneBy({ + id: ctx.params.followee, + host: Not(IsNull()), + }), + ]); + + if (follower == null || followee == null) { + ctx.status = 404; + return; + } + + ctx.body = renderActivity(renderFollow(follower, followee)); + ctx.set('Cache-Control', 'public, max-age=180'); + setResponseType(ctx); +}); + export default router; diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index 9a85e4565..46afde4e4 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -1,11 +1,12 @@ -import Koa from 'koa'; import { performance } from 'perf_hooks'; -import { limiter } from './limiter.js'; +import Koa from 'koa'; import { CacheableLocalUser, User } from '@/models/entities/user.js'; -import endpoints, { IEndpoint } from './endpoints.js'; +import { AccessToken } from '@/models/entities/access-token.js'; +import { getIpHash } from '@/misc/get-ip-hash.js'; +import { limiter } from './limiter.js'; +import endpoints, { IEndpointMeta } from './endpoints.js'; import { ApiError } from './error.js'; import { apiLogger } from './logger.js'; -import { AccessToken } from '@/models/entities/access-token.js'; const accessDenied = { message: 'Access denied.', @@ -15,6 +16,7 @@ const accessDenied = { export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => { const isSecure = user != null && token == null; + const isModerator = user != null && (user.isModerator || user.isAdmin); const ep = endpoints.find(e => e.name === endpoint); @@ -31,6 +33,32 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi throw new ApiError(accessDenied); } + if (ep.meta.limit) { + // 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 }, limitActor).catch(e => { + throw new ApiError({ + message: 'Rate limit exceeded. Please try again later.', + code: 'RATE_LIMIT_EXCEEDED', + id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', + httpStatusCode: 429, + }); + }); + } + if (ep.meta.requireCredential && user == null) { throw new ApiError({ message: 'Credential required.', @@ -53,7 +81,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi throw new ApiError(accessDenied, { reason: 'You are not the admin.' }); } - if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) { + if (ep.meta.requireModerator && !isModerator) { throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); } @@ -65,18 +93,6 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi }); } - if (ep.meta.requireCredential && ep.meta.limit && !user!.isAdmin && !user!.isModerator) { - // Rate limit - await limiter(ep as IEndpoint & { meta: { limit: NonNullable } }, user!).catch(e => { - throw new ApiError({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - httpStatusCode: 429, - }); - }); - } - // Cast non JSON input if (ep.meta.requireFile && ep.params.properties) { for (const k of Object.keys(ep.params.properties)) { @@ -104,20 +120,20 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi if (e instanceof ApiError) { throw e; } else { - apiLogger.error(`Internal error occurred in ${ep.name}: ${e?.message}`, { + apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`, { ep: ep.name, ps: data, e: { - message: e?.message, - code: e?.name, - stack: e?.stack, + message: e.message, + code: e.name, + stack: e.stack, }, }); throw new ApiError(null, { e: { - message: e?.message, - code: e?.name, - stack: e?.stack, + message: e.message, + code: e.name, + stack: e.stack, }, }); } diff --git a/packages/backend/src/server/api/common/generate-visibility-query.ts b/packages/backend/src/server/api/common/generate-visibility-query.ts index 715982934..b50b6812f 100644 --- a/packages/backend/src/server/api/common/generate-visibility-query.ts +++ b/packages/backend/src/server/api/common/generate-visibility-query.ts @@ -3,6 +3,7 @@ import { Followings } from '@/models/index.js'; import { Brackets, SelectQueryBuilder } from 'typeorm'; export function generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: User['id'] } | null) { + // This code must always be synchronized with the checks in Notes.isVisibleForMe. if (me == null) { q.andWhere(new Brackets(qb => { qb .where(`note.visibility = 'public'`) @@ -11,7 +12,7 @@ export function generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: U } else { const followingQuery = Followings.createQueryBuilder('following') .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); + .where('following.followerId = :meId'); q.andWhere(new Brackets(qb => { qb // 公開投稿である @@ -20,21 +21,22 @@ export function generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: U .orWhere(`note.visibility = 'home'`); })) // または 自分自身 - .orWhere('note.userId = :userId1', { userId1: me.id }) + .orWhere('note.userId = :meId') // または 自分宛て - .orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`) + .orWhere(':meId = ANY(note.visibleUserIds)') + .orWhere(':meId = ANY(note.mentions)') .orWhere(new Brackets(qb => { qb // または フォロワー宛ての投稿であり、 - .where('note.visibility = \'followers\'') + .where(`note.visibility = 'followers'`) .andWhere(new Brackets(qb => { qb // 自分がフォロワーである .where(`note.userId IN (${ followingQuery.getQuery() })`) // または 自分の投稿へのリプライ - .orWhere('note.replyUserId = :userId3', { userId3: me.id }); + .orWhere('note.replyUserId = :meId'); })); })); })); - q.setParameters(followingQuery.getParameters()); + q.setParameters({ meId: me.id }); } } diff --git a/packages/backend/src/server/api/common/read-notification.ts b/packages/backend/src/server/api/common/read-notification.ts index 0dad35bcc..8c4ba41a3 100644 --- a/packages/backend/src/server/api/common/read-notification.ts +++ b/packages/backend/src/server/api/common/read-notification.ts @@ -9,6 +9,8 @@ export async function readNotification( userId: User['id'], notificationIds: Notification['id'][] ) { + if (notificationIds.length === 0) return; + // Update documents await Notifications.update({ id: In(notificationIds), diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index e2db03f13..5fac7df23 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -99,6 +99,7 @@ import * as ep___charts_user_notes from './endpoints/charts/user/notes.js'; import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js'; import * as ep___charts_users from './endpoints/charts/users.js'; import * as ep___clips_addNote from './endpoints/clips/add-note.js'; +import * as ep___clips_removeNote from './endpoints/clips/remove-note.js'; import * as ep___clips_create from './endpoints/clips/create.js'; import * as ep___clips_delete from './endpoints/clips/delete.js'; import * as ep___clips_list from './endpoints/clips/list.js'; @@ -409,6 +410,7 @@ const eps = [ ['charts/user/reactions', ep___charts_user_reactions], ['charts/users', ep___charts_users], ['clips/add-note', ep___clips_addNote], + ['clips/remove-note', ep___clips_removeNote], ['clips/create', ep___clips_create], ['clips/delete', ep___clips_delete], ['clips/list', ep___clips_list], @@ -654,7 +656,6 @@ export interface IEndpointMeta { /** * エンドポイントのリミテーションに関するやつ * 省略した場合はリミテーションは無いものとして解釈されます。 - * また、withCredential が false の場合はリミテーションを行うことはできません。 */ readonly limit?: { diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index bf6cc1653..78033aed5 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -1,5 +1,5 @@ +import { Signins, UserProfiles, Users } from '@/models/index.js'; import define from '../../define.js'; -import { Users } from '@/models/index.js'; export const meta = { tags: ['admin'], @@ -23,9 +23,12 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); + const [user, profile] = await Promise.all([ + Users.findOneBy({ id: ps.userId }), + UserProfiles.findOneBy({ userId: ps.userId }) + ]); - if (user == null) { + if (user == null || profile == null) { throw new Error('user not found'); } @@ -34,8 +37,37 @@ export default define(meta, paramDef, async (ps, me) => { throw new Error('cannot show info of admin'); } + if (!_me.isAdmin) { + return { + isModerator: user.isModerator, + isSilenced: user.isSilenced, + isSuspended: user.isSuspended, + }; + } + + const maskedKeys = ['accessToken', 'accessTokenSecret', 'refreshToken']; + Object.keys(profile.integrations).forEach(integration => { + maskedKeys.forEach(key => profile.integrations[integration][key] = ''); + }); + + const signins = await Signins.findBy({ userId: user.id }); + return { - ...user, - token: user.token != null ? '' : user.token, + email: profile.email, + emailVerified: profile.emailVerified, + autoAcceptFollowed: profile.autoAcceptFollowed, + noCrawle: profile.noCrawle, + alwaysMarkNsfw: profile.alwaysMarkNsfw, + carefulBot: profile.carefulBot, + injectFeaturedNote: profile.injectFeaturedNote, + receiveAnnouncementEmail: profile.receiveAnnouncementEmail, + integrations: profile.integrations, + mutedWords: profile.mutedWords, + mutedInstances: profile.mutedInstances, + mutingNotificationTypes: profile.mutingNotificationTypes, + isModerator: user.isModerator, + isSilenced: user.isSilenced, + isSuspended: user.isSuspended, + signins, }; }); diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts index 222efdcef..23cb93c9a 100644 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ b/packages/backend/src/server/api/endpoints/announcements.ts @@ -1,5 +1,5 @@ -import define from '../define.js'; import { Announcements, AnnouncementReads } from '@/models/index.js'; +import define from '../define.js'; import { makePaginationQuery } from '../common/make-pagination-query.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts new file mode 100644 index 000000000..8b90e31f6 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts @@ -0,0 +1,57 @@ +import define from '../../define.js'; +import { ClipNotes, Clips } from '@/models/index.js'; +import { ApiError } from '../../error.js'; +import { getNote } from '../../common/getters.js'; + +export const meta = { + tags: ['account', 'notes', 'clips'], + + requireCredential: true, + + kind: 'write:account', + + errors: { + noSuchClip: { + message: 'No such clip.', + code: 'NO_SUCH_CLIP', + id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', + }, + + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: 'aff017de-190e-434b-893e-33a9ff5049d8', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + clipId: { type: 'string', format: 'misskey:id' }, + noteId: { type: 'string', format: 'misskey:id' }, + }, + required: ['clipId', 'noteId'], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps, user) => { + const clip = await Clips.findOneBy({ + id: ps.clipId, + userId: user.id, + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + + const note = await getNote(ps.noteId).catch(e => { + if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw e; + }); + + await ClipNotes.delete({ + noteId: note.id, + clipId: clip.id, + }); +}); diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index c599d96ca..47e940cdd 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -1,6 +1,6 @@ -import define from '../define.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { DriveFiles } from '@/models/index.js'; +import define from '../define.js'; export const meta = { tags: ['drive', 'account'], diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index 7ffe89a1e..415a8cc69 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -9,6 +9,8 @@ export const meta = { kind: 'read:drive', + description: 'Find the notes to which the given file is attached.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts index 80293df5d..bbae9bf4e 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts @@ -8,6 +8,8 @@ export const meta = { kind: 'read:drive', + description: 'Check if a given file exists.', + res: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index 0939ae336..7397fd9ce 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -20,6 +20,8 @@ export const meta = { kind: 'write:drive', + description: 'Upload a new drive file.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts index 61c56e631..6108ae7da 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -11,6 +11,8 @@ export const meta = { kind: 'write:drive', + description: 'Delete an existing drive file.', + errors: { noSuchFile: { message: 'No such file.', diff --git a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts index 0b74cb9f0..f2bc7348c 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts @@ -8,6 +8,8 @@ export const meta = { kind: 'read:drive', + description: 'Search for a drive file by a hash of the contents.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/drive/files/find.ts b/packages/backend/src/server/api/endpoints/drive/files/find.ts index 4938a69d1..245fb45a6 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts @@ -9,6 +9,8 @@ export const meta = { kind: 'read:drive', + description: 'Search for a drive file by the given parameters.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts index fb19345fe..2c604c54c 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -10,6 +10,8 @@ export const meta = { kind: 'read:drive', + description: 'Show the properties of a drive file.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index 4b3f5f2dc..e3debe0b4 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -11,6 +11,8 @@ export const meta = { kind: 'write:drive', + description: 'Update the properties of a drive file.', + errors: { invalidFileName: { message: 'Invalid file name.', diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index 3bfecac80..53f2298f2 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -13,6 +13,8 @@ export const meta = { max: 60, }, + description: 'Request the server to download a new drive file from the specified URL.', + requireCredential: true, kind: 'write:drive', diff --git a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts index bc8d2e2ac..5fe622932 100644 --- a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts +++ b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts @@ -1,6 +1,6 @@ -import define from '../define.js'; -import { createExportCustomEmojisJob } from '@/queue/index.js'; import ms from 'ms'; +import { createExportCustomEmojisJob } from '@/queue/index.js'; +import define from '../define.js'; export const meta = { secure: true, diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts index b0c1225be..56c550297 100644 --- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts +++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts @@ -1,6 +1,6 @@ +import { MoreThan } from 'typeorm'; import { USER_ONLINE_THRESHOLD } from '@/const.js'; import { Users } from '@/models/index.js'; -import { MoreThan } from 'typeorm'; import define from '../define.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index 5b1ad2b09..22aedfeee 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -1,5 +1,5 @@ -import define from '../define.js'; import { Users } from '@/models/index.js'; +import define from '../define.js'; export const meta = { tags: ['account'], diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 6ea8cb357..1c31ce7a6 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -1,17 +1,22 @@ +import { Brackets } from 'typeorm'; +import { Notifications, Followings, Mutings, Users } from '@/models/index.js'; +import { notificationTypes } from '@/types.js'; +import read from '@/services/note/read.js'; import { readNotification } from '../../common/read-notification.js'; import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateMutedInstanceNotificationQuery } from '../../common/generate-muted-instance-query.js'; -import { Notifications, Followings, Mutings, Users } from '@/models/index.js'; -import { notificationTypes } from '@/types.js'; -import read from '@/services/note/read.js'; -import { Brackets } from 'typeorm'; export const meta = { tags: ['account', 'notifications'], requireCredential: true, + limit: { + duration: 60000, + max: 10, + }, + kind: 'read:notifications', res: { @@ -67,7 +72,7 @@ export default define(meta, paramDef, async (ps, user) => { .where('users.isSuspended = TRUE'); const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId) - .andWhere(`notification.notifieeId = :meId`, { meId: user.id }) + .andWhere('notification.notifieeId = :meId', { meId: user.id }) .leftJoinAndSelect('notification.notifier', 'notifier') .leftJoinAndSelect('notification.note', 'note') .leftJoinAndSelect('notifier.avatar', 'notifierAvatar') @@ -103,13 +108,13 @@ export default define(meta, paramDef, async (ps, user) => { } if (ps.includeTypes && ps.includeTypes.length > 0) { - query.andWhere(`notification.type IN (:...includeTypes)`, { includeTypes: ps.includeTypes }); + query.andWhere('notification.type IN (:...includeTypes)', { includeTypes: ps.includeTypes }); } else if (ps.excludeTypes && ps.excludeTypes.length > 0) { - query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes }); + query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes: ps.excludeTypes }); } if (ps.unreadOnly) { - query.andWhere(`notification.isRead = false`); + query.andWhere('notification.isRead = false'); } const notifications = await query.take(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index e1ae282a9..5b624842c 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -1,10 +1,10 @@ +import { IsNull, MoreThan } from 'typeorm'; import config from '@/config/index.js'; -import define from '../define.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Ads, Emojis, Users } from '@/models/index.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits.js'; -import { IsNull, MoreThan } from 'typeorm'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import define from '../define.js'; export const meta = { tags: ['meta'], diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts index 2733c826e..015b0338e 100644 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ b/packages/backend/src/server/api/endpoints/notes.ts @@ -1,6 +1,6 @@ +import { Notes } from '@/models/index.js'; import define from '../define.js'; import { makePaginationQuery } from '../common/make-pagination-query.js'; -import { Notes } from '@/models/index.js'; export const meta = { tags: ['notes'], @@ -34,8 +34,8 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps) => { const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(`note.visibility = 'public'`) - .andWhere(`note.localOnly = FALSE`) + .andWhere('note.visibility = \'public\'') + .andWhere('note.localOnly = FALSE') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') @@ -61,7 +61,7 @@ export default define(meta, paramDef, async (ps) => { } if (ps.withFiles !== undefined) { - query.andWhere(ps.withFiles ? `note.fileIds != '{}'` : `note.fileIds = '{}'`); + query.andWhere(ps.withFiles ? 'note.fileIds != \'{}\'' : 'note.fileIds = \'{}\''); } if (ps.poll !== undefined) { diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 86dde30d6..50ba293a5 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -1,9 +1,9 @@ +import { Brackets } from 'typeorm'; +import { Notes } from '@/models/index.js'; import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { Brackets } from 'typeorm'; -import { Notes } from '@/models/index.js'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js'; @@ -38,13 +38,13 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) .andWhere(new Brackets(qb => { qb - .where(`note.replyId = :noteId`, { noteId: ps.noteId }) + .where('note.replyId = :noteId', { noteId: ps.noteId }) .orWhere(new Brackets(qb => { qb - .where(`note.renoteId = :noteId`, { noteId: ps.noteId }) + .where('note.renoteId = :noteId', { noteId: ps.noteId }) .andWhere(new Brackets(qb => { qb - .where(`note.text IS NOT NULL`) - .orWhere(`note.fileIds != '{}'`) - .orWhere(`note.hasPoll = TRUE`); + .where('note.text IS NOT NULL') + .orWhere('note.fileIds != \'{}\'') + .orWhere('note.hasPoll = TRUE'); })); })); })) diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts index 8683a7f75..e79f8563e 100644 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ b/packages/backend/src/server/api/endpoints/notes/clips.ts @@ -1,8 +1,8 @@ -import define from '../../define.js'; +import { In } from 'typeorm'; import { ClipNotes, Clips } from '@/models/index.js'; +import define from '../../define.js'; import { getNote } from '../../common/getters.js'; import { ApiError } from '../../error.js'; -import { In } from 'typeorm'; export const meta = { tags: ['clips', 'notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index b991a495f..b731d1824 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -1,8 +1,8 @@ +import { Note } from '@/models/entities/note.js'; +import { Notes } from '@/models/index.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; import { getNote } from '../../common/getters.js'; -import { Note } from '@/models/entities/note.js'; -import { Notes } from '@/models/index.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 955f53bbc..a13329416 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -134,7 +134,7 @@ export const paramDef = { { // (re)note with text, files and poll are optional properties: { - text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false }, + text: { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false }, }, required: ['text'], }, diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index 804e146fa..c23ceeb5b 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -1,9 +1,9 @@ -import deleteNote from '@/services/note/delete.js'; -import define from '../../define.js'; import ms from 'ms'; +import deleteNote from '@/services/note/delete.js'; +import { Users } from '@/models/index.js'; +import define from '../../define.js'; import { getNote } from '../../common/getters.js'; import { ApiError } from '../../error.js'; -import { Users } from '@/models/index.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index 41dc5ac8e..097371a42 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -1,8 +1,8 @@ +import { NoteFavorites } from '@/models/index.js'; +import { genId } from '@/misc/gen-id.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; import { getNote } from '../../../common/getters.js'; -import { NoteFavorites } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; export const meta = { tags: ['notes', 'favorites'], diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts index a48f7a0aa..82ef4fa19 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts @@ -1,7 +1,7 @@ +import { NoteFavorites } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; import { getNote } from '../../../common/getters.js'; -import { NoteFavorites } from '@/models/index.js'; export const meta = { tags: ['notes', 'favorites'], diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index 6308d2369..dd9cc581a 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -1,6 +1,6 @@ +import { Notes } from '@/models/index.js'; import define from '../../define.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { Notes } from '@/models/index.js'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; export const meta = { @@ -36,9 +36,9 @@ export default define(meta, paramDef, async (ps, user) => { const query = Notes.createQueryBuilder('note') .addSelect('note.score') .where('note.userHost IS NULL') - .andWhere(`note.score > 0`) - .andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) }) - .andWhere(`note.visibility = 'public'`) + .andWhere('note.score > 0') + .andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) }) + .andWhere('note.visibility = \'public\'') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index cb402ecaa..418fc62c3 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -1,11 +1,11 @@ -import define from '../../define.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; +import { Notes, Users } from '@/models/index.js'; +import { activeUsersChart } from '@/services/chart/index.js'; +import define from '../../define.js'; import { ApiError } from '../../error.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Notes, Users } from '@/models/index.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js'; -import { activeUsersChart } from '@/services/chart/index.js'; import { generateRepliesQuery } from '../../common/generate-replies-query.js'; import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; @@ -60,7 +60,7 @@ export default define(meta, paramDef, async (ps, user) => { //#region Construct query const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('note.visibility = \'public\'') .andWhere('note.channelId IS NULL') .innerJoinAndSelect('note.user', 'user') diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index f9893527e..52ee81799 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -1,13 +1,13 @@ -import define from '../../define.js'; +import { Brackets } from 'typeorm'; import { fetchMeta } from '@/misc/fetch-meta.js'; +import { Followings, Notes, Users } from '@/models/index.js'; +import { activeUsersChart } from '@/services/chart/index.js'; +import define from '../../define.js'; import { ApiError } from '../../error.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Followings, Notes, Users } from '@/models/index.js'; -import { Brackets } from 'typeorm'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js'; -import { activeUsersChart } from '@/services/chart/index.js'; import { generateRepliesQuery } from '../../common/generate-replies-query.js'; import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js'; import { generateChannelQuery } from '../../common/generate-channel-query.js'; @@ -70,7 +70,7 @@ export default define(meta, paramDef, async (ps, user) => { .where('following.followerId = :followerId', { followerId: user.id }); const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere(new Brackets(qb => { qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id }) .orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 03edf30b3..aac2a3749 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -1,12 +1,12 @@ -import define from '../../define.js'; +import { Brackets } from 'typeorm'; import { fetchMeta } from '@/misc/fetch-meta.js'; -import { ApiError } from '../../error.js'; import { Notes, Users } from '@/models/index.js'; +import { activeUsersChart } from '@/services/chart/index.js'; +import define from '../../define.js'; +import { ApiError } from '../../error.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { activeUsersChart } from '@/services/chart/index.js'; -import { Brackets } from 'typeorm'; import { generateRepliesQuery } from '../../common/generate-replies-query.js'; import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js'; import { generateChannelQuery } from '../../common/generate-channel-query.js'; @@ -66,7 +66,7 @@ export default define(meta, paramDef, async (ps, user) => { //#region Construct query const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index eafbba322..9b4154452 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -1,10 +1,10 @@ -import define from '../../define.js'; +import { Brackets } from 'typeorm'; import read from '@/services/note/read.js'; import { Notes, Followings } from '@/models/index.js'; +import define from '../../define.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Brackets } from 'typeorm'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; import { generateMutedNoteThreadQuery } from '../../common/generate-muted-note-thread-query.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts index 28bfade2f..2150efaaf 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -1,6 +1,6 @@ -import define from '../../../define.js'; -import { Polls, Mutings, Notes, PollVotes } from '@/models/index.js'; import { Brackets, In } from 'typeorm'; +import { Polls, Mutings, Notes, PollVotes } from '@/models/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['notes'], @@ -31,8 +31,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const query = Polls.createQueryBuilder('poll') .where('poll.userHost IS NULL') - .andWhere(`poll.userId != :meId`, { meId: user.id }) - .andWhere(`poll.noteVisibility = 'public'`) + .andWhere('poll.userId != :meId', { meId: user.id }) + .andWhere('poll.noteVisibility = \'public\'') .andWhere(new Brackets(qb => { qb .where('poll.expiresAt IS NULL') .orWhere('poll.expiresAt > :now', { now: new Date() }); diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index 6244b55cf..45a832cbd 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -1,16 +1,16 @@ +import { Not } from 'typeorm'; import { publishNoteStream } from '@/services/stream.js'; import { createNotification } from '@/services/create-notification.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; -import { getNote } from '../../../common/getters.js'; import { deliver } from '@/queue/index.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderVote from '@/remote/activitypub/renderer/vote.js'; import { deliverQuestionUpdate } from '@/services/note/polls/update.js'; import { PollVotes, NoteWatchings, Users, Polls, Blockings } from '@/models/index.js'; -import { Not } from 'typeorm'; import { IRemoteUser } from '@/models/entities/user.js'; import { genId } from '@/misc/gen-id.js'; +import { getNote } from '../../../common/getters.js'; +import { ApiError } from '../../../error.js'; +import define from '../../../define.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts index 639ecae26..c13cafa21 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -1,6 +1,6 @@ -import define from '../../../define.js'; import ms from 'ms'; import deleteReaction from '@/services/note/reaction/delete.js'; +import define from '../../../define.js'; import { getNote } from '../../../common/getters.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index 87c855a5e..28be36076 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -1,10 +1,10 @@ +import { Notes } from '@/models/index.js'; import define from '../../define.js'; import { getNote } from '../../common/getters.js'; import { ApiError } from '../../error.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Notes } from '@/models/index.js'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; export const meta = { @@ -50,7 +50,7 @@ export default define(meta, paramDef, async (ps, user) => { }); const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(`note.renoteId = :renoteId`, { renoteId: note.id }) + .andWhere('note.renoteId = :renoteId', { renoteId: note.id }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index 3053eabe3..ab0018f58 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { Notes } from '@/models/index.js'; +import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index bb85c9200..777de7221 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -1,11 +1,11 @@ -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Notes } from '@/models/index.js'; -import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { Brackets } from 'typeorm'; +import { Notes } from '@/models/index.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; +import define from '../../define.js'; +import { makePaginationQuery } from '../../common/make-pagination-query.js'; +import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index af9b5f0a1..4e2cdae80 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,8 +1,8 @@ +import { In } from 'typeorm'; +import { Notes } from '@/models/index.js'; +import config from '@/config/index.js'; import es from '../../../../db/elasticsearch.js'; import define from '../../define.js'; -import { Notes } from '@/models/index.js'; -import { In } from 'typeorm'; -import config from '@/config/index.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; @@ -99,7 +99,7 @@ export default define(meta, paramDef, async (ps, me) => { userHost: ps.host, }, }] : [] - : []; + : []; const result = await es.search({ index: config.elasticsearch.index || 'misskey_note', diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index d6692923c..5cd74bd2c 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -1,7 +1,7 @@ +import { Notes } from '@/models/index.js'; import define from '../../define.js'; import { getNote } from '../../common/getters.js'; import { ApiError } from '../../error.js'; -import { Notes } from '@/models/index.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index 069f11fa4..01afa5add 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts index e48a2cf57..cf360526d 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts @@ -1,9 +1,9 @@ -import define from '../../../define.js'; -import { getNote } from '../../../common/getters.js'; -import { ApiError } from '../../../error.js'; import { Notes, NoteThreadMutings } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import readNote from '@/services/note/read.js'; +import define from '../../../define.js'; +import { getNote } from '../../../common/getters.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts index 4fb3137a5..ac310d0fe 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts @@ -1,7 +1,7 @@ +import { NoteThreadMutings } from '@/models/index.js'; import define from '../../../define.js'; import { getNote } from '../../../common/getters.js'; import { ApiError } from '../../../error.js'; -import { NoteThreadMutings } from '@/models/index.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 0f976d18b..d80940e95 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -1,11 +1,11 @@ +import { Brackets } from 'typeorm'; +import { Notes, Followings } from '@/models/index.js'; +import { activeUsersChart } from '@/services/chart/index.js'; import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Notes, Followings } from '@/models/index.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js'; -import { activeUsersChart } from '@/services/chart/index.js'; -import { Brackets } from 'typeorm'; import { generateRepliesQuery } from '../../common/generate-replies-query.js'; import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js'; import { generateChannelQuery } from '../../common/generate-channel-query.js'; @@ -62,10 +62,10 @@ export default define(meta, paramDef, async (ps, user) => { .where('following.followerId = :followerId', { followerId: user.id }); const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere(new Brackets(qb => { qb .where('note.userId = :meId', { meId: user.id }); - if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`); + if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`); })) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts index 5e8c31eaf..3fba0efe0 100644 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts @@ -1,9 +1,9 @@ -import deleteNote from '@/services/note/delete.js'; -import define from '../../define.js'; import ms from 'ms'; +import deleteNote from '@/services/note/delete.js'; +import { Notes, Users } from '@/models/index.js'; +import define from '../../define.js'; import { getNote } from '../../common/getters.js'; import { ApiError } from '../../error.js'; -import { Notes, Users } from '@/models/index.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index fd4a87903..e603a8f62 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -1,10 +1,10 @@ +import { Brackets } from 'typeorm'; +import { UserLists, UserListJoinings, Notes } from '@/models/index.js'; +import { activeUsersChart } from '@/services/chart/index.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { UserLists, UserListJoinings, Notes } from '@/models/index.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { activeUsersChart } from '@/services/chart/index.js'; -import { Brackets } from 'typeorm'; export const meta = { tags: ['notes', 'lists'], diff --git a/packages/backend/src/server/api/endpoints/notes/watching/create.ts b/packages/backend/src/server/api/endpoints/notes/watching/create.ts index 8fdf84624..7d482b073 100644 --- a/packages/backend/src/server/api/endpoints/notes/watching/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/watching/create.ts @@ -1,5 +1,5 @@ -import define from '../../../define.js'; import watch from '@/services/note/watch.js'; +import define from '../../../define.js'; import { getNote } from '../../../common/getters.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts index d58f09797..2c1a2e5fb 100644 --- a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts @@ -1,5 +1,5 @@ -import define from '../../../define.js'; import unwatch from '@/services/note/unwatch.js'; +import define from '../../../define.js'; import { getNote } from '../../../common/getters.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts index b339c8723..80d513d8d 100644 --- a/packages/backend/src/server/api/endpoints/notifications/create.ts +++ b/packages/backend/src/server/api/endpoints/notifications/create.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { createNotification } from '@/services/create-notification.js'; +import define from '../../define.js'; export const meta = { tags: ['notifications'], diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts index 4575cba43..d169afbb3 100644 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -1,7 +1,7 @@ import { publishMainStream } from '@/services/stream.js'; import { pushNotification } from '@/services/push-notification.js'; -import define from '../../define.js'; import { Notifications } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['notifications', 'account'], diff --git a/packages/backend/src/server/api/endpoints/notifications/read.ts b/packages/backend/src/server/api/endpoints/notifications/read.ts index 65e96d486..7bce525a5 100644 --- a/packages/backend/src/server/api/endpoints/notifications/read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/read.ts @@ -2,17 +2,14 @@ import define from '../../define.js'; import { readNotification } from '../../common/read-notification.js'; export const meta = { - desc: { - 'ja-JP': '通知を既読にします。', - 'en-US': 'Mark a notification as read.' - }, - tags: ['notifications', 'account'], requireCredential: true, kind: 'write:notifications', + description: 'Mark a notification as read.', + errors: { noSuchNotification: { message: 'No such notification.', @@ -34,7 +31,11 @@ export const paramDef = { { type: 'object', properties: { - notificationIds: { type: 'array', items: { type: 'string', format: 'misskey:id' } }, + notificationIds: { + type: 'array', + items: { type: 'string', format: 'misskey:id' }, + maxItems: 100, + }, }, required: ['notificationIds'], }, diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts index 7096aaa3d..6dd3ede85 100644 --- a/packages/backend/src/server/api/endpoints/page-push.ts +++ b/packages/backend/src/server/api/endpoints/page-push.ts @@ -1,6 +1,6 @@ -import define from '../define.js'; import { publishMainStream } from '@/services/stream.js'; import { Users, Pages } from '@/models/index.js'; +import define from '../define.js'; import { ApiError } from '../error.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index c171cd39f..b008cde84 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -1,8 +1,8 @@ import ms from 'ms'; -import define from '../../define.js'; import { Pages, DriveFiles } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { Page } from '@/models/entities/page.js'; +import define from '../../define.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -51,7 +51,7 @@ export const paramDef = { } }, script: { type: 'string' }, eyeCatchingImageId: { type: 'string', format: 'misskey:id', nullable: true }, - font: { type: 'string', enum: ['serif', 'sans-serif'], default: "sans-serif" }, + font: { type: 'string', enum: ['serif', 'sans-serif'], default: 'sans-serif' }, alignCenter: { type: 'boolean', default: false }, hideTitleWhenPinned: { type: 'boolean', default: false }, }, diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts index e35ad9ebf..a7708e658 100644 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ b/packages/backend/src/server/api/endpoints/pages/delete.ts @@ -1,6 +1,6 @@ +import { Pages } from '@/models/index.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { Pages } from '@/models/index.js'; export const meta = { tags: ['pages'], diff --git a/packages/backend/src/server/api/endpoints/pages/featured.ts b/packages/backend/src/server/api/endpoints/pages/featured.ts index eeb6d509c..5a149a626 100644 --- a/packages/backend/src/server/api/endpoints/pages/featured.ts +++ b/packages/backend/src/server/api/endpoints/pages/featured.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { Pages } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['pages'], diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index 20793db98..269b539f7 100644 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ b/packages/backend/src/server/api/endpoints/pages/like.ts @@ -1,7 +1,7 @@ -import define from '../../define.js'; -import { ApiError } from '../../error.js'; import { Pages, PageLikes } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; +import define from '../../define.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['pages'], diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts index 636f3c714..6b3a2bec1 100644 --- a/packages/backend/src/server/api/endpoints/pages/unlike.ts +++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts @@ -1,6 +1,6 @@ +import { Pages, PageLikes } from '@/models/index.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { Pages, PageLikes } from '@/models/index.js'; export const meta = { tags: ['pages'], diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index bf95ab36f..d241f585a 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -1,8 +1,8 @@ import ms from 'ms'; +import { Not } from 'typeorm'; +import { Pages, DriveFiles } from '@/models/index.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { Pages, DriveFiles } from '@/models/index.js'; -import { Not } from 'typeorm'; export const meta = { tags: ['pages'], diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 8d253c1f3..41595b47d 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -1,9 +1,9 @@ -import define from '../define.js'; +import { IsNull } from 'typeorm'; import { Users } from '@/models/index.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import * as Acct from '@/misc/acct.js'; import { User } from '@/models/entities/user.js'; -import { IsNull } from 'typeorm'; +import define from '../define.js'; export const meta = { tags: ['users'], diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts index cc602857d..c6a940c65 100644 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ b/packages/backend/src/server/api/endpoints/promo/read.ts @@ -1,8 +1,8 @@ +import { PromoReads } from '@/models/index.js'; +import { genId } from '@/misc/gen-id.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; import { getNote } from '../../common/getters.js'; -import { PromoReads } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts index 046337f04..511a6bbb5 100644 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts @@ -1,17 +1,21 @@ -import { publishMainStream } from '@/services/stream.js'; -import define from '../define.js'; import rndstr from 'rndstr'; -import config from '@/config/index.js'; import ms from 'ms'; +import { IsNull } from 'typeorm'; +import { publishMainStream } from '@/services/stream.js'; +import config from '@/config/index.js'; import { Users, UserProfiles, PasswordResetRequests } from '@/models/index.js'; import { sendEmail } from '@/services/send-email.js'; -import { ApiError } from '../error.js'; import { genId } from '@/misc/gen-id.js'; -import { IsNull } from 'typeorm'; +import { ApiError } from '../error.js'; +import define from '../define.js'; export const meta = { + tags: ['reset password'], + requireCredential: false, + description: 'Request a users password to be reset.', + limit: { duration: ms('1hour'), max: 3, diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index dbe64e9a1..140f96d57 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -1,10 +1,14 @@ +import { resetDb } from '@/db/postgre.js'; import define from '../define.js'; import { ApiError } from '../error.js'; -import { resetDb } from '@/db/postgre.js'; export const meta = { + tags: ['non-productive'], + requireCredential: false, + description: 'Only available when running with NODE_ENV=testing. Reset the database and flush Redis.', + errors: { }, diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index 7acc545c4..797169c2c 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -1,12 +1,16 @@ import bcrypt from 'bcryptjs'; import { publishMainStream } from '@/services/stream.js'; -import define from '../define.js'; import { Users, UserProfiles, PasswordResetRequests } from '@/models/index.js'; +import define from '../define.js'; import { ApiError } from '../error.js'; export const meta = { + tags: ['reset password'], + requireCredential: false, + description: 'Complete the password reset that was previously requested.', + errors: { }, diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts index f8a1ee29d..cc94f8bf2 100644 --- a/packages/backend/src/server/api/endpoints/stats.ts +++ b/packages/backend/src/server/api/endpoints/stats.ts @@ -1,5 +1,5 @@ -import define from '../define.js'; import { Instances, NoteReactions, Notes, Users } from '@/models/index.js'; +import define from '../define.js'; import { } from '@/services/chart/index.js'; import { IsNull } from 'typeorm'; diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index a48973a0d..437f8874f 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -1,13 +1,15 @@ -import define from '../../define.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { genId } from '@/misc/gen-id.js'; import { SwSubscriptions } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['account'], requireCredential: true, + description: 'Register to receive push notifications.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts index 9748f2a22..c19e06b87 100644 --- a/packages/backend/src/server/api/endpoints/sw/unregister.ts +++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts @@ -1,10 +1,12 @@ -import define from '../../define.js'; import { SwSubscriptions } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['account'], requireCredential: true, + + description: 'Unregister from receiving push notifications.', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts index 256da1a66..9949237a7 100644 --- a/packages/backend/src/server/api/endpoints/test.ts +++ b/packages/backend/src/server/api/endpoints/test.ts @@ -1,6 +1,10 @@ import define from '../define.js'; export const meta = { + tags: ['non-productive'], + + description: 'Endpoint for testing input validation.', + requireCredential: false, } as const; diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts index 04b754f4a..3e41aeaed 100644 --- a/packages/backend/src/server/api/endpoints/username/available.ts +++ b/packages/backend/src/server/api/endpoints/username/available.ts @@ -1,6 +1,6 @@ -import define from '../../define.js'; -import { Users, UsedUsernames } from '@/models/index.js'; import { IsNull } from 'typeorm'; +import { Users, UsedUsernames } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['users'], diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index 10527d15c..2377faebd 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -1,5 +1,5 @@ -import define from '../define.js'; import { Users } from '@/models/index.js'; +import define from '../define.js'; import { generateMutedUserQueryForUsers } from '../common/generate-muted-user-query.js'; import { generateBlockQueryForUsers } from '../common/generate-block-query.js'; @@ -25,8 +25,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] }, - state: { type: 'string', enum: ['all', 'admin', 'moderator', 'adminOrModerator', 'alive'], default: "all" }, - origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" }, + state: { type: 'string', enum: ['all', 'admin', 'moderator', 'adminOrModerator', 'alive'], default: 'all' }, + origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' }, }, required: [], } as const; diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts index 424c59474..09fdf27c2 100644 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ b/packages/backend/src/server/api/endpoints/users/clips.ts @@ -1,9 +1,21 @@ -import define from '../../define.js'; import { Clips } from '@/models/index.js'; +import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; export const meta = { 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; export const paramDef = { @@ -20,7 +32,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const query = makePaginationQuery(Clips.createQueryBuilder('clip'), ps.sinceId, ps.untilId) - .andWhere(`clip.userId = :userId`, { userId: ps.userId }) + .andWhere('clip.userId = :userId', { userId: ps.userId }) .andWhere('clip.isPublic = true'); const clips = await query diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 26b1f20df..7f9f98076 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -1,15 +1,17 @@ +import { IsNull } from 'typeorm'; +import { Users, Followings, UserProfiles } from '@/models/index.js'; +import { toPunyNullable } from '@/misc/convert-host.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { toPunyNullable } from '@/misc/convert-host.js'; -import { IsNull } from 'typeorm'; export const meta = { tags: ['users'], requireCredential: false, + description: 'Show everyone that follows this user.', + res: { type: 'array', optional: false, nullable: false, @@ -94,7 +96,7 @@ export default define(meta, paramDef, async (ps, me) => { } const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) - .andWhere(`following.followeeId = :userId`, { userId: user.id }) + .andWhere('following.followeeId = :userId', { userId: user.id }) .innerJoinAndSelect('following.follower', 'follower'); const followings = await query diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 42cf5216e..0aaa810f7 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -1,15 +1,17 @@ +import { IsNull } from 'typeorm'; +import { Users, Followings, UserProfiles } from '@/models/index.js'; +import { toPunyNullable } from '@/misc/convert-host.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { toPunyNullable } from '@/misc/convert-host.js'; -import { IsNull } from 'typeorm'; export const meta = { tags: ['users'], requireCredential: false, + description: 'Show everyone that this user is following.', + res: { type: 'array', optional: false, nullable: false, @@ -94,7 +96,7 @@ export default define(meta, paramDef, async (ps, me) => { } const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) - .andWhere(`following.followerId = :userId`, { userId: user.id }) + .andWhere('following.followerId = :userId', { userId: user.id }) .innerJoinAndSelect('following.followee', 'followee'); const followings = await query diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts index d7c435256..35bf2df59 100644 --- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts @@ -4,6 +4,18 @@ import { makePaginationQuery } from '../../../common/make-pagination-query.js'; export const meta = { 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; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts index 73cadc0df..56965d306 100644 --- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -1,15 +1,17 @@ -import define from '../../define.js'; +import { Not, In, IsNull } from 'typeorm'; import { maximum } from '@/prelude/array.js'; +import { Notes, Users } from '@/models/index.js'; +import define from '../../define.js'; import { ApiError } from '../../error.js'; import { getUser } from '../../common/getters.js'; -import { Not, In, IsNull } from 'typeorm'; -import { Notes, Users } from '@/models/index.js'; export const meta = { tags: ['users'], requireCredential: false, + description: 'Get a list of other users that the specified user frequently replies to.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/groups/create.ts b/packages/backend/src/server/api/endpoints/users/groups/create.ts index fc775d7cc..4a6362a3c 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/create.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/create.ts @@ -1,8 +1,8 @@ -import define from '../../../define.js'; import { UserGroups, UserGroupJoinings } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { UserGroup } from '@/models/entities/user-group.js'; import { UserGroupJoining } from '@/models/entities/user-group-joining.js'; +import define from '../../../define.js'; export const meta = { tags: ['groups'], @@ -11,6 +11,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Create a new group.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/groups/delete.ts b/packages/backend/src/server/api/endpoints/users/groups/delete.ts index f68006994..2ff1f9aec 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/delete.ts @@ -1,6 +1,6 @@ +import { UserGroups } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { UserGroups } from '@/models/index.js'; export const meta = { tags: ['groups'], @@ -9,6 +9,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Delete an existing group.', + errors: { noSuchGroup: { message: 'No such group.', diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts index 75c1acc30..220fff5f3 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts @@ -1,8 +1,8 @@ -import define from '../../../../define.js'; -import { ApiError } from '../../../../error.js'; import { UserGroupJoinings, UserGroupInvitations } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { UserGroupJoining } from '@/models/entities/user-group-joining.js'; +import { ApiError } from '../../../../error.js'; +import define from '../../../../define.js'; export const meta = { tags: ['groups', 'users'], @@ -11,6 +11,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Join a group the authenticated user has been invited to.', + errors: { noSuchInvitation: { message: 'No such invitation.', diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts index 46bc780ab..8d1d3db73 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts @@ -1,6 +1,6 @@ +import { UserGroupInvitations } from '@/models/index.js'; import define from '../../../../define.js'; import { ApiError } from '../../../../error.js'; -import { UserGroupInvitations } from '@/models/index.js'; export const meta = { tags: ['groups', 'users'], @@ -9,6 +9,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Delete an existing group invitation for the authenticated user without joining the group.', + errors: { noSuchInvitation: { message: 'No such invitation.', diff --git a/packages/backend/src/server/api/endpoints/users/groups/invite.ts b/packages/backend/src/server/api/endpoints/users/groups/invite.ts index 30a5beb1d..1a8d320f3 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invite.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invite.ts @@ -1,10 +1,10 @@ -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; -import { getUser } from '../../../common/getters.js'; import { UserGroups, UserGroupJoinings, UserGroupInvitations } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { UserGroupInvitation } from '@/models/entities/user-group-invitation.js'; import { createNotification } from '@/services/create-notification.js'; +import { getUser } from '../../../common/getters.js'; +import { ApiError } from '../../../error.js'; +import define from '../../../define.js'; export const meta = { tags: ['groups', 'users'], @@ -13,6 +13,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Invite a user to an existing group.', + errors: { noSuchGroup: { message: 'No such group.', diff --git a/packages/backend/src/server/api/endpoints/users/groups/joined.ts b/packages/backend/src/server/api/endpoints/users/groups/joined.ts index 77dc59d3e..16c6e544e 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/joined.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/joined.ts @@ -1,6 +1,6 @@ -import define from '../../../define.js'; -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; import { Not, In } from 'typeorm'; +import { UserGroups, UserGroupJoinings } from '@/models/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['groups', 'account'], @@ -9,6 +9,8 @@ export const meta = { kind: 'read:user-groups', + description: 'List the groups that the authenticated user is a member of.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/groups/leave.ts b/packages/backend/src/server/api/endpoints/users/groups/leave.ts index 33abd5439..83dc757db 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/leave.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/leave.ts @@ -1,6 +1,6 @@ +import { UserGroups, UserGroupJoinings } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; export const meta = { tags: ['groups', 'users'], @@ -9,6 +9,8 @@ export const meta = { 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: { noSuchGroup: { message: 'No such group.', diff --git a/packages/backend/src/server/api/endpoints/users/groups/owned.ts b/packages/backend/src/server/api/endpoints/users/groups/owned.ts index b1289e601..d77cf1a52 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/owned.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/owned.ts @@ -1,5 +1,5 @@ -import define from '../../../define.js'; import { UserGroups } from '@/models/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['groups', 'account'], @@ -8,6 +8,8 @@ export const meta = { kind: 'read:user-groups', + description: 'List the groups that the authenticated user is the owner of.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/groups/pull.ts b/packages/backend/src/server/api/endpoints/users/groups/pull.ts index b31990b2e..ba67a1e5c 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/pull.ts @@ -1,7 +1,7 @@ +import { UserGroups, UserGroupJoinings } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; import { getUser } from '../../../common/getters.js'; -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; export const meta = { tags: ['groups', 'users'], @@ -10,6 +10,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Removes a specified user from a group. The owner can not be removed.', + errors: { noSuchGroup: { message: 'No such group.', diff --git a/packages/backend/src/server/api/endpoints/users/groups/show.ts b/packages/backend/src/server/api/endpoints/users/groups/show.ts index 3ffb0f5ba..21e3d9da2 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/show.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/show.ts @@ -1,6 +1,6 @@ +import { UserGroups, UserGroupJoinings } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; export const meta = { tags: ['groups', 'account'], @@ -9,6 +9,8 @@ export const meta = { kind: 'read:user-groups', + description: 'Show the properties of a group.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts index 41ceee3b2..6456e70dd 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts @@ -1,7 +1,7 @@ +import { UserGroups, UserGroupJoinings } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; import { getUser } from '../../../common/getters.js'; -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; export const meta = { tags: ['groups', 'users'], @@ -10,6 +10,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Transfer ownership of a group from the authenticated user to another user.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/groups/update.ts b/packages/backend/src/server/api/endpoints/users/groups/update.ts index 1016aa892..0a96165fc 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/update.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/update.ts @@ -1,6 +1,6 @@ +import { UserGroups } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { UserGroups } from '@/models/index.js'; export const meta = { tags: ['groups'], @@ -9,6 +9,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Update the properties of a group.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts index d5260256d..783e63f5d 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -1,7 +1,7 @@ -import define from '../../../define.js'; import { UserLists } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { UserList } from '@/models/entities/user-list.js'; +import define from '../../../define.js'; export const meta = { tags: ['lists'], @@ -10,6 +10,8 @@ export const meta = { kind: 'write:account', + description: 'Create a new list of users.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts index b7ad96eef..5a7613c98 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/delete.ts @@ -1,6 +1,6 @@ +import { UserLists } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { UserLists } from '@/models/index.js'; export const meta = { tags: ['lists'], @@ -9,6 +9,8 @@ export const meta = { kind: 'write:account', + description: 'Delete an existing list of users.', + errors: { noSuchList: { message: 'No such list.', diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts index 78311292c..889052fa3 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -1,5 +1,5 @@ -import define from '../../../define.js'; import { UserLists } from '@/models/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['lists', 'account'], @@ -8,6 +8,8 @@ export const meta = { kind: 'read:account', + description: 'Show all lists that the authenticated user has created.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts index 76863f07d..d3d1d6555 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -1,8 +1,8 @@ import { publishUserListStream } from '@/services/stream.js'; +import { UserLists, UserListJoinings, Users } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; import { getUser } from '../../../common/getters.js'; -import { UserLists, UserListJoinings, Users } from '@/models/index.js'; export const meta = { tags: ['lists', 'users'], @@ -11,6 +11,8 @@ export const meta = { kind: 'write:account', + description: 'Remove a user from a list.', + errors: { noSuchList: { message: 'No such list.', diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 260665c63..12b7b8634 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -1,8 +1,8 @@ +import { pushUserToUserList } from '@/services/user-list/push.js'; +import { UserLists, UserListJoinings, Blockings } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; import { getUser } from '../../../common/getters.js'; -import { pushUserToUserList } from '@/services/user-list/push.js'; -import { UserLists, UserListJoinings, Blockings } from '@/models/index.js'; export const meta = { tags: ['lists', 'users'], @@ -11,6 +11,8 @@ export const meta = { kind: 'write:account', + description: 'Add a user to an existing list.', + errors: { noSuchList: { message: 'No such list.', diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index 5f51980e9..fd0612f73 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -1,6 +1,6 @@ +import { UserLists } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { UserLists } from '@/models/index.js'; export const meta = { tags: ['lists', 'account'], @@ -9,6 +9,8 @@ export const meta = { kind: 'read:account', + description: 'Show the properties of a list.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index 52353a14c..65e708b95 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -1,6 +1,6 @@ +import { UserLists } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { UserLists } from '@/models/index.js'; export const meta = { tags: ['lists'], @@ -9,6 +9,8 @@ export const meta = { kind: 'write:account', + description: 'Update the properties of a list.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 16318d222..aec5c0ea9 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -1,17 +1,19 @@ +import { Brackets } from 'typeorm'; +import { Notes } from '@/models/index.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; import { getUser } from '../../common/getters.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { Notes } from '@/models/index.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { Brackets } from 'typeorm'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js'; export const meta = { tags: ['users', 'notes'], + description: 'Show all notes that this user created.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts index b8b3e8192..b1d28af84 100644 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ b/packages/backend/src/server/api/endpoints/users/pages.ts @@ -1,9 +1,21 @@ -import define from '../../define.js'; import { Pages } from '@/models/index.js'; +import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; export const meta = { 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; export const paramDef = { @@ -20,7 +32,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId) - .andWhere(`page.userId = :userId`, { userId: ps.userId }) + .andWhere('page.userId = :userId', { userId: ps.userId }) .andWhere('page.visibility = \'public\''); const pages = await query diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index c2d199434..9668bd21b 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { NoteReactions, UserProfiles } from '@/models/index.js'; +import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { ApiError } from '../../error.js'; @@ -9,6 +9,8 @@ export const meta = { requireCredential: false, + description: 'Show all reactions this user made.', + res: { type: 'array', optional: false, nullable: false, @@ -50,8 +52,8 @@ export default define(meta, paramDef, async (ps, me) => { } const query = makePaginationQuery(NoteReactions.createQueryBuilder('reaction'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere(`reaction.userId = :userId`, { userId: ps.userId }) + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('reaction.userId = :userId', { userId: ps.userId }) .leftJoinAndSelect('reaction.note', 'note'); generateVisibilityQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index a8f18de52..e7654e171 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -1,6 +1,6 @@ import ms from 'ms'; -import define from '../../define.js'; import { Users, Followings } from '@/models/index.js'; +import define from '../../define.js'; import { generateMutedUserQueryForUsers } from '../../common/generate-muted-user-query.js'; import { generateBlockedUserQuery, generateBlockQueryForUsers } from '../../common/generate-block-query.js'; @@ -11,6 +11,8 @@ export const meta = { kind: 'read:account', + description: 'Show users that the authenticated user might be interested to follow.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index c6262122d..233a6a90b 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -1,11 +1,13 @@ -import define from '../../define.js'; import { Users } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['users'], requireCredential: true, + description: 'Show the different kinds of relations between the authenticated user and the specified user(s).', + res: { optional: false, nullable: false, oneOf: [ diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 0be385dbb..a9987eafa 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -1,18 +1,20 @@ import * as sanitizeHtml from 'sanitize-html'; -import define from '../../define.js'; import { publishAdminStream } from '@/services/stream.js'; -import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; import { AbuseUserReports, Users } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { sendEmail } from '@/services/send-email.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; +import { getUser } from '../../common/getters.js'; +import { ApiError } from '../../error.js'; +import define from '../../define.js'; export const meta = { tags: ['users'], requireCredential: true, + description: 'File a report.', + errors: { noSuchUser: { message: 'No such user.', diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index f74d80e2a..6e5bc46bb 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -1,14 +1,16 @@ -import define from '../../define.js'; -import { Followings, Users } from '@/models/index.js'; import { Brackets } from 'typeorm'; +import { Followings, Users } from '@/models/index.js'; import { USER_ACTIVE_THRESHOLD } from '@/const.js'; import { User } from '@/models/entities/user.js'; +import define from '../../define.js'; export const meta = { tags: ['users'], requireCredential: false, + description: 'Search for a user by username and/or host.', + res: { type: 'array', optional: false, nullable: false, @@ -65,7 +67,7 @@ export default define(meta, paramDef, async (ps, me) => { const query = Users.createQueryBuilder('user') .where(`user.id IN (${ followingQuery.getQuery() })`) - .andWhere(`user.id != :meId`, { meId: me.id }) + .andWhere('user.id != :meId', { meId: me.id }) .andWhere('user.isSuspended = FALSE') .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) .andWhere(new Brackets(qb => { qb @@ -83,7 +85,7 @@ export default define(meta, paramDef, async (ps, me) => { if (users.length < ps.limit) { const otherQuery = await Users.createQueryBuilder('user') .where(`user.id NOT IN (${ followingQuery.getQuery() })`) - .andWhere(`user.id != :meId`, { meId: me.id }) + .andWhere('user.id != :meId', { meId: me.id }) .andWhere('user.isSuspended = FALSE') .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) .andWhere('user.updatedAt IS NOT NULL'); diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index f93d4f718..01729de66 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -1,13 +1,15 @@ -import define from '../../define.js'; +import { Brackets } from 'typeorm'; import { UserProfiles, Users } from '@/models/index.js'; import { User } from '@/models/entities/user.js'; -import { Brackets } from 'typeorm'; +import define from '../../define.js'; export const meta = { tags: ['users'], requireCredential: false, + description: 'Search for users.', + res: { type: 'array', optional: false, nullable: false, @@ -25,7 +27,7 @@ export const paramDef = { query: { type: 'string' }, offset: { type: 'integer', default: 0 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - origin: { type: 'string', enum: ['local', 'remote', 'combined'], default: "combined" }, + origin: { type: 'string', enum: ['local', 'remote', 'combined'], default: 'combined' }, detail: { type: 'boolean', default: true }, }, required: ['query'], @@ -111,7 +113,7 @@ export default define(meta, paramDef, async (ps, me) => { .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') .take(ps.limit) .skip(ps.offset) - .getMany() + .getMany(), ); } } diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 183ff1b8b..846d83b49 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -1,16 +1,18 @@ +import { FindOptionsWhere, In, IsNull } from 'typeorm'; import { resolveUser } from '@/remote/resolve-user.js'; +import { Users } from '@/models/index.js'; +import { User } from '@/models/entities/user.js'; import define from '../../define.js'; import { apiLogger } from '../../logger.js'; import { ApiError } from '../../error.js'; -import { Users } from '@/models/index.js'; -import { FindOptionsWhere, In, IsNull } from 'typeorm'; -import { User } from '@/models/entities/user.js'; export const meta = { tags: ['users'], requireCredential: false, + description: 'Show the properties of a user.', + res: { optional: false, nullable: false, oneOf: [ diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts index d138019a7..47f322ee9 100644 --- a/packages/backend/src/server/api/endpoints/users/stats.ts +++ b/packages/backend/src/server/api/endpoints/users/stats.ts @@ -1,12 +1,15 @@ +import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index.js'; +import { awaitAll } from '@/prelude/await-all.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index.js'; export const meta = { tags: ['users'], requireCredential: false, + description: 'Show statistics about a user.', + errors: { noSuchUser: { message: 'No such user.', @@ -14,6 +17,94 @@ export const meta = { 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; export const paramDef = { @@ -31,109 +122,72 @@ export default define(meta, paramDef, async (ps, me) => { throw new ApiError(meta.errors.noSuchUser); } - const [ - notesCount, - repliesCount, - renotesCount, - repliedCount, - renotedCount, - pollVotesCount, - pollVotedCount, - localFollowingCount, - remoteFollowingCount, - localFollowersCount, - remoteFollowersCount, - sentReactionsCount, - receivedReactionsCount, - noteFavoritesCount, - pageLikesCount, - pageLikedCount, - driveFilesCount, - driveUsage, - ] = await Promise.all([ - Notes.createQueryBuilder('note') + const result = await awaitAll({ + notesCount: Notes.createQueryBuilder('note') .where('note.userId = :userId', { userId: user.id }) .getCount(), - Notes.createQueryBuilder('note') + repliesCount: Notes.createQueryBuilder('note') .where('note.userId = :userId', { userId: user.id }) .andWhere('note.replyId IS NOT NULL') .getCount(), - Notes.createQueryBuilder('note') + renotesCount: Notes.createQueryBuilder('note') .where('note.userId = :userId', { userId: user.id }) .andWhere('note.renoteId IS NOT NULL') .getCount(), - Notes.createQueryBuilder('note') + repliedCount: Notes.createQueryBuilder('note') .where('note.replyUserId = :userId', { userId: user.id }) .getCount(), - Notes.createQueryBuilder('note') + renotedCount: Notes.createQueryBuilder('note') .where('note.renoteUserId = :userId', { userId: user.id }) .getCount(), - PollVotes.createQueryBuilder('vote') + pollVotesCount: PollVotes.createQueryBuilder('vote') .where('vote.userId = :userId', { userId: user.id }) .getCount(), - PollVotes.createQueryBuilder('vote') + pollVotedCount: PollVotes.createQueryBuilder('vote') .innerJoin('vote.note', 'note') .where('note.userId = :userId', { userId: user.id }) .getCount(), - Followings.createQueryBuilder('following') + localFollowingCount: Followings.createQueryBuilder('following') .where('following.followerId = :userId', { userId: user.id }) .andWhere('following.followeeHost IS NULL') .getCount(), - Followings.createQueryBuilder('following') + remoteFollowingCount: Followings.createQueryBuilder('following') .where('following.followerId = :userId', { userId: user.id }) .andWhere('following.followeeHost IS NOT NULL') .getCount(), - Followings.createQueryBuilder('following') + localFollowersCount: Followings.createQueryBuilder('following') .where('following.followeeId = :userId', { userId: user.id }) .andWhere('following.followerHost IS NULL') .getCount(), - Followings.createQueryBuilder('following') + remoteFollowersCount: Followings.createQueryBuilder('following') .where('following.followeeId = :userId', { userId: user.id }) .andWhere('following.followerHost IS NOT NULL') .getCount(), - NoteReactions.createQueryBuilder('reaction') + sentReactionsCount: NoteReactions.createQueryBuilder('reaction') .where('reaction.userId = :userId', { userId: user.id }) .getCount(), - NoteReactions.createQueryBuilder('reaction') + receivedReactionsCount: NoteReactions.createQueryBuilder('reaction') .innerJoin('reaction.note', 'note') .where('note.userId = :userId', { userId: user.id }) .getCount(), - NoteFavorites.createQueryBuilder('favorite') + noteFavoritesCount: NoteFavorites.createQueryBuilder('favorite') .where('favorite.userId = :userId', { userId: user.id }) .getCount(), - PageLikes.createQueryBuilder('like') + pageLikesCount: PageLikes.createQueryBuilder('like') .where('like.userId = :userId', { userId: user.id }) .getCount(), - PageLikes.createQueryBuilder('like') + pageLikedCount: PageLikes.createQueryBuilder('like') .innerJoin('like.page', 'page') .where('page.userId = :userId', { userId: user.id }) .getCount(), - DriveFiles.createQueryBuilder('file') + driveFilesCount: DriveFiles.createQueryBuilder('file') .where('file.userId = :userId', { userId: user.id }) .getCount(), - DriveFiles.calcDriveUsageOf(user), - ]); + driveUsage: DriveFiles.calcDriveUsageOf(user), + }); - return { - notesCount, - repliesCount, - renotesCount, - repliedCount, - renotedCount, - pollVotesCount, - pollVotedCount, - localFollowingCount, - remoteFollowingCount, - localFollowersCount, - remoteFollowersCount, - followingCount: localFollowingCount + remoteFollowingCount, - followersCount: localFollowersCount + remoteFollowersCount, - sentReactionsCount, - receivedReactionsCount, - noteFavoritesCount, - pageLikesCount, - pageLikedCount, - driveFilesCount, - driveUsage, - }; + result.followingCount = result.localFollowingCount + result.remoteFollowingCount; + result.followersCount = result.localFollowersCount + result.remoteFollowersCount; + + return result; }); diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts index e74db8466..6ca3ebf18 100644 --- a/packages/backend/src/server/api/limiter.ts +++ b/packages/backend/src/server/api/limiter.ts @@ -1,25 +1,17 @@ import Limiter from 'ratelimiter'; -import { redisClient } from '../../db/redis.js'; -import { IEndpoint } from './endpoints.js'; -import * as Acct from '@/misc/acct.js'; import { CacheableLocalUser, User } from '@/models/entities/user.js'; import Logger from '@/services/logger.js'; +import { redisClient } from '../../db/redis.js'; +import { IEndpointMeta } from './endpoints.js'; const logger = new Logger('limiter'); -export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable } }, user: CacheableLocalUser) => new Promise((ok, reject) => { - const limitation = endpoint.meta.limit; - - const key = Object.prototype.hasOwnProperty.call(limitation, 'key') - ? limitation.key - : endpoint.name; - - const hasShortTermLimit = - Object.prototype.hasOwnProperty.call(limitation, 'minInterval'); +export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string) => new Promise((ok, reject) => { + const hasShortTermLimit = typeof limitation.minInterval === 'number'; const hasLongTermLimit = - Object.prototype.hasOwnProperty.call(limitation, 'duration') && - Object.prototype.hasOwnProperty.call(limitation, 'max'); + typeof limitation.duration === 'number' && + typeof limitation.max === 'number'; if (hasShortTermLimit) { min(); @@ -32,7 +24,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable { ctx.set('Access-Control-Allow-Origin', config.url); ctx.set('Access-Control-Allow-Credentials', 'true'); const body = ctx.request.body as any; - - const instance = await fetchMeta(true); - const username = body['username']; const password = body['password']; const token = body['token']; @@ -29,6 +26,21 @@ export default async (ctx: Koa.Context) => { ctx.body = { error }; } + try { + // not more than 1 attempt per second and not more than 10 attempts per hour + await limiter({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(ctx.ip)); + } catch (err) { + ctx.status = 429; + ctx.body = { + error: { + message: 'Too many failed attempts to sign in. Try again later.', + code: 'TOO_MANY_AUTHENTICATION_FAILURES', + id: '22d05606-fbcf-421a-a2db-b32610dcfd1b', + }, + }; + return; + } + if (typeof username !== 'string') { ctx.status = 400; return; @@ -84,18 +96,6 @@ export default async (ctx: Koa.Context) => { } if (!profile.twoFactorEnabled) { - if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { - await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => { - ctx.throw(400, e); - }); - } - - if (instance.enableRecaptcha && instance.recaptchaSecretKey) { - await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => { - ctx.throw(400, e); - }); - } - if (same) { signin(ctx, user); return; @@ -172,7 +172,7 @@ export default async (ctx: Koa.Context) => { body.credentialId .replace(/-/g, '+') .replace(/_/g, '/'), - 'base64', + 'base64' ).toString('hex'), }); diff --git a/packages/backend/src/server/proxy/proxy-media.ts b/packages/backend/src/server/proxy/proxy-media.ts index 48887bf12..ca036e8fd 100644 --- a/packages/backend/src/server/proxy/proxy-media.ts +++ b/packages/backend/src/server/proxy/proxy-media.ts @@ -1,13 +1,16 @@ import * as fs from 'node:fs'; import Koa from 'koa'; -import { serverLogger } from '../index.js'; +import sharp from 'sharp'; import { IImage, convertToWebp } from '@/services/drive/image-processor.js'; import { createTemp } from '@/misc/create-temp.js'; import { downloadUrl } from '@/misc/download-url.js'; import { detectType } from '@/misc/get-file-info.js'; import { StatusError } from '@/misc/fetch.js'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; +import { serverLogger } from '../index.js'; +import { isMimeImage } from '@/misc/is-mime-image.js'; +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export async function proxyMedia(ctx: Koa.Context) { const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url; @@ -23,14 +26,50 @@ export async function proxyMedia(ctx: Koa.Context) { await downloadUrl(url, path); const { mime, ext } = await detectType(path); + const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image'); let image: IImage; - if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'].includes(mime)) { + if ('static' in ctx.query && isConvertibleImage) { image = await convertToWebp(path, 498, 280); - } else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/svg+xml'].includes(mime)) { + } else if ('preview' in ctx.query && isConvertibleImage) { image = await convertToWebp(path, 200, 200); - } else if (['image/svg+xml'].includes(mime)) { + } else if ('badge' in ctx.query) { + if (!isConvertibleImage) { + // 画像でないなら404でお茶を濁す + throw new StatusError('Unexpected mime', 404); + } + + const mask = sharp(path) + .resize(96, 96, { + fit: 'inside', + withoutEnlargement: false, + }) + .greyscale() + .normalise() + .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast + .flatten({ background: '#000' }) + .toColorspace('b-w'); + + const stats = await mask.clone().stats(); + + if (stats.entropy < 0.1) { + // エントロピーがあまりない場合は404にする + throw new StatusError('Skip to provide badge', 404); + } + + const data = sharp({ + create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } }, + }) + .pipelineColorspace('b-w') + .boolean(await mask.png().toBuffer(), 'eor'); + + image = { + data: await data.png().toBuffer(), + ext: 'png', + type: 'image/png', + }; + } else if (mime === 'image/svg+xml') { image = await convertToWebp(path, 2048, 2048, 1); } else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) { throw new StatusError('Rejected type', 403, 'Rejected type'); @@ -48,7 +87,7 @@ export async function proxyMedia(ctx: Koa.Context) { } catch (e) { serverLogger.error(`${e}`); - if (e instanceof StatusError && e.isClientError) { + if (e instanceof StatusError && (e.statusCode === 302 || e.isClientError)) { ctx.status = e.statusCode; } else { ctx.status = 500; diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index a9ee0df4f..94329e11c 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -54,14 +54,10 @@ //#endregion //#region Script - const salt = localStorage.getItem('salt') - ? `?salt=${localStorage.getItem('salt')}` - : ''; - - import(`/assets/${CLIENT_ENTRY}${salt}`) - .catch(async () => { + import(`/assets/${CLIENT_ENTRY}`) + .catch(async e => { await checkUpdate(); - renderError('APP_FETCH_FAILED'); + renderError('APP_FETCH_FAILED', JSON.stringify(e)); }) //#endregion @@ -142,9 +138,6 @@ // eslint-disable-next-line no-inner-declarations function refresh() { - // Random - localStorage.setItem('salt', Math.random().toString().substr(2, 8)); - // Clear cache (service worker) try { navigator.serviceWorker.controller.postMessage('clear'); diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 9e31f2389..be95becb6 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -11,6 +11,7 @@ import Router from '@koa/router'; import send from 'koa-send'; import favicon from 'koa-favicon'; import views from 'koa-views'; +import sharp from 'sharp'; import { createBullBoard } from '@bull-board/api'; import { BullAdapter } from '@bull-board/api/bullAdapter.js'; import { KoaAdapter } from '@bull-board/koa'; @@ -74,9 +75,9 @@ app.use(views(_dirname + '/views', { extension: 'pug', options: { version: config.version, - clientEntry: () => process.env.NODE_ENV === 'production' ? + getClientEntry: () => process.env.NODE_ENV === 'production' ? config.clientEntry : - JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'].file.replace(/^_client_dist_\//, ''), + JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'], config, }, })); @@ -140,6 +141,49 @@ router.get('/twemoji/(.*)', async ctx => { }); }); +router.get('/twemoji-badge/(.*)', async ctx => { + const path = ctx.path.replace('/twemoji-badge/', ''); + + if (!path.match(/^[0-9a-f-]+\.png$/)) { + ctx.status = 404; + return; + } + + const mask = await sharp( + `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/${path.replace('.png', '')}.svg`, + { density: 1000 }, + ) + .resize(488, 488) + .greyscale() + .normalise() + .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast + .flatten({ background: '#000' }) + .extend({ + top: 12, + bottom: 12, + left: 12, + right: 12, + background: '#000', + }) + .toColorspace('b-w') + .png() + .toBuffer(); + + const buffer = await sharp({ + create: { width: 512, height: 512, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } }, + }) + .pipelineColorspace('b-w') + .boolean(mask, 'eor') + .resize(96, 96) + .png() + .toBuffer(); + + ctx.set('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); + ctx.set('Cache-Control', 'max-age=2592000'); + ctx.set('Content-Type', 'image/png'); + ctx.body = buffer; +}); + // ServiceWorker router.get(`/sw.js`, async ctx => { await send(ctx as any, `/sw.js`, { @@ -247,7 +291,7 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => { icon: meta.iconUrl, themeColor: meta.themeColor, }); - ctx.set('Cache-Control', 'public, max-age=30'); + ctx.set('Cache-Control', 'public, max-age=15'); } else { // リモートユーザーなので // モデレータがAPI経由で参照可能にするために404にはしない @@ -292,7 +336,7 @@ router.get('/notes/:note', async (ctx, next) => { themeColor: meta.themeColor, }); - ctx.set('Cache-Control', 'public, max-age=180'); + ctx.set('Cache-Control', 'public, max-age=15'); return; } @@ -329,7 +373,7 @@ router.get('/@:user/pages/:page', async (ctx, next) => { }); if (['public'].includes(page.visibility)) { - ctx.set('Cache-Control', 'public, max-age=180'); + ctx.set('Cache-Control', 'public, max-age=15'); } else { ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); } @@ -360,7 +404,7 @@ router.get('/clips/:clip', async (ctx, next) => { themeColor: meta.themeColor, }); - ctx.set('Cache-Control', 'public, max-age=180'); + ctx.set('Cache-Control', 'public, max-age=15'); return; } @@ -385,7 +429,7 @@ router.get('/gallery/:post', async (ctx, next) => { themeColor: meta.themeColor, }); - ctx.set('Cache-Control', 'public, max-age=180'); + ctx.set('Cache-Control', 'public, max-age=15'); return; } @@ -409,7 +453,7 @@ router.get('/channels/:channel', async (ctx, next) => { themeColor: meta.themeColor, }); - ctx.set('Cache-Control', 'public, max-age=180'); + ctx.set('Cache-Control', 'public, max-age=15'); return; } @@ -468,7 +512,7 @@ router.get('(.*)', async ctx => { icon: meta.iconUrl, themeColor: meta.themeColor, }); - ctx.set('Cache-Control', 'public, max-age=300'); + ctx.set('Cache-Control', 'public, max-age=15'); }); // Register router diff --git a/packages/backend/src/server/web/manifest.ts b/packages/backend/src/server/web/manifest.ts index 61d766006..ee568b807 100644 --- a/packages/backend/src/server/web/manifest.ts +++ b/packages/backend/src/server/web/manifest.ts @@ -3,7 +3,9 @@ import { fetchMeta } from '@/misc/fetch-meta.js'; import manifest from './manifest.json' assert { type: 'json' }; export const manifestHandler = async (ctx: Koa.Context) => { - const res = structuredClone(manifest); + // TODO + //const res = structuredClone(manifest); + const res = JSON.parse(JSON.stringify(manifest)); const instance = await fetchMeta(true); diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index a488e5117..5bb156f0f 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -1,17 +1,21 @@ block vars +block loadClientEntry + - const clientEntry = getClientEntry(); + doctype html -!= '\n' +// + - + _____ _ _ + | |_|___ ___| |_ ___ _ _ + | | | | |_ -|_ -| '_| -_| | | + |_|_|_|_|___|___|_,_|___|_ | + |___| + Thank you for using Misskey! + If you are reading this message... how about joining the development? + https://github.com/misskey-dev/misskey + html @@ -30,8 +34,14 @@ html link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') - link(rel='preload' href='/assets/fontawesome/css/all.css' as='style') link(rel='stylesheet' href='/assets/fontawesome/css/all.css') + link(rel='modulepreload' href=`/assets/${clientEntry.file}`) + + each href in clientEntry.css + link(rel='preload' href=`/assets/${href}` as='style') + + each href in clientEntry.css + link(rel='preload' href=`/assets/${href}` as='style') title block title @@ -52,7 +62,7 @@ html script. var VERSION = "#{version}"; - var CLIENT_ENTRY = "#{clientEntry()}"; + var CLIENT_ENTRY = "#{clientEntry.file}"; script include ../boot.js diff --git a/packages/backend/src/services/blocking/create.ts b/packages/backend/src/services/blocking/create.ts index b2be78b22..a2c61cca2 100644 --- a/packages/backend/src/services/blocking/create.ts +++ b/packages/backend/src/services/blocking/create.ts @@ -2,9 +2,10 @@ import { publishMainStream, publishUserEvent } from '@/services/stream.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderFollow from '@/remote/activitypub/renderer/follow.js'; import renderUndo from '@/remote/activitypub/renderer/undo.js'; -import renderBlock from '@/remote/activitypub/renderer/block.js'; +import { renderBlock } from '@/remote/activitypub/renderer/block.js'; import { deliver } from '@/queue/index.js'; import renderReject from '@/remote/activitypub/renderer/reject.js'; +import { Blocking } from '@/models/entities/blocking.js'; import { User } from '@/models/entities/user.js'; import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index.js'; import { perUserFollowingChart } from '@/services/chart/index.js'; @@ -22,15 +23,19 @@ export default async function(blocker: User, blockee: User) { removeFromList(blockee, blocker), ]); - await Blockings.insert({ + const blocking = { id: genId(), createdAt: new Date(), + blocker, blockerId: blocker.id, + blockee, blockeeId: blockee.id, - }); + } as Blocking; + + await Blockings.insert(blocking); if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { - const content = renderActivity(renderBlock(blocker, blockee)); + const content = renderActivity(renderBlock(blocking)); deliver(blocker, content, blockee.inbox); } } diff --git a/packages/backend/src/services/blocking/delete.ts b/packages/backend/src/services/blocking/delete.ts index d7b5ddd5f..cb16651bc 100644 --- a/packages/backend/src/services/blocking/delete.ts +++ b/packages/backend/src/services/blocking/delete.ts @@ -1,5 +1,5 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderBlock from '@/remote/activitypub/renderer/block.js'; +import { renderBlock } from '@/remote/activitypub/renderer/block.js'; import renderUndo from '@/remote/activitypub/renderer/undo.js'; import { deliver } from '@/queue/index.js'; import Logger from '../logger.js'; @@ -19,11 +19,16 @@ export default async function(blocker: CacheableUser, blockee: CacheableUser) { return; } + // Since we already have the blocker and blockee, we do not need to fetch + // them in the query above and can just manually insert them here. + blocking.blocker = blocker; + blocking.blockee = blockee; + Blockings.delete(blocking.id); // deliver if remote bloking if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { - const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker)); + const content = renderActivity(renderUndo(renderBlock(blocking), blocker)); deliver(blocker, content, blockee.inbox); } } diff --git a/packages/backend/src/services/chart/core.ts b/packages/backend/src/services/chart/core.ts index cf69e2194..2960bac8f 100644 --- a/packages/backend/src/services/chart/core.ts +++ b/packages/backend/src/services/chart/core.ts @@ -91,27 +91,20 @@ type ToJsonSchema = { }; export function getJsonSchema(schema: S): ToJsonSchema>> { - const object = {}; - for (const [k, v] of Object.entries(schema)) { - nestedProperty.set(object, k, null); - } + const jsonSchema = { + type: 'object', + properties: {} as Record, + required: [], + }; - function f(obj: Record>) { - const jsonSchema = { - type: 'object', - properties: {} as Record, - required: [], + for (const k in schema) { + jsonSchema.properties[k] = { + type: 'array', + items: { type: 'number' }, }; - for (const [k, v] of Object.entries(obj)) { - jsonSchema.properties[k] = v === null ? { - type: 'array', - items: { type: 'number' }, - } : f(v as Record>); - } - return jsonSchema; } - return f(object) as ToJsonSchema>>; + return jsonSchema as ToJsonSchema>>; } /** diff --git a/packages/backend/src/services/chart/entities.ts b/packages/backend/src/services/chart/entities.ts index 13e994cb6..a9eeabd63 100644 --- a/packages/backend/src/services/chart/entities.ts +++ b/packages/backend/src/services/chart/entities.ts @@ -11,6 +11,11 @@ import { entity as PerUserFollowingChart } from './charts/entities/per-user-foll import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js'; import { entity as ApRequestChart } from './charts/entities/ap-request.js'; +import { entity as TestChart } from './charts/entities/test.js'; +import { entity as TestGroupedChart } from './charts/entities/test-grouped.js'; +import { entity as TestUniqueChart } from './charts/entities/test-unique.js'; +import { entity as TestIntersectionChart } from './charts/entities/test-intersection.js'; + export const entities = [ FederationChart.hour, FederationChart.day, NotesChart.hour, NotesChart.day, @@ -24,4 +29,11 @@ export const entities = [ PerUserFollowingChart.hour, PerUserFollowingChart.day, PerUserDriveChart.hour, PerUserDriveChart.day, ApRequestChart.hour, ApRequestChart.day, + + ...(process.env.NODE_ENV === 'test' ? [ + TestChart.hour, TestChart.day, + TestGroupedChart.hour, TestGroupedChart.day, + TestUniqueChart.hour, TestUniqueChart.day, + TestIntersectionChart.hour, TestIntersectionChart.day, + ] : []), ]; diff --git a/packages/backend/src/services/drive/generate-video-thumbnail.ts b/packages/backend/src/services/drive/generate-video-thumbnail.ts index ca12ab8d3..6e6666481 100644 --- a/packages/backend/src/services/drive/generate-video-thumbnail.ts +++ b/packages/backend/src/services/drive/generate-video-thumbnail.ts @@ -1,12 +1,10 @@ import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { createTemp } from '@/misc/create-temp.js'; +import { createTempDir } from '@/misc/create-temp.js'; import { IImage, convertToJpeg } from './image-processor.js'; import FFmpeg from 'fluent-ffmpeg'; export async function GenerateVideoThumbnail(source: string): Promise { - const [file, cleanup] = await createTemp(); - const parsed = path.parse(file); + const [dir, cleanup] = await createTempDir(); try { await new Promise((res, rej) => { @@ -16,15 +14,15 @@ export async function GenerateVideoThumbnail(source: string): Promise { .on('end', res) .on('error', rej) .screenshot({ - folder: parsed.dir, - filename: parsed.base, + folder: dir, + filename: 'out.png', // must have .png extension count: 1, timestamps: ['5%'], }); }); // JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる) - return await convertToJpeg(498, 280); + return await convertToJpeg(`${dir}/out.png`, 498, 280); } finally { cleanup(); } diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index ceb5e8cc7..e2bf9d5b5 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -312,7 +312,8 @@ export default async (user: { id: User['id']; username: User['username']; host: endedPollNotificationQueue.add({ noteId: note.id, }, { - delay + delay, + removeOnComplete: true, }); } diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts index 08bf72cc2..6bc430443 100644 --- a/packages/backend/src/services/relay.ts +++ b/packages/backend/src/services/relay.ts @@ -1,4 +1,4 @@ -import { createSystemUser } from './create-system-user.js'; +import { IsNull } from 'typeorm'; import { renderFollowRelay } from '@/remote/activitypub/renderer/follow-relay.js'; import { renderActivity, attachLdSignature } from '@/remote/activitypub/renderer/index.js'; import renderUndo from '@/remote/activitypub/renderer/undo.js'; @@ -8,7 +8,7 @@ import { Users, Relays } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { Cache } from '@/misc/cache.js'; import { Relay } from '@/models/entities/relay.js'; -import { IsNull } from 'typeorm'; +import { createSystemUser } from './create-system-user.js'; const ACTOR_USERNAME = 'relay.actor' as const; @@ -88,7 +88,9 @@ export async function deliverToRelays(user: { id: User['id']; host: null; }, act })); if (relays.length === 0) return; - const copy = structuredClone(activity); + // TODO + //const copy = structuredClone(activity); + const copy = JSON.parse(JSON.stringify(activity)); if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; const signed = await attachLdSignature(copy, user); diff --git a/packages/backend/test/activitypub.ts b/packages/backend/test/activitypub.ts index 5d8b28ec7..f4ae27e5e 100644 --- a/packages/backend/test/activitypub.ts +++ b/packages/backend/test/activitypub.ts @@ -2,11 +2,13 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import rndstr from 'rndstr'; +import { initDb } from '../src/db/postgre.js'; import { initTestDb } from './utils.js'; describe('ActivityPub', () => { before(async () => { - await initTestDb(); + //await initTestDb(); + await initDb(); }); describe('Parse minimum object', () => { diff --git a/packages/backend/test/chart.ts b/packages/backend/test/chart.ts index 823e388a8..ac0844679 100644 --- a/packages/backend/test/chart.ts +++ b/packages/backend/test/chart.ts @@ -6,26 +6,17 @@ import TestChart from '../src/services/chart/charts/test.js'; import TestGroupedChart from '../src/services/chart/charts/test-grouped.js'; import TestUniqueChart from '../src/services/chart/charts/test-unique.js'; import TestIntersectionChart from '../src/services/chart/charts/test-intersection.js'; -import * as _TestChart from '../src/services/chart/charts/entities/test.js'; -import * as _TestGroupedChart from '../src/services/chart/charts/entities/test-grouped.js'; -import * as _TestUniqueChart from '../src/services/chart/charts/entities/test-unique.js'; -import * as _TestIntersectionChart from '../src/services/chart/charts/entities/test-intersection.js'; -import { async, initTestDb } from './utils.js'; +import { initDb } from '../src/db/postgre.js'; describe('Chart', () => { let testChart: TestChart; let testGroupedChart: TestGroupedChart; let testUniqueChart: TestUniqueChart; let testIntersectionChart: TestIntersectionChart; - let clock: lolex.Clock; + let clock: lolex.InstalledClock; - beforeEach(async(async () => { - await initTestDb(false, [ - _TestChart.entity.hour, _TestChart.entity.day, - _TestGroupedChart.entity.hour, _TestGroupedChart.entity.day, - _TestUniqueChart.entity.hour, _TestUniqueChart.entity.day, - _TestIntersectionChart.entity.hour, _TestIntersectionChart.entity.day, - ]); + beforeEach(async () => { + await initDb(true); testChart = new TestChart(); testGroupedChart = new TestGroupedChart(); @@ -34,14 +25,15 @@ describe('Chart', () => { clock = lolex.install({ now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)), + shouldClearNativeTimers: true, }); - })); + }); - afterEach(async(async () => { + afterEach(() => { clock.uninstall(); - })); + }); - it('Can updates', async(async () => { + it('Can updates', async () => { await testChart.increment(); await testChart.save(); @@ -63,9 +55,9 @@ describe('Chart', () => { total: [1, 0, 0], }, }); - })); + }); - it('Can updates (dec)', async(async () => { + it('Can updates (dec)', async () => { await testChart.decrement(); await testChart.save(); @@ -87,9 +79,9 @@ describe('Chart', () => { total: [-1, 0, 0], }, }); - })); + }); - it('Empty chart', async(async () => { + it('Empty chart', async () => { const chartHours = await testChart.getChart('hour', 3, null); const chartDays = await testChart.getChart('day', 3, null); @@ -108,9 +100,9 @@ describe('Chart', () => { total: [0, 0, 0], }, }); - })); + }); - it('Can updates at multiple times at same time', async(async () => { + it('Can updates at multiple times at same time', async () => { await testChart.increment(); await testChart.increment(); await testChart.increment(); @@ -134,9 +126,9 @@ describe('Chart', () => { total: [3, 0, 0], }, }); - })); + }); - it('複数回saveされてもデータの更新は一度だけ', async(async () => { + it('複数回saveされてもデータの更新は一度だけ', async () => { await testChart.increment(); await testChart.save(); await testChart.save(); @@ -160,9 +152,9 @@ describe('Chart', () => { total: [1, 0, 0], }, }); - })); + }); - it('Can updates at different times', async(async () => { + it('Can updates at different times', async () => { await testChart.increment(); await testChart.save(); @@ -189,11 +181,11 @@ describe('Chart', () => { total: [2, 0, 0], }, }); - })); + }); // 仕様上はこうなってほしいけど、実装は難しそうなのでskip /* - it('Can updates at different times without save', async(async () => { + it('Can updates at different times without save', async () => { await testChart.increment(); clock.tick('01:00:00'); @@ -219,10 +211,10 @@ describe('Chart', () => { total: [2, 0, 0] }, }); - })); + }); */ - it('Can padding', async(async () => { + it('Can padding', async () => { await testChart.increment(); await testChart.save(); @@ -249,10 +241,10 @@ describe('Chart', () => { total: [2, 0, 0], }, }); - })); + }); // 要求された範囲にログがひとつもない場合でもパディングできる - it('Can padding from past range', async(async () => { + it('Can padding from past range', async () => { await testChart.increment(); await testChart.save(); @@ -276,11 +268,11 @@ describe('Chart', () => { total: [1, 0, 0], }, }); - })); + }); // 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる // Issue #3190 - it('Can padding from past range 2', async(async () => { + it('Can padding from past range 2', async () => { await testChart.increment(); await testChart.save(); @@ -307,9 +299,9 @@ describe('Chart', () => { total: [2, 0, 0], }, }); - })); + }); - it('Can specify offset', async(async () => { + it('Can specify offset', async () => { await testChart.increment(); await testChart.save(); @@ -336,9 +328,9 @@ describe('Chart', () => { total: [2, 0, 0], }, }); - })); + }); - it('Can specify offset (floor time)', async(async () => { + it('Can specify offset (floor time)', async () => { clock.tick('00:30:00'); await testChart.increment(); @@ -367,10 +359,10 @@ describe('Chart', () => { total: [2, 0, 0], }, }); - })); + }); describe('Grouped', () => { - it('Can updates', async(async () => { + it('Can updates', async () => { await testGroupedChart.increment('alice'); await testGroupedChart.save(); @@ -410,11 +402,11 @@ describe('Chart', () => { total: [0, 0, 0], }, }); - })); + }); }); describe('Unique increment', () => { - it('Can updates', async(async () => { + it('Can updates', async () => { await testUniqueChart.uniqueIncrement('alice'); await testUniqueChart.uniqueIncrement('alice'); await testUniqueChart.uniqueIncrement('bob'); @@ -430,10 +422,10 @@ describe('Chart', () => { assert.deepStrictEqual(chartDays, { foo: [2, 0, 0], }); - })); + }); describe('Intersection', () => { - it('条件が満たされていない場合はカウントされない', async(async () => { + it('条件が満たされていない場合はカウントされない', async () => { await testIntersectionChart.addA('alice'); await testIntersectionChart.addA('bob'); await testIntersectionChart.addB('carol'); @@ -453,9 +445,9 @@ describe('Chart', () => { b: [1, 0, 0], aAndB: [0, 0, 0], }); - })); + }); - it('条件が満たされている場合にカウントされる', async(async () => { + it('条件が満たされている場合にカウントされる', async () => { await testIntersectionChart.addA('alice'); await testIntersectionChart.addA('bob'); await testIntersectionChart.addB('carol'); @@ -476,12 +468,12 @@ describe('Chart', () => { b: [2, 0, 0], aAndB: [1, 0, 0], }); - })); + }); }); }); describe('Resync', () => { - it('Can resync', async(async () => { + it('Can resync', async () => { testChart.total = 1; await testChart.resync(); @@ -504,9 +496,9 @@ describe('Chart', () => { total: [1, 0, 0], }, }); - })); + }); - it('Can resync (2)', async(async () => { + it('Can resync (2)', async () => { await testChart.increment(); await testChart.save(); @@ -534,6 +526,6 @@ describe('Chart', () => { total: [100, 0, 0], }, }); - })); + }); }); }); diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock index d131f70e3..4c89fdf05 100644 --- a/packages/backend/yarn.lock +++ b/packages/backend/yarn.lock @@ -12,20 +12,6 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.9.tgz#ca34cb95e1c2dd126863a84465ae8ef66114be99" integrity sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw== -"@babel/runtime@^7.16.0": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" - integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.6.2": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d" - integrity sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/types@^7.6.1", "@babel/types@^7.9.6": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80" @@ -35,33 +21,33 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@bull-board/api@3.10.4": - version "3.10.4" - resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-3.10.4.tgz#f29d95a9624224ceec0f3ff26ef2c2bba8106921" - integrity sha512-JJjMg8O/ELeaqkuL1Wsdn6rdQfH+/2+BfnFD0B7j4ZCtLVAPfsOUZYpLqSKUgaNizwp1nTw0e3L/EI0yvX5aiw== +"@bull-board/api@3.11.1": + version "3.11.1" + resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-3.11.1.tgz#98b2c9556f643718bb5bde4a1306e6706af8192e" + integrity sha512-ElwX7sM+Ng4ZL9KUsbDubRE+r2hu/gss85OsROeE9bmyfkW14jOJkgr5MKUyjTTgPEeMs1Mw55TgQs2vxoWBiA== dependencies: redis-info "^3.0.8" -"@bull-board/koa@3.10.4": - version "3.10.4" - resolved "https://registry.yarnpkg.com/@bull-board/koa/-/koa-3.10.4.tgz#8e54600bfd8e003a8d5838ae6e65f9ec8c9979f7" - integrity sha512-NO0kzgVrl5lGNnX6maBAuP6aecGvROGka3RJSALubDfsrQ3aWNuY2BjUMUvm4ZDVfAeYT3wPaak8rdRCwxYE2g== +"@bull-board/koa@3.11.1": + version "3.11.1" + resolved "https://registry.yarnpkg.com/@bull-board/koa/-/koa-3.11.1.tgz#1872aba2c65d116d1183b3003e4a2cb2c1e2fbbf" + integrity sha512-F/thrTuC1JWpdBO7DPdKD/wr8c+d7MJGu0sr5ARsT1WXhng7sU7OqBEP/5Y7HhByurjDFXDxcgk/mc78Tmeb/Q== dependencies: - "@bull-board/api" "3.10.4" - "@bull-board/ui" "3.10.4" - ejs "^3.1.6" + "@bull-board/api" "3.11.1" + "@bull-board/ui" "3.11.1" + ejs "^3.1.7" koa "^2.13.1" koa-mount "^4.0.0" koa-router "^10.0.0" koa-static "^5.0.0" koa-views "^7.0.1" -"@bull-board/ui@3.10.4": - version "3.10.4" - resolved "https://registry.yarnpkg.com/@bull-board/ui/-/ui-3.10.4.tgz#6455b4e75fdbec1bc2ee84fde2a6a283b3c77bc9" - integrity sha512-nqnE3wqqpso7ORPcmcGVesYeFkHwv3AsBdRV2W0VLtfBPGzMdqZ1sJeSTAmlanFZnvTprU4Eg/G0DcEeMUTGhA== +"@bull-board/ui@3.11.1": + version "3.11.1" + resolved "https://registry.yarnpkg.com/@bull-board/ui/-/ui-3.11.1.tgz#17a2af5573f31811a543105b9a96249c95e93ce7" + integrity sha512-SRrfvxHF/WaBICiAFuWAoAlTvoBYUBmX94oRbSKzVILRFZMe3gs0hN071BFohrn4yOTFHAkWPN7cjMbaqHwCag== dependencies: - "@bull-board/api" "3.10.4" + "@bull-board/api" "3.11.1" "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -75,14 +61,14 @@ resolved "https://registry.yarnpkg.com/@cto.af/textdecoder/-/textdecoder-0.0.0.tgz#e1e8d84c936c30a0f4619971f19ca41941af9fdc" integrity sha512-sJpx3F5xcVV/9jNYJQtvimo4Vfld/nD3ph+ZWtQzZ03Zo8rJC7QKQTRcIGS13Rcz80DwFNthCWMrd58vpY4ZAQ== -"@digitalbazaar/http-client@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@digitalbazaar/http-client/-/http-client-1.1.0.tgz#cac383b24ace04b18b919deab773462b03d3d7b0" - integrity sha512-ks7hqa6hm9NyULdbm9qL6TRS8rADzBw8R0lETvUgvdNXu9H62XG2YqoKRDThtfgWzWxLwRJ3Z2o4ev81dZZbyQ== +"@digitalbazaar/http-client@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@digitalbazaar/http-client/-/http-client-3.2.0.tgz#b85ea09028c7d0f288f976c852d0a8f3875f0fcf" + integrity sha512-NhYXcWE/JDE7AnJikNX7q0S6zNuUPA2NuIoRdUpmvHlarjmRqyr6hIO3Awu2FxlUzbdiI1uzuWrZyB9mD1tTvw== dependencies: - esm "^3.2.22" - ky "^0.25.1" - ky-universal "^0.8.2" + ky "^0.30.0" + ky-universal "^0.10.1" + undici "^5.2.0" "@discordapp/twemoji@14.0.2": version "14.0.2" @@ -693,10 +679,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50" integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA== -"@types/node@17.0.35": - version "17.0.35" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.35.tgz#635b7586086d51fb40de0a2ec9d1014a5283ba4a" - integrity sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg== +"@types/node@17.0.41": + version "17.0.41" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.41.tgz#1607b2fd3da014ae5d4d1b31bc792a39348dfb9b" + integrity sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw== "@types/node@^14.11.8": version "14.17.9" @@ -872,14 +858,14 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.26.0.tgz#c1f98ccba9d345e38992975d3ca56ed6260643c2" - integrity sha512-oGCmo0PqnRZZndr+KwvvAUvD3kNE4AfyoGCwOZpoCncSh4MVD06JTE8XQa2u9u+NX5CsyZMBTEc2C72zx38eYA== +"@typescript-eslint/eslint-plugin@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.1.tgz#fdf59c905354139046b41b3ed95d1609913d0758" + integrity sha512-6dM5NKT57ZduNnJfpY81Phe9nc9wolnMCnknb1im6brWi1RYv84nbMS3olJa27B6+irUVV1X/Wb+Am0FjJdGFw== dependencies: - "@typescript-eslint/scope-manager" "5.26.0" - "@typescript-eslint/type-utils" "5.26.0" - "@typescript-eslint/utils" "5.26.0" + "@typescript-eslint/scope-manager" "5.27.1" + "@typescript-eslint/type-utils" "5.27.1" + "@typescript-eslint/utils" "5.27.1" debug "^4.3.4" functional-red-black-tree "^1.0.1" ignore "^5.2.0" @@ -887,69 +873,69 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.26.0.tgz#a61b14205fe2ab7533deb4d35e604add9a4ceee2" - integrity sha512-n/IzU87ttzIdnAH5vQ4BBDnLPly7rC5VnjN3m0xBG82HK6rhRxnCb3w/GyWbNDghPd+NktJqB/wl6+YkzZ5T5Q== +"@typescript-eslint/parser@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.27.1.tgz#3a4dcaa67e45e0427b6ca7bb7165122c8b569639" + integrity sha512-7Va2ZOkHi5NP+AZwb5ReLgNF6nWLGTeUJfxdkVUAPPSaAdbWNnFZzLZ4EGGmmiCTg+AwlbE1KyUYTBglosSLHQ== dependencies: - "@typescript-eslint/scope-manager" "5.26.0" - "@typescript-eslint/types" "5.26.0" - "@typescript-eslint/typescript-estree" "5.26.0" + "@typescript-eslint/scope-manager" "5.27.1" + "@typescript-eslint/types" "5.27.1" + "@typescript-eslint/typescript-estree" "5.27.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.26.0.tgz#44209c7f649d1a120f0717e0e82da856e9871339" - integrity sha512-gVzTJUESuTwiju/7NiTb4c5oqod8xt5GhMbExKsCTp6adU3mya6AGJ4Pl9xC7x2DX9UYFsjImC0mA62BCY22Iw== +"@typescript-eslint/scope-manager@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.27.1.tgz#4d1504392d01fe5f76f4a5825991ec78b7b7894d" + integrity sha512-fQEOSa/QroWE6fAEg+bJxtRZJTH8NTskggybogHt4H9Da8zd4cJji76gA5SBlR0MgtwF7rebxTbDKB49YUCpAg== dependencies: - "@typescript-eslint/types" "5.26.0" - "@typescript-eslint/visitor-keys" "5.26.0" + "@typescript-eslint/types" "5.27.1" + "@typescript-eslint/visitor-keys" "5.27.1" -"@typescript-eslint/type-utils@5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.26.0.tgz#937dee97702361744a3815c58991acf078230013" - integrity sha512-7ccbUVWGLmcRDSA1+ADkDBl5fP87EJt0fnijsMFTVHXKGduYMgienC/i3QwoVhDADUAPoytgjbZbCOMj4TY55A== +"@typescript-eslint/type-utils@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.27.1.tgz#369f695199f74c1876e395ebea202582eb1d4166" + integrity sha512-+UC1vVUWaDHRnC2cQrCJ4QtVjpjjCgjNFpg8b03nERmkHv9JV9X5M19D7UFMd+/G7T/sgFwX2pGmWK38rqyvXw== dependencies: - "@typescript-eslint/utils" "5.26.0" + "@typescript-eslint/utils" "5.27.1" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.26.0.tgz#cb204bb154d3c103d9cc4d225f311b08219469f3" - integrity sha512-8794JZFE1RN4XaExLWLI2oSXsVImNkl79PzTOOWt9h0UHROwJedNOD2IJyfL0NbddFllcktGIO2aOu10avQQyA== +"@typescript-eslint/types@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.27.1.tgz#34e3e629501349d38be6ae97841298c03a6ffbf1" + integrity sha512-LgogNVkBhCTZU/m8XgEYIWICD6m4dmEDbKXESCbqOXfKZxRKeqpiJXQIErv66sdopRKZPo5l32ymNqibYEH/xg== -"@typescript-eslint/typescript-estree@5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.26.0.tgz#16cbceedb0011c2ed4f607255f3ee1e6e43b88c3" - integrity sha512-EyGpw6eQDsfD6jIqmXP3rU5oHScZ51tL/cZgFbFBvWuCwrIptl+oueUZzSmLtxFuSOQ9vDcJIs+279gnJkfd1w== +"@typescript-eslint/typescript-estree@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.1.tgz#7621ee78607331821c16fffc21fc7a452d7bc808" + integrity sha512-DnZvvq3TAJ5ke+hk0LklvxwYsnXpRdqUY5gaVS0D4raKtbznPz71UJGnPTHEFo0GDxqLOLdMkkmVZjSpET1hFw== dependencies: - "@typescript-eslint/types" "5.26.0" - "@typescript-eslint/visitor-keys" "5.26.0" + "@typescript-eslint/types" "5.27.1" + "@typescript-eslint/visitor-keys" "5.27.1" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.26.0.tgz#896b8480eb124096e99c8b240460bb4298afcfb4" - integrity sha512-PJFwcTq2Pt4AMOKfe3zQOdez6InIDOjUJJD3v3LyEtxHGVVRK3Vo7Dd923t/4M9hSH2q2CLvcTdxlLPjcIk3eg== +"@typescript-eslint/utils@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.27.1.tgz#b4678b68a94bc3b85bf08f243812a6868ac5128f" + integrity sha512-mZ9WEn1ZLDaVrhRaYgzbkXBkTPghPFsup8zDbbsYTxC5OmqrFE7skkKS/sraVsLP3TcT3Ki5CSyEFBRkLH/H/w== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.26.0" - "@typescript-eslint/types" "5.26.0" - "@typescript-eslint/typescript-estree" "5.26.0" + "@typescript-eslint/scope-manager" "5.27.1" + "@typescript-eslint/types" "5.27.1" + "@typescript-eslint/typescript-estree" "5.27.1" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.26.0.tgz#7195f756e367f789c0e83035297c45b417b57f57" - integrity sha512-wei+ffqHanYDOQgg/fS6Hcar6wAWv0CUPQ3TZzOWd2BLfgP539rb49bwua8WRAs7R6kOSLn82rfEu2ro6Llt8Q== +"@typescript-eslint/visitor-keys@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.1.tgz#05a62666f2a89769dac2e6baa48f74e8472983af" + integrity sha512-xYs6ffo01nhdJgPieyk7HAOpjhTsx7r/oB9LWEhwAXgwn33tkr+W8DI2ChboqhZlC4q3TC6geDYPoiX8ROqyOQ== dependencies: - "@typescript-eslint/types" "5.26.0" + "@typescript-eslint/types" "5.27.1" eslint-visitor-keys "^3.3.0" "@ungap/promise-all-settled@1.1.2": @@ -1288,10 +1274,10 @@ autwh@0.1.0: dependencies: oauth "0.9.15" -aws-sdk@2.1135.0: - version "2.1135.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1135.0.tgz#8c14aa6894be529cb5fb7b6d19f3dc70e4f35816" - integrity sha512-bl9n4QgrEh52hmQ+Jo76BgJXM/p+PwfVZvImEQHFeel/33H/PDLcTJquEw5bzxM1HRNI24iH+FNPwyWLMrttTw== +aws-sdk@2.1152.0: + version "2.1152.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1152.0.tgz#73e4fb81b3a9c289234b5d6848bcdb854f169bdf" + integrity sha512-Lqwk0bDhm3vzpYb3AAM9VgGHeDpbB8+o7UJnP9R+CO23kJfi/XRpKihAcbyKDD/AUQ+O1LJaUVpvaJYLS9Am7w== dependencies: buffer "4.9.2" events "1.1.1" @@ -1300,7 +1286,7 @@ aws-sdk@2.1135.0: querystring "0.2.0" sax "1.2.1" url "0.10.3" - uuid "3.3.2" + uuid "8.0.0" xml2js "0.4.19" axios@^0.24.0: @@ -1428,19 +1414,6 @@ braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -broadcast-channel@4.12.0: - version "4.12.0" - resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.12.0.tgz#891876c5262376ab714b33a0d9e9d87a894b5bcb" - integrity sha512-hfb0L2P2CEsdM5nSqlRiZ2gQFHPdJNs1VU4rTLnFPYtq5vQAnyOdjIx+04KCWfFfRhfP3OEbxxQmnouSi8WCbQ== - dependencies: - "@babel/runtime" "^7.16.0" - detect-node "^2.1.0" - microtime "3.0.0" - oblivious-set "1.1.1" - p-queue "6.6.2" - rimraf "3.0.2" - unload "2.3.1" - browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" @@ -2097,11 +2070,6 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -data-uri-to-buffer@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" - integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== - data-uri-to-buffer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b" @@ -2289,11 +2257,6 @@ detect-libc@^2.0.0: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.0.tgz#c528bc09bc6d1aa30149228240917c225448f204" integrity sha512-S55LzUl8HUav8l9E2PBTlC5PAJrHK7tkM+XXFGD+fbsbkTzhCpG6K05LxJcUOEWzMa4v6ptcMZ9s3fOdJDu0Zw== -detect-node@2.1.0, detect-node@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - dicer@0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" @@ -2483,10 +2446,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -ejs@^3.1.6: - version "3.1.7" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.7.tgz#c544d9c7f715783dd92f0bddcf73a59e6962d006" - integrity sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw== +ejs@^3.1.7: + version "3.1.8" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" + integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ== dependencies: jake "^10.8.5" @@ -2718,10 +2681,10 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@8.16.0: - version "8.16.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.16.0.tgz#6d936e2d524599f2a86c708483b4c372c5d3bbae" - integrity sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA== +eslint@8.17.0: + version "8.17.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.17.0.tgz#1cfc4b6b6912f77d24b874ca1506b0fe09328c21" + integrity sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw== dependencies: "@eslint/eslintrc" "^1.3.0" "@humanwhocodes/config-array" "^0.9.2" @@ -2759,11 +2722,6 @@ eslint@8.16.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" -esm@^3.2.22: - version "3.2.25" - resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" - integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== - espree@^9.3.2: version "9.3.2" resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.2.tgz#f58f77bd334731182801ced3380a8cc859091596" @@ -2817,7 +2775,7 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -eventemitter3@^4.0.4, eventemitter3@^4.0.7: +eventemitter3@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -2938,11 +2896,6 @@ feed@4.2.2: dependencies: xml-js "^1.6.11" -fetch-blob@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-2.1.2.tgz#a7805db1361bd44c1ef62bb57fb5fe8ea173ef3c" - integrity sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow== - fetch-blob@^3.1.2, fetch-blob@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.1.4.tgz#e8c6567f80ad7fc22fd302e7dcb72bafde9c1717" @@ -2958,10 +2911,10 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -file-type@17.1.1: - version "17.1.1" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-17.1.1.tgz#24c59bc663df0c0c181b31dfacde25e06431afbe" - integrity sha512-heRUMZHby2Qj6wZAA3YHeMlRmZNQTcb6VxctkGmM+mcM6ROQKvHpr7SS6EgdfEhH+s25LDshBjvPx/Ecm+bOVQ== +file-type@17.1.2: + version "17.1.2" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-17.1.2.tgz#9257437a64e0c3623f70d9f27430522d978b1384" + integrity sha512-3thBUSfa9YEUEGO/NAAiQGvjujZxZiJTF6xNwyDn6kB0NcEtwMn5ttkGG9jGwm/Nt/t8U1bpBNqyBNZCz4F4ig== dependencies: readable-web-to-node-stream "^3.0.2" strtok3 "^7.0.0-alpha.7" @@ -3290,10 +3243,10 @@ got@11.5.1: p-cancelable "^2.0.0" responselike "^2.0.0" -got@12.0.4: - version "12.0.4" - resolved "https://registry.yarnpkg.com/got/-/got-12.0.4.tgz#e3b6bf6992425f904076fd71aac7030da5122de8" - integrity sha512-2Eyz4iU/ktq7wtMFXxzK7g5p35uNYLLdiZarZ5/Yn3IJlNEpBd5+dCgcAyxN8/8guZLszffwe3wVyw+DEVrpBg== +got@12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/got/-/got-12.1.0.tgz#099f3815305c682be4fd6b0ee0726d8e4c6b0af4" + integrity sha512-hBv2ty9QN2RdbJJMK3hesmSkFTjVIHyIDDbssCKnSmq62edGgImJWD10Eb1k77TiV1bxloxqcFAVK8+9pkhOig== dependencies: "@sindresorhus/is" "^4.6.0" "@szmarczak/http-timer" "^5.0.1" @@ -3653,10 +3606,10 @@ ip-address@^7.1.0: jsbn "1.1.0" sprintf-js "1.1.2" -ip-cidr@3.0.8: - version "3.0.8" - resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-3.0.8.tgz#51876484109e4aa200b86b3dc32f24a1d8986429" - integrity sha512-DLrHwoFNuLVNulwoQuHLdkIED1Hyo9iB0MB8XzZdfie23b5bonJXVB5aCyVbbvXYOmQrw3nZDcjnSO7MMYgjJg== +ip-cidr@3.0.10: + version "3.0.10" + resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-3.0.10.tgz#e1a039705196d84b43858f81a243fd70def9cefc" + integrity sha512-PXSsrRYirsuaCI1qBVyVXRLUIpNzxm76eHd3UvN5NXTMUG85GWGZpr6P+70mimc5e7Nfh/tShmjk0oSywErMWg== dependencies: ip-address "^7.1.0" jsbn "^1.1.0" @@ -4093,12 +4046,12 @@ jsonfile@^5.0.0: optionalDependencies: graceful-fs "^4.1.6" -jsonld@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-5.2.0.tgz#d1e8af38a334cb95edf0f2ae4e2b58baf8d2b5a9" - integrity sha512-JymgT6Xzk5CHEmHuEyvoTNviEPxv6ihLWSPu1gFdtjSAyM6cFqNrv02yS/SIur3BBIkCf0HjizRc24d8/FfQKw== +jsonld@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-6.0.0.tgz#560a8a871dce72aba5d4c6b08356438d863d62fb" + integrity sha512-1SkN2RXhMCTCSkX+bzHvr9ycM2HTmjWyV41hn2xG7k6BqlCgRjw0zHmuqfphjBRPqi1gKMIqgBCe/0RZMcWrAA== dependencies: - "@digitalbazaar/http-client" "^1.1.0" + "@digitalbazaar/http-client" "^3.2.0" canonicalize "^1.0.1" lru-cache "^6.0.0" rdf-canonize "^3.0.0" @@ -4113,10 +4066,10 @@ jsprim@^1.2.2: json-schema "0.4.0" verror "1.10.0" -jsrsasign@10.5.22: - version "10.5.22" - resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-10.5.22.tgz#a702fb591e634767ca3296ce7a212f92974df17c" - integrity sha512-exqUDmWKOCUK4fT79z/Fi2qGV4c+WCPjHrtJ/lVUUrrbBwJ5T5HppCcalGf3tuOlmyNdyMZ074r1bqPOUNl4Uw== +jsrsasign@10.5.24: + version "10.5.24" + resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-10.5.24.tgz#2d159e1756b2268682c6eb5e147184e33e946b1c" + integrity sha512-0i/UHRgJZifp/YmoXHyNQXUY4eKWiSd7YxuD7oKEw9mlqgr51hg9lZQw2nlEDvwHDh7pyj6ZjYlxldlW27xb/Q== jstransformer@1.0.0: version "1.0.0" @@ -4309,18 +4262,18 @@ koa@2.13.4, koa@^2.13.1: type-is "^1.6.16" vary "^1.1.2" -ky-universal@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/ky-universal/-/ky-universal-0.8.2.tgz#edc398d54cf495d7d6830aa1ab69559a3cc7f824" - integrity sha512-xe0JaOH9QeYxdyGLnzUOVGK4Z6FGvDVzcXFTdrYA1f33MZdEa45sUDaMBy98xQMcsd2XIBrTXRrRYnegcSdgVQ== +ky-universal@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/ky-universal/-/ky-universal-0.10.1.tgz#778881e098f6e3c52a87b382d9acca54d22bb0d3" + integrity sha512-r8909k+ELKZAxhVA5c440x22hqw5XcMRwLRbgpPQk4JHy3/ddJnvzcnSo5Ww3HdKdNeS3Y8dBgcIYyVahMa46g== dependencies: abort-controller "^3.0.0" - node-fetch "3.0.0-beta.9" + node-fetch "^3.2.2" -ky@^0.25.1: - version "0.25.1" - resolved "https://registry.yarnpkg.com/ky/-/ky-0.25.1.tgz#0df0bd872a9cc57e31acd5dbc1443547c881bfbc" - integrity sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA== +ky@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/ky/-/ky-0.30.0.tgz#a3d293e4f6c4604a9a4694eceb6ce30e73d27d64" + integrity sha512-X/u76z4JtDVq10u1JA5UQfatPxgPaVDMYTrgHyiTpGN2z4TMEJkIHsoSBBSg9SWZEIXTKsi9kHgiQ9o3Y/4yog== lazystream@^1.0.0: version "1.0.1" @@ -4589,14 +4542,6 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -microtime@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/microtime/-/microtime-3.0.0.tgz#d140914bde88aa89b4f9fd2a18620b435af0f39b" - integrity sha512-SirJr7ZL4ow2iWcb54bekS4aWyBQNVcEDBiwAz9D/sTgY59A+uE8UJU15cp5wyZmPBwg/3zf8lyCJ5NUe1nVlQ== - dependencies: - node-addon-api "^1.2.0" - node-gyp-build "^3.8.0" - mime-db@1.44.0: version "1.44.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" @@ -4902,11 +4847,6 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" -node-addon-api@^1.2.0: - version "1.7.2" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" - integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== - node-addon-api@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" @@ -4926,18 +4866,10 @@ node-fetch@*: fetch-blob "^3.1.4" formdata-polyfill "^4.0.10" -node-fetch@3.0.0-beta.9: - version "3.0.0-beta.9" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.0.0-beta.9.tgz#0a7554cfb824380dd6812864389923c783c80d9b" - integrity sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg== - dependencies: - data-uri-to-buffer "^3.0.1" - fetch-blob "^2.1.1" - -node-fetch@3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.4.tgz#3fbca2d8838111048232de54cb532bd3cf134947" - integrity sha512-WvYJRN7mMyOLurFR2YpysQGuwYrJN+qrrpHjJDuKMcSPdfFccRUla/kng2mz6HWSBxJcqPbvatS6Gb4RhOzCJw== +node-fetch@3.2.6, node-fetch@^3.2.2: + version "3.2.6" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.6.tgz#6d4627181697a9d9674aae0d61548e0d629b31b9" + integrity sha512-LAy/HZnLADOVkVPubaxHDft29booGglPFDr2Hw0J1AercRh01UiVFm++KMDnJeH9sHgNB4hsXPii7Sgym/sTbw== dependencies: data-uri-to-buffer "^4.0.0" fetch-blob "^3.1.4" @@ -4950,11 +4882,6 @@ node-fetch@^2.6.1: dependencies: whatwg-url "^5.0.0" -node-gyp-build@^3.8.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.9.0.tgz#53a350187dd4d5276750da21605d1cb681d09e25" - integrity sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A== - node-gyp-build@^4.2.3: version "4.3.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" @@ -5111,11 +5038,6 @@ object.values@^1.1.5: define-properties "^1.1.3" es-abstract "^1.19.1" -oblivious-set@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.1.1.tgz#d9d38e9491d51f27a5c3ec1681d2ba40aa81e98b" - integrity sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w== - on-finished@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -5263,14 +5185,6 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" -p-queue@6.6.2: - version "6.6.2" - resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" - integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== - dependencies: - eventemitter3 "^4.0.4" - p-timeout "^3.2.0" - p-timeout@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" @@ -5946,11 +5860,6 @@ reflect-metadata@0.1.13, reflect-metadata@^0.1.13: resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== -regenerator-runtime@^0.13.4: - version "0.13.7" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" - integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== - regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -6047,7 +5956,7 @@ rimraf@2: dependencies: glob "^7.1.3" -rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -6520,16 +6429,17 @@ style-loader@3.3.1: resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== -summaly@2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.5.0.tgz#ec5af6e84857efcb6c844d896e83569e64a923ea" - integrity sha512-IzvO2s7yj/PUyH42qWjVjSPpIiPlgTRWGh33t4cIZKOqPQJ2INo7e83hXhHFr4hXTb3JRcIdCuM1ELjlrujiUQ== +summaly@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.6.0.tgz#aaac80eb8ae88b130318f44d9b98da9c2ccb328c" + integrity sha512-wIv6fL3aeFfXcQoZISzeUfNUgD3u8Hwx8Rg0awZliQhans62w23K3nDezwfvmYAQCgXs6e0EF7jtGmJv/qeVTA== dependencies: cheerio "0.22.0" debug "4.3.3" escape-regexp "0.0.1" got "11.5.1" html-entities "2.3.2" + iconv-lite "0.6.3" jschardet "3.0.0" koa "2.13.4" private-ip "2.3.3" @@ -6574,10 +6484,10 @@ syslog-pro@1.0.0: dependencies: moment "^2.22.2" -systeminformation@5.11.15: - version "5.11.15" - resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.15.tgz#013038688e7ba375a5c8e88b8e7739bda7110e6b" - integrity sha512-zUbObRjQeZcu84z9NVSm9JTiCPyPQ3MefJ3+76yvp+TeCv9WsO3szijyQLv0fChRrm2/sl2De3y1ewUOYOtz2Q== +systeminformation@5.11.16: + version "5.11.16" + resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.16.tgz#5f6fda2447fafe204bd2ab543475f1ffa8c14a85" + integrity sha512-/a1VfP9WELKLT330yhAHJ4lWCXRYynel1kMMHKc/qdzCgDt3BIcMlo+3tKcTiRHFefjV3fz4AvqMx7dGO/72zw== tapable@^2.2.0: version "2.2.0" @@ -6745,10 +6655,10 @@ ts-loader@9.3.0: micromatch "^4.0.0" semver "^7.3.4" -ts-node@10.8.0: - version "10.8.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.8.0.tgz#3ceb5ac3e67ae8025c1950626aafbdecb55d82ce" - integrity sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA== +ts-node@10.8.1: + version "10.8.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.8.1.tgz#ea2bd3459011b52699d7e88daa55a45a1af4f066" + integrity sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g== dependencies: "@cspotcode/source-map-support" "^0.8.0" "@tsconfig/node10" "^1.0.7" @@ -6764,10 +6674,10 @@ ts-node@10.8.0: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tsc-alias@1.6.7: - version "1.6.7" - resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.6.7.tgz#354840d6444db79dd13fcc4f9ec37574ff9d5120" - integrity sha512-GRbZx/zTee01JtrHB7hkddgxn+aQqYDmRaFv/MTYIqBbk/L8Zf0nA/T60wXOr/Q7002YXppUFXsqsu5ViWB4vQ== +tsc-alias@1.6.9: + version "1.6.9" + resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.6.9.tgz#d04d95124b95ad8eea55e52d45cf65a744c26baa" + integrity sha512-5lv5uAHn0cgxY1XfpXIdquUSz2xXq3ryQyNtxC6DYH7YT5rt/W+9Gsft2uyLFTh+ozk4qU8iCSP3VemjT69xlQ== dependencies: chokidar "^3.5.3" commander "^9.0.0" @@ -6911,10 +6821,10 @@ typeorm@0.3.6: xml2js "^0.4.23" yargs "^17.3.1" -typescript@4.7.2: - version "4.7.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4" - integrity sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A== +typescript@4.7.3: + version "4.7.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d" + integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA== ulid@2.3.0: version "2.3.0" @@ -6931,6 +6841,11 @@ unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" +undici@^5.2.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.4.0.tgz#c474fae02743d4788b96118d46008a24195024d2" + integrity sha512-A1SRXysDg7J+mVP46jF+9cKANw0kptqSFZ8tGyL+HBiv0K1spjxPX8Z4EGu+Eu6pjClJUBdnUPlxrOafR668/g== + unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -6950,14 +6865,6 @@ universalify@^0.1.0, universalify@^0.1.2: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== -unload@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/unload/-/unload-2.3.1.tgz#9d16862d372a5ce5cb630ad1309c2fd6e35dacfe" - integrity sha512-MUZEiDqvAN9AIDRbbBnVYVvfcR6DrjCqeU2YQMmliFZl9uaBUjTkhuDQkBiyAy8ad5bx1TXVbqZ3gg7namsWjA== - dependencies: - "@babel/runtime" "^7.6.2" - detect-node "2.1.0" - unpipe@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -7011,16 +6918,16 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -uuid@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== - uuid@7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== +uuid@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" + integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== + uuid@8.3.2, uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -7223,10 +7130,10 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -ws@8.6.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.6.0.tgz#e5e9f1d9e7ff88083d0c0dd8281ea662a42c9c23" - integrity sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw== +ws@8.8.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.0.tgz#8e71c75e2f6348dbf8d78005107297056cb77769" + integrity sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ== ws@^8.2.3: version "8.4.2" diff --git a/packages/client/.eslintrc.js b/packages/client/.eslintrc.js index a6e23e517..10f0e5a9c 100644 --- a/packages/client/.eslintrc.js +++ b/packages/client/.eslintrc.js @@ -1,68 +1,85 @@ module.exports = { root: true, env: { - "node": false + 'node': false, }, - parser: "vue-eslint-parser", + parser: 'vue-eslint-parser', parserOptions: { - "parser": "@typescript-eslint/parser", + 'parser': '@typescript-eslint/parser', tsconfigRootDir: __dirname, - //project: ['./tsconfig.json'], + project: ['./tsconfig.json'], + extraFileExtensions: ['.vue'], }, extends: [ - //"../shared/.eslintrc.js", - "plugin:vue/vue3-recommended" + '../shared/.eslintrc.js', + 'plugin:vue/vue3-recommended', ], rules: { + '@typescript-eslint/no-empty-interface': [ + 'error', + { + 'allowSingleExtends': true, + }, + ], // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため // data の禁止理由: 抽象的すぎるため // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため - "id-denylist": ["error", "window", "data", "e"], + 'id-denylist': ['error', 'window', 'data', 'e'], 'eqeqeq': ['error', 'always', { 'null': 'ignore' }], - "no-shadow": ["warn"], - "vue/attributes-order": ["error", { - "alphabetical": false + 'no-shadow': ['warn'], + 'vue/attributes-order': ['error', { + 'alphabetical': false, }], - "vue/no-use-v-if-with-v-for": ["error", { - "allowUsingIterationVar": false + 'vue/no-use-v-if-with-v-for': ['error', { + 'allowUsingIterationVar': false, }], - "vue/no-ref-as-operand": "error", - "vue/no-multi-spaces": ["error", { - "ignoreProperties": false + 'vue/no-ref-as-operand': 'error', + 'vue/no-multi-spaces': ['error', { + 'ignoreProperties': false, }], - "vue/no-v-html": "error", - "vue/order-in-components": "error", - "vue/html-indent": ["warn", "tab", { - "attribute": 1, - "baseIndent": 0, - "closeBracket": 0, - "alignAttributesVertically": true, - "ignores": [] + 'vue/no-v-html': 'error', + 'vue/order-in-components': 'error', + 'vue/html-indent': ['warn', 'tab', { + 'attribute': 1, + 'baseIndent': 0, + 'closeBracket': 0, + 'alignAttributesVertically': true, + 'ignores': [], }], - "vue/html-closing-bracket-spacing": ["warn", { - "startTag": "never", - "endTag": "never", - "selfClosingTag": "never" + 'vue/html-closing-bracket-spacing': ['warn', { + 'startTag': 'never', + 'endTag': 'never', + 'selfClosingTag': 'never', }], - "vue/multi-word-component-names": "warn", - "vue/require-v-for-key": "warn", - "vue/no-unused-components": "warn", - "vue/valid-v-for": "warn", - "vue/return-in-computed-property": "warn", - "vue/no-setup-props-destructure": "warn", - "vue/max-attributes-per-line": "off", - "vue/html-self-closing": "off", - "vue/singleline-html-element-content-newline": "off", + 'vue/multi-word-component-names': 'warn', + 'vue/require-v-for-key': 'warn', + 'vue/no-unused-components': 'warn', + 'vue/valid-v-for': 'warn', + 'vue/return-in-computed-property': 'warn', + 'vue/no-setup-props-destructure': 'warn', + 'vue/max-attributes-per-line': 'off', + 'vue/html-self-closing': 'off', + 'vue/singleline-html-element-content-newline': 'off', }, globals: { - "require": false, - "_DEV_": false, - "_LANGS_": false, - "_VERSION_": false, - "_ENV_": false, - "_PERF_PREFIX_": false, - "_DATA_TRANSFER_DRIVE_FILE_": false, - "_DATA_TRANSFER_DRIVE_FOLDER_": false, - "_DATA_TRANSFER_DECK_COLUMN_": false - } -} + // Node.js + 'module': false, + 'require': false, + '__dirname': false, + + // Vue + '$$': false, + '$ref': false, + '$computed': false, + + // Misskey + '_DEV_': false, + '_LANGS_': false, + '_VERSION_': false, + '_ENV_': false, + '_PERF_PREFIX_': false, + '_DATA_TRANSFER_DRIVE_FILE_': false, + '_DATA_TRANSFER_DRIVE_FOLDER_': false, + '_DATA_TRANSFER_DECK_COLUMN_': false, + }, +}; diff --git a/packages/client/@types/theme.d.ts b/packages/client/@types/theme.d.ts index b8b906b82..67f724a9a 100644 --- a/packages/client/@types/theme.d.ts +++ b/packages/client/@types/theme.d.ts @@ -1,5 +1,7 @@ -import { Theme } from '../src/scripts/theme'; - declare module '@/themes/*.json5' { - export = Theme; + import { Theme } from "@/scripts/theme"; + + const theme: Theme; + + export default theme; } diff --git a/packages/client/package.json b/packages/client/package.json index 599c080ea..9d1ec288d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -12,13 +12,17 @@ "dependencies": { "@discordapp/twemoji": "14.0.2", "@fortawesome/fontawesome-free": "6.1.1", + "@rollup/plugin-alias": "3.1.9", + "@rollup/plugin-json": "4.1.0", "@syuilo/aiscript": "0.11.1", + "@vitejs/plugin-vue": "2.3.3", + "@vue/compiler-sfc": "3.2.37", "abort-controller": "3.0.0", "autobind-decorator": "2.4.0", "autosize": "5.0.1", "autwh": "0.1.0", "blurhash": "1.1.5", - "broadcast-channel": "4.12.0", + "broadcast-channel": "4.13.0", "browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2", "chart.js": "3.8.0", "chartjs-adapter-date-fns": "2.0.0", @@ -26,6 +30,7 @@ "chartjs-plugin-zoom": "1.2.1", "compare-versions": "4.1.3", "content-disposition": "0.5.4", + "cropperjs": "2.0.0-beta", "date-fns": "2.28.0", "deepcopy": "2.1.0", "escape-regexp": "0.0.1", @@ -52,35 +57,30 @@ "random-seed": "0.3.0", "reflect-metadata": "0.1.13", "rndstr": "1.0.0", + "rollup": "2.75.6", "s-age": "1.1.2", - "sass": "1.52.1", + "sass": "1.52.3", "seedrandom": "3.0.5", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "syuilo-password-strength": "0.0.1", "textarea-caret": "3.1.0", - "three": "0.140.2", + "three": "0.141.0", "throttle-debounce": "5.0.0", "tinycolor2": "1.4.2", - "tsc-alias": "1.6.7", + "tsc-alias": "1.6.9", "tsconfig-paths": "4.0.0", "twemoji-parser": "14.0.0", + "typescript": "4.7.3", "uuid": "8.3.2", "v-debounce": "0.1.2", "vanilla-tilt": "1.7.2", - "vue": "3.2.36", + "vite": "2.9.10", + "vue": "3.2.37", "vue-prism-editor": "2.0.0-alpha.2", - "vue-router": "4.0.15", "vuedraggable": "4.0.1", "websocket": "1.0.34", - "@vitejs/plugin-vue": "2.3.3", - "@vue/compiler-sfc": "3.2.36", - "@rollup/plugin-alias": "3.1.9", - "@rollup/plugin-json": "4.1.0", - "rollup": "2.74.1", - "typescript": "4.7.2", - "vite": "2.9.9", - "ws": "8.6.0" + "ws": "8.8.0" }, "devDependencies": { "@types/escape-regexp": "0.0.1", @@ -101,13 +101,13 @@ "@types/uuid": "8.3.4", "@types/websocket": "1.0.5", "@types/ws": "8.5.3", - "@typescript-eslint/eslint-plugin": "5.26.0", - "@typescript-eslint/parser": "5.26.0", - "eslint": "8.16.0", - "eslint-plugin-vue": "9.0.1", + "@typescript-eslint/eslint-plugin": "5.27.1", + "@typescript-eslint/parser": "5.27.1", "cross-env": "7.0.3", - "cypress": "9.7.0", + "cypress": "10.0.3", + "eslint": "8.17.0", "eslint-plugin-import": "2.26.0", + "eslint-plugin-vue": "9.1.0", "start-server-and-test": "1.14.0" } } diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index ce4af61f1..2b07dd199 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -1,11 +1,11 @@ -import { del, get, set } from '@/scripts/idb-proxy'; import { defineAsyncComponent, reactive } from 'vue'; import * as misskey from 'misskey-js'; +import { showSuspendedDialog } from './scripts/show-suspended-dialog'; +import { i18n } from './i18n'; +import { del, get, set } from '@/scripts/idb-proxy'; import { apiUrl } from '@/config'; import { waiting, api, popup, popupMenu, success, alert } from '@/os'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; -import { showSuspendedDialog } from './scripts/show-suspended-dialog'; -import { i18n } from './i18n'; // TODO: 他のタブと永続化されたstateを同期 @@ -22,13 +22,7 @@ export async function signout() { waiting(); localStorage.removeItem('account'); - //#region Remove account - const accounts = await getAccounts(); - accounts.splice(accounts.findIndex(x => x.id === $i.id), 1); - - if (accounts.length > 0) await set('accounts', accounts); - else await del('accounts'); - //#endregion + await removeAccount($i.id); //#region Remove service worker registration try { @@ -55,7 +49,7 @@ export async function signout() { } catch (err) {} //#endregion - document.cookie = `igi=; path=/`; + document.cookie = 'igi=; path=/'; if (accounts.length > 0) login(accounts[0].token); else unisonReload('/'); @@ -72,14 +66,22 @@ export async function addAccount(id: Account['id'], token: Account['token']) { } } +export async function removeAccount(id: Account['id']) { + const accounts = await getAccounts(); + accounts.splice(accounts.findIndex(x => x.id === id), 1); + + if (accounts.length > 0) await set('accounts', accounts); + else await del('accounts'); +} + function fetchAccount(token: string): Promise { return new Promise((done, fail) => { // Fetch user fetch(`${apiUrl}/i`, { method: 'POST', body: JSON.stringify({ - i: token - }) + i: token, + }), }) .then(res => res.json()) .then(res => { @@ -216,13 +218,13 @@ export async function openAccountMenu(opts: { type: 'link', icon: 'fas fa-users', text: i18n.ts.manageAccounts, - to: `/settings/accounts`, + to: '/settings/accounts', }]], ev.currentTarget ?? ev.target, { - align: 'left' + align: 'left', }); } else { popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, { - align: 'left' + align: 'left', }); } } diff --git a/packages/client/src/components/abuse-report.vue b/packages/client/src/components/abuse-report.vue index a947406f8..86eccd627 100644 --- a/packages/client/src/components/abuse-report.vue +++ b/packages/client/src/components/abuse-report.vue @@ -1,13 +1,19 @@ - diff --git a/packages/client/src/components/autocomplete.vue b/packages/client/src/components/autocomplete.vue index 1e4a4506f..ae708026e 100644 --- a/packages/client/src/components/autocomplete.vue +++ b/packages/client/src/components/autocomplete.vue @@ -35,6 +35,7 @@ + + diff --git a/packages/client/src/components/drive-file-thumbnail.vue b/packages/client/src/components/drive-file-thumbnail.vue index dd24440e8..b346585ce 100644 --- a/packages/client/src/components/drive-file-thumbnail.vue +++ b/packages/client/src/components/drive-file-thumbnail.vue @@ -1,6 +1,6 @@