diff --git a/.config/docker_example.yml b/.config/docker_example.yml new file mode 100644 index 0000000000..bd5eab492b --- /dev/null +++ b/.config/docker_example.yml @@ -0,0 +1,151 @@ +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# Misskey configuration +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +# ┌─────┐ +#───┘ URL └───────────────────────────────────────────────────── + +# Final accessible URL seen by a user. +url: https://example.tld/ + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# URL SETTINGS AFTER THAT! + +# ┌───────────────────────┐ +#───┘ Port and TLS settings └─────────────────────────────────── + +# +# Misskey requires a reverse proxy to support HTTPS connections. +# +# +----- https://example.tld/ ------------+ +# +------+ |+-------------+ +----------------+| +# | User | ---> || Proxy (443) | ---> | Misskey (3000) || +# +------+ |+-------------+ +----------------+| +# +---------------------------------------+ +# +# You need to set up a reverse proxy. (e.g. nginx) +# An encrypted connection with HTTPS is highly recommended +# because tokens may be transferred in GET requests. + +# The port that your Misskey server should listen on. +port: 3000 + +# ┌──────────────────────────┐ +#───┘ PostgreSQL configuration └──────────────────────────────── + +db: + host: db + port: 5432 + + # Database name + db: misskey + + # Auth + user: example-misskey-user + pass: example-misskey-pass + + # Whether disable Caching queries + #disableCache: true + + # Extra Connection options + #extra: + # ssl: true + +# ┌─────────────────────┐ +#───┘ Redis configuration └───────────────────────────────────── + +redis: + host: redis + port: 6379 + #family: 0 # 0=Both, 4=IPv4, 6=IPv6 + #pass: example-pass + #prefix: example-prefix + #db: 1 + +# ┌─────────────────────────────┐ +#───┘ Elasticsearch configuration └───────────────────────────── + +#elasticsearch: +# host: localhost +# port: 9200 +# ssl: false +# user: +# pass: + +# ┌───────────────┐ +#───┘ ID generation └─────────────────────────────────────────── + +# You can select the ID generation method. +# You don't usually need to change this setting, but you can +# change it according to your preferences. + +# Available methods: +# aid ... Short, Millisecond accuracy +# meid ... Similar to ObjectID, Millisecond accuracy +# ulid ... Millisecond accuracy +# objectid ... This is left for backward compatibility + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# ID SETTINGS AFTER THAT! + +id: 'aid' + +# ┌─────────────────────┐ +#───┘ Other configuration └───────────────────────────────────── + +# Whether disable HSTS +#disableHsts: true + +# Number of worker processes +#clusterLimit: 1 + +# Job concurrency per worker +# deliverJobConcurrency: 128 +# inboxJobConcurrency: 16 + +# Job rate limiter +# deliverJobPerSec: 128 +# inboxJobPerSec: 16 + +# Job attempts +# deliverJobMaxAttempts: 12 +# inboxJobMaxAttempts: 8 + +# IP address family used for outgoing request (ipv4, ipv6 or dual) +#outgoingAddressFamily: ipv4 + +# Syslog option +#syslog: +# host: localhost +# port: 514 + +# Proxy for HTTP/HTTPS +#proxy: http://127.0.0.1:3128 + +proxyBypassHosts: + - api.deepl.com + - api-free.deepl.com + - www.recaptcha.net + - hcaptcha.com + - challenges.cloudflare.com + +# Proxy for SMTP/SMTPS +#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT +#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 +#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 + +# Media Proxy +#mediaProxy: https://example.com/proxy + +# Proxy remote files (default: false) +#proxyRemoteFiles: true + +# Sign to ActivityPub GET request (default: true) +signToActivityPubGet: true + +#allowedPrivateNetworks: [ +# '127.0.0.1/32' +#] + +# Upload or download file size limits (bytes) +#maxFileSize: 262144000 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0d287327a7..e878e5836a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,11 @@ version: 2 updates: +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 0 - package-ecosystem: npm directory: "/" schedule: @@ -20,3 +25,8 @@ updates: schedule: interval: daily open-pull-requests-limit: 0 +- package-ecosystem: npm + directory: "/packages/sw" + schedule: + interval: daily + open-pull-requests-limit: 0 diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml index 09331edd1a..f04888e982 100644 --- a/.github/workflows/docker-develop.yml +++ b/.github/workflows/docker-develop.yml @@ -10,10 +10,10 @@ jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest - + if: github.repository == 'misskey-dev/misskey' steps: - name: Check out the repo - uses: actions/checkout@v2 + uses: actions/checkout@v3.3.0 - name: Docker meta id: meta uses: docker/metadata-action@v3 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a84e4bc96e..84d36f8465 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Check out the repo - uses: actions/checkout@v2 + uses: actions/checkout@v3.3.0 - name: Docker meta id: meta uses: docker/metadata-action@v3 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a0e84a0985..3c51d94bc0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,11 +11,11 @@ jobs: yarn_install: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.3.0 with: fetch-depth: 0 submodules: true - - uses: actions/setup-node@v3.2.0 + - uses: actions/setup-node@v3.6.0 with: node-version: 18.x cache: 'yarn' @@ -33,11 +33,11 @@ jobs: - frontend - sw steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.3.0 with: fetch-depth: 0 submodules: true - - uses: actions/setup-node@v3.2.0 + - uses: actions/setup-node@v3.6.0 with: node-version: 18.x cache: 'yarn' diff --git a/.github/workflows/pr-preview-deploy.yml b/.github/workflows/pr-preview-deploy.yml index fd43bce9e6..9b786d34aa 100644 --- a/.github/workflows/pr-preview-deploy.yml +++ b/.github/workflows/pr-preview-deploy.yml @@ -1,7 +1,5 @@ # Run secret-dependent integration tests only after /deploy approval on: - pull_request: - types: [opened, reopened, synchronize] repository_dispatch: types: [deploy-command] @@ -12,11 +10,10 @@ jobs: 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 + - uses: actions/github-script@v6.3.3 id: check-id env: number: ${{ github.event.client_payload.pull_request.number }} @@ -40,7 +37,7 @@ jobs: return check[0].id; - - uses: actions/github-script@v5 + - uses: actions/github-script@v6.3.3 env: check_id: ${{ steps.check-id.outputs.result }} details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }} @@ -56,7 +53,7 @@ jobs: # Check out merge commit - name: Fork based /deploy checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3.3.0 with: ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' @@ -75,7 +72,7 @@ jobs: timeout: 15m # Update check run called "integration-fork" - - uses: actions/github-script@v5 + - uses: actions/github-script@v6.3.3 id: update-check-run if: ${{ always() }} env: diff --git a/.github/workflows/pr-preview-destroy.yml b/.github/workflows/pr-preview-destroy.yml index c14c3db5c5..49f1ba8a34 100644 --- a/.github/workflows/pr-preview-destroy.yml +++ b/.github/workflows/pr-preview-destroy.yml @@ -9,6 +9,7 @@ name: Destroy preview environment jobs: destroy-preview-environment: runs-on: ubuntu-latest + if: github.repository == github.event.pull_request.head.repo.full_name steps: - name: Context uses: okteto/context@latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 444ee0b5c9..a607a79cb4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,11 +30,11 @@ jobs: - 56312:6379 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.3.0 with: submodules: true - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3.2.0 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ matrix.node-version }} cache: 'yarn' @@ -77,7 +77,7 @@ jobs: - 56312:6379 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.3.0 with: submodules: true # https://github.com/cypress-io/cypress-docker-images/issues/150 @@ -87,7 +87,7 @@ jobs: #- uses: browser-actions/setup-firefox@latest # if: ${{ matrix.browser == 'firefox' }} - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3.2.0 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ matrix.node-version }} cache: 'yarn' diff --git a/.gitignore b/.gitignore index bfccdd46cb..11ef3dd40d 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ coverage # config /.config/* !/.config/example.yml +!/.config/docker_example.yml !/.config/docker_example.env # misskey diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cda5537dc..3aba40aef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,18 @@ You should also include the user name that made the change. ## 13.0.0 (unreleased) ### TL;DR -- New features (Play, new widgets, new charts, etc) +- New features (Role system, Misskey Play, New widgets, New charts, 🍪👈, etc) - Rewriten backend - Better performance (backend and frontend) - Various usability improvements - Various UI tweaks +### Notable features +- ロール機能 + - 従来より柔軟にユーザーの権限を管理できます。例えば、「インスタンスのパトロンはアンテナを30個まで作れる」「基本的にLTLは見れないが、許可した人だけ見れる」「招待制インスタンスだけどユーザーなら誰でも他者を招待できる」のような運用はもちろん、「ローカルユーザーかつアカウント作成から1日未満のユーザーはパブリックな投稿を行えない」のように複数条件を組み合わせて、自動でロールを付与する設定も可能です。 +- Misskey Play + - 従来の動的なPagesに代わる、新しいプラットフォームです。動的なコンテンツ(アプリケーション)に特化していて、Pagesに比べてはるかに柔軟なアプリケーションを作成可能です。 + ### Changes #### For server admins - Node.js 18.x or later is required @@ -27,18 +33,30 @@ You should also include the user name that made the change. - 代わりに今後任意の検索プロバイダを設定できる仕組みを構想しています。その仕組みを使えば今まで通りElasticsearchも利用できます - Migrate to Yarn Berry (v3.2.1) @ThatOneCalculator - You may have to `yarn run clean-all`, `sudo corepack enable` and `yarn set version berry` before running `yarn install` if you're still on yarn classic +- インスタンスブロックはサブドメインにも適用されるようになります +- ロールの導入に伴い、いくつかの機能がロールと統合されました + - モデレーターはロールに統合されました。今までのモデレーター情報は失われるため、予めモデレーター一覧を記録しておき、アップデート後にモデレーターロールを作りアサインし直してください。 + - サイレンスはロールに統合されました。今までのユーザーは恩赦されるため、予めサイレンス一覧を記録しておくのをおすすめします。 + - ユーザーごとのドライブ容量設定はロールに統合されました。 + - インスタンスデフォルトのドライブ容量設定はロールに統合されました。アップデート後、ベースロールのドライブ容量を編集してください。 + - LTL/GTLの解放状態はロールに統合されました。 #### For users - ノートのウォッチ機能が削除されました +- アンケートに投票された際に通知が作成されなくなりました +- ノートの数式埋め込みが削除されました - 新たに動的なPagesを作ることはできなくなりました - 代わりにAiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能が実装されています。 -- AiScriptが0.12.1にアップデートされました +- AiScriptが0.12.2にアップデートされました - 0.12.xの変更点についてはこちら https://github.com/syuilo/aiscript/blob/master/CHANGELOG.md#0120 - - 0.12.1未満のプラグインは読み込むことはできません + - 0.12.x未満のプラグインは読み込むことはできません - iOS15以下のデバイスはサポートされなくなりました -- Firefox109以下はサポートされなくなりました +- Firefox110以下はサポートされなくなりました + - 109でもContainerQueriesのフラグを有効にする事で問題なく使用できます #### For app developers +- API: metaのレスポンスに`emojis`プロパティが含まれなくなりました + - カスタム絵文字一覧情報を取得するには、`emojis`エンドポイントにリクエストします - API: カスタム絵文字エンティティに`url`プロパティが含まれなくなりました - 絵文字画像を表示するには、`/emoji/.webp`にリクエストすると画像が返ります。 - e.g. `https://p1.a9z.dev/emoji/misskey.webp` @@ -48,6 +66,7 @@ You should also include the user name that made the change. - API: `instance`エンティティに`latestStatus`、`lastCommunicatedAt`、`latestRequestSentAt`プロパティが含まれなくなりました ### Improvements +- Role system @syuilo - Misskey Play @syuilo - Introduce retention-rate aggregation @syuilo - Make possible to export favorited notes @syuilo @@ -55,37 +74,60 @@ You should also include the user name that made the change. - Push notification of Antenna note @tamaina - AVIF support @tamaina - Add Cloudflare Turnstile CAPTCHA support @CyberRex0 +- レートリミットをユーザーごとに調整可能に @syuilo +- 非モデレーターでも、権限を持つロールをアサインされたユーザーはインスタンスの招待コードを発行できるように @syuilo +- 非モデレーターでも、権限を持つロールをアサインされたユーザーはカスタム絵文字の追加、編集、削除を行えるように @syuilo +- クリップおよびクリップ内のノートの作成可能数を設定可能に @syuilo +- ユーザーリストおよびユーザーリスト内のユーザーの作成可能数を設定可能に @syuilo +- ハードワードミュートの最大文字数を設定可能に @syuilo +- Webhookの作成可能数を設定可能に @syuilo +- ノートをピン留めできる数を設定可能に @syuilo - Server: signToActivityPubGet is set to true by default @syuilo - Server: improve syslog performance @syuilo +- Server: Use undici instead of node-fetch and got @tamaina +- Server: Judge instance block by endsWith @tamaina - Server: improve note scoring for featured notes @CyberRex0 +- Server: アンケート選択肢の文字数制限を緩和 @syuilo +- Server: add rate limits for some endpoints @syuilo +- Server: improve stats api performance @syuilo +- Server: improve nodeinfo performance @syuilo - Server: delete outdated notifications regularly to improve db performance @syuilo - Server: delete outdated hard-mutes regularly to improve db performance @syuilo - Server: delete outdated notes of antenna regularly to improve db performance @syuilo - Server: improve activitypub deliver performance @syuilo - Client: use tabler-icons instead of fontawesome to better design @syuilo -- Client: Add AiScript App widget - Client: Add new gabber kick sounds (thanks for noizenecio) - Client: Add link to user RSS feed in profile menu @ssmucny - Client: Compress non-animated PNG files @saschanaz - Client: YouTube window player @sim1222 +- Client: show readable error when rate limit exceeded @syuilo - Client: enhance dashboard of control panel @syuilo - Client: Vite is upgraded to v4 @syuilo, @tamaina - Client: HMR is available while yarn dev @tamaina -- Client: Make widgets of universal/classic sync between devices @tamaina - Client: Implement the button to subscribe push notification @tamaina - Client: Implement the toggle to or not to close push notifications when notifications or messages are read @tamaina -- Client: Improve RSS widget @tamaina - Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz - Client: OpenSearch support @SoniEx2 @chaoticryptidz +- Client: Support remote objects in search @SoniEx2 +- Client: user activity page @syuilo +- Client: Make widgets of universal/classic sync between devices @tamaina - Client: add user list widget @syuilo +- Client: Add AiScript App widget +- Client: add profile widget @syuilo +- Client: add instance info widget @syuilo +- Client: Improve RSS widget @tamaina - Client: add heatmap of daily active users to about page @syuilo - Client: introduce fluent emoji @syuilo +- Client: add new theme @syuilo +- Client: add new mfm function (position, fg, bg) @syuilo - Client: show fireworks when visit user who today is birthday @syuilo - Client: show bot warning on screen when logged in as bot account @syuilo - Client: improve overall performance of client @syuilo - Client: ui tweaks @syuilo +- Client: clicker game @syuilo ### Bugfixes +- Server: Fix @tensorflow/tfjs-core's MODULE_NOT_FOUND error @ikuradon - Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468 - Server: Bug fix for Pinned Users lookup on instance @squidicuzz - Server: Fix peers API returning suspended instances @ineffyble @@ -94,14 +136,29 @@ You should also include the user name that made the change. - Server: アンテナの作成数上限を追加 @syuilo - Server: pages/likeのエラーIDが重複しているのを修正 @syuilo - Server: pages/updateのパラメータによってはsummaryの値が更新されないのを修正 @syuilo +- Server: Escape SQL LIKE @mei23 +- Server: 特定のPNG画像のアップロードに失敗する問題を修正 @usbharu +- Server: 非公開のクリップのURLでOGPレンダリングされる問題を修正 @syuilo +- Server: アンテナタイムライン(ストリーミング)が、フォローしていないユーザーの鍵投稿も拾ってしまう @syuilo +- Server: follow request list api pagination @sim1222 +- Server: ドライブ容量超過時のエラーが適切にレスポンスされない問題を修正 @syuilo +- Client: パスワードマネージャーなどでユーザー名がオートコンプリートされない問題を修正 @massongit +- Client: 日付形式の文字列などがカスタム絵文字として表示されるのを修正 @syuilo - Client: case insensitive emoji search @saschanaz +- Client: 画面の幅が狭いとウィジェットドロワーを閉じる手段がなくなるのを修正 @syuilo - Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina - Client: use proxied image for instance icon @syuilo - Client: Webhookの編集画面で、内容を保存することができない問題を修正 @m-hayabusa +- Client: Page編集でブロックの移動が行えない問題を修正 @syuilo - Client: update emoji picker immediately on all input @saschanaz - Client: チャートのツールチップが画面に残ることがあるのを修正 @syuilo - Client: fix wrong link in tutorial @syuilo +### Special thanks +- All contributors +- All who have created instances for the beta test +- All who participated in the beta test + ## 12.119.1 (2022/12/03) ### Bugfixes - Server: Mitigate AP reference chain DoS vector @skehmatics diff --git a/COPYING b/COPYING index afa1794598..c218443d42 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,5 @@ Unless otherwise stated this repository is -Copyright © 2014-2022 syuilo and contributers +Copyright © 2014-2023 syuilo and contributers And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE. diff --git a/Dockerfile b/Dockerfile index 25cec42a9b..a86686426f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,6 @@ -FROM node:18.12.1-bullseye AS builder +ARG NODE_VERSION=18.13.0-bullseye + +FROM node:${NODE_VERSION} AS builder ARG NODE_ENV=production @@ -22,23 +24,29 @@ COPY . ./ RUN git submodule update --init RUN yarn build -FROM node:18.12.1-bullseye-slim AS runner +FROM node:${NODE_VERSION}-slim AS runner -WORKDIR /misskey +ARG UID="991" +ARG GID="991" RUN apt-get update \ && apt-get install -y --no-install-recommends \ ffmpeg tini \ && apt-get -y clean \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && groupadd -g "${GID}" misskey \ + && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey -COPY --from=builder /misskey/.yarn/install-state.gz ./.yarn/install-state.gz -COPY --from=builder /misskey/node_modules ./node_modules -COPY --from=builder /misskey/built ./built -COPY --from=builder /misskey/packages/backend/node_modules ./packages/backend/node_modules -COPY --from=builder /misskey/packages/backend/built ./packages/backend/built -COPY --from=builder /misskey/packages/frontend/node_modules ./packages/frontend/node_modules -COPY . ./ +USER misskey +WORKDIR /misskey + +COPY --chown=misskey:misskey --from=builder /misskey/.yarn/install-state.gz ./.yarn/install-state.gz +COPY --chown=misskey:misskey --from=builder /misskey/node_modules ./node_modules +COPY --chown=misskey:misskey --from=builder /misskey/built ./built +COPY --chown=misskey:misskey --from=builder /misskey/packages/backend/node_modules ./packages/backend/node_modules +COPY --chown=misskey:misskey --from=builder /misskey/packages/backend/built ./packages/backend/built +COPY --chown=misskey:misskey --from=builder /misskey/packages/frontend/node_modules ./packages/frontend/node_modules +COPY --chown=misskey:misskey . ./ ENV NODE_ENV=production ENTRYPOINT ["/usr/bin/tini", "--"] diff --git a/cypress/e2e/widgets.cy.js b/cypress/e2e/widgets.cy.js index 56ad95ee94..271917889e 100644 --- a/cypress/e2e/widgets.cy.js +++ b/cypress/e2e/widgets.cy.js @@ -29,15 +29,15 @@ describe('After user signed in', () => { 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); + cy.get('.data-cy-customize-container:first-child .data-cy-customize-container-remove._button').click(); + cy.get('.data-cy-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('.data-cy-bg._modalBg.data-cy-transparent').click({ multiple: true, force: true }); cy.get('.mk-widget-add').click({ force: true }); cy.get(`.mkw-${widgetName}`).should('exist'); }); diff --git a/docker-compose.yml b/docker-compose.yml index 0bf17a5557..efc85c372e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,11 @@ services: - db - redis # - es + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy ports: - "3000:3000" networks: @@ -24,6 +29,10 @@ services: - internal_network volumes: - ./redis:/data + healthcheck: + test: "redis-cli ping" + interval: 5s + retries: 20 db: restart: always @@ -34,6 +43,10 @@ services: - .config/docker.env volumes: - ./db:/var/lib/postgresql/data + healthcheck: + test: "pg_isready" + interval: 5s + retries: 20 # es: # restart: always diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 161a393bc2..ad000b7eda 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -817,6 +817,7 @@ account: "الحسابات" cannotLoad: "تعذر التحميل" like: "أعجبني" show: "المظهر" +color: "اللون" _emailUnavailable: used: "هذا البريد الإلكتروني مستخدم" format: "صيغة البريد الإلكتروني غير صالحة" @@ -1117,6 +1118,8 @@ _weekday: friday: "الجمعة" saturday: "السبت" _widgets: + profile: "الملف التعريفي" + instanceInfo: "معلومات مثيل الخادم" memo: "ملاحظة لاصقة" notifications: "الإشعارات" timeline: "الخيط الزمني" @@ -1294,7 +1297,6 @@ _notification: youGotReply: "ردّ عليك {name}" youGotQuote: "اقتبس منك {name}" youRenoted: "إعادت نشر من {name}" - youGotPoll: "شارك {name} في استطلاع الرأي" youGotMessagingMessageFromUser: "لقد تلقيت رسالة مِن {name}" youGotMessagingMessageFromGroup: "لقد أرسِلَت رسالة إلى الفريق {name}" youWereFollowed: "يتابعك" @@ -1311,7 +1313,6 @@ _notification: renote: "أعد النشر" quote: "الاقتباسات" reaction: "التفاعلات" - pollVote: "مصوِت شارك في الاستطلاع" receiveFollowRequest: "طلبات المتابعة المتلقاة" followRequestAccepted: "طلبات المتابعة المقبولة" groupInvited: "دعوات الفريق" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 593cbb1b32..92659bc28a 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -853,6 +853,7 @@ localOnly: "শুধুমাত্র লোকাল" account: "অ্যাকাউন্টগুলি" like: "পছন্দ করা" show: "প্রদর্শন" +color: "রং" _emailUnavailable: used: "এই ইমেইল ঠিকানাটি ইতোমধ্যে ব্যবহৃত হয়েছে" format: "এই ইমেল ঠিকানাটি সঠিকভাবে লিখা হয়নি" @@ -1200,6 +1201,8 @@ _weekday: friday: "শুক্রবার" saturday: "শনিবার" _widgets: + profile: "প্রোফাইল" + instanceInfo: "ইন্সট্যান্সের তথ্য" memo: "স্টিকি নোট" notifications: "বিজ্ঞপ্তি" timeline: "টাইমলাইন" @@ -1386,7 +1389,6 @@ _notification: youGotReply: "{name} আপনাকে জবাব দিয়েছে" youGotQuote: "{name} আপনাকে উদ্ধৃত করেছে" youRenoted: "{name} এর Renote" - youGotPoll: "{name} আপনার পোলে ভোট দিয়েছে" youGotMessagingMessageFromUser: "{name} আপনাকে মেসেজ করেছে" youGotMessagingMessageFromGroup: "{name} গ্রুপে একটি নতুন মেসেজ আছে" youWereFollowed: "আপনাকে অনুসরণ করছে" @@ -1403,7 +1405,6 @@ _notification: renote: "রিনোট" quote: "উদ্ধৃতি" reaction: "প্রতিক্রিয়া" - pollVote: "পোলে ভোট আছে" pollEnded: "পোল শেষ" receiveFollowRequest: "প্রাপ্ত অনুসরণের অনুরোধসমূহ" followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 5127803ebc..be97b7f60a 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -399,6 +399,8 @@ _antennaSources: userList: "Publicacions d'una llista d'usuaris" userGroup: "Publicacions d'usuaris d'un grup" _widgets: + profile: "Perfil" + instanceInfo: "Informació del fitxer d'instal·lació" notifications: "Notificacions" timeline: "Línia de temps" activity: "Activitat" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 2d80008c47..79f521a80a 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -611,6 +611,7 @@ slow: "Pomalá" fast: "Rychlá" account: "Účty" show: "Zobrazit" +color: "Barva" _ad: back: "Zpět" _gallery: @@ -694,6 +695,8 @@ _weekday: friday: "Pátek" saturday: "Sobota" _widgets: + profile: "Váš profil" + instanceInfo: "Informace o instanci" notifications: "Oznámení" timeline: "Časová osa" calendar: "Kalendář" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index db6bd9ab05..703e40a374 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -920,6 +920,67 @@ like: "Gefällt mir" unlike: "\"Gefällt mir\" entfernen" numberOfLikes: "\"Gefällt mir\"-Anzahl" show: "Anzeigen" +neverShow: "Nicht wieder anzeigen" +remindMeLater: "Vielleicht später" +didYouLikeMisskey: "Gefällt dir Misskey?" +pleaseDonate: "Misskey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!" +roles: "Rollen" +role: "Rolle" +normalUser: "Standardbenutzer" +undefined: "Undefiniert" +assign: "Zuweisen" +unassign: "Entfernen" +color: "Farbe" +manageCustomEmojis: "Benutzerdefinierte Emojis verwalten" +youCannotCreateAnymore: "Du hast das Erstellungslimit erreicht." +_role: + new: "Rolle erstellen" + edit: "Rolle bearbeiten" + name: "Rollenname" + description: "Rollenbeschreibung" + permission: "Rollenberechtigungen" + descriptionOfPermission: "Moderatoren können grundlegende Verwaltungsaufgaben erledigen.\nAdministratoren können alle Einstellungen der Instanz verwalten." + assignTarget: "Zuweisungsart" + descriptionOfAssignTarget: "Manuell bedeutet, dass die Liste der Benutzer einer Rolle manuell verwaltet wird.\nKonditionell bedeutet, dass die Liste der Benutzer einer Rolle durch eine Bedingung automatisch verwaltet wird." + manual: "Manuell" + conditional: "Konditional" + condition: "Bedingung" + isConditionalRole: "Dies ist eine konditionale Rolle." + isPublic: "Öffentliche Rolle" + descriptionOfIsPublic: "Ist dies aktiviert, so kann jeder die Liste der Benutzer, die dieser Rolle zugewiesen sind, einsehen. Zusätzlich wird diese Rolle im Profil zugewiesener Benutzer angezeigt." + options: "Optionen" + baseRole: "Rollenvorlage" + useBaseValue: "Wert der Rollenvorlage verwenden" + chooseRoleToAssign: "Zuzuweisende Rolle auswählen" + canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen" + descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten." + _options: + gtlAvailable: "Kann auf die globale Chronik zugreifen" + ltlAvailable: "Kann auf die lokale Chronik zugreifen" + canPublicNote: "Kann öffentliche Notizen erstellen" + canInvite: "Einladungscodes für diese Instanz erstellen" + canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten" + driveCapacity: "Drive-Kapazität" + pinMax: "Maximale Anzahl an angehefteten Notizen" + antennaMax: "Maximale Anzahl an Antennen" + wordMuteMax: "Maximale Zeichenlänge für Wortstummschaltungen" + webhookMax: "Maximale Anzahl an Webhooks" + clipMax: "Maximale Anzahl an Clips" + noteEachClipsMax: "Maximale Anzahl an Notizen innerhalb eines Clips" + userListMax: "Maximale Anzahl an Benutzern in einer Benutzerliste" + userEachUserListsMax: "Maximale Anzahl an Benutzerlisten" + _condition: + isLocal: "Lokaler Benutzer" + isRemote: "Benutzer fremder Instanz" + createdLessThan: "Kontoerstellung liegt weniger als X zurück" + createdMoreThan: "Kontoerstellung liegt mehr als X zurück" + followersLessThanOrEq: "Hat X oder weniger Follower" + followersMoreThanOrEq: "Hat X oder mehr Follower" + followingLessThanOrEq: "Folgt X oder weniger Benutzern" + followingMoreThanOrEq: "Folgt X oder mehr Benutzern" + and: "UND-Bedingung" + or: "ODER-Bedingung" + not: "NICHT-Bedingung" _sensitiveMediaDetection: description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht." sensitivity: "Erkennungssensitivität" @@ -1298,6 +1359,8 @@ _weekday: friday: "Freitag" saturday: "Samstag" _widgets: + profile: "Profil" + instanceInfo: "Instanzinformationen" memo: "Merkzettel" notifications: "Benachrichtigungen" timeline: "Chronik" @@ -1324,6 +1387,7 @@ _widgets: userList: "Benutzerliste" _userList: chooseList: "Liste auswählen" + clicker: "Klickzähler" _cw: hide: "Inhalt verbergen" show: "Inhalt anzeigen" @@ -1499,7 +1563,6 @@ _notification: youGotReply: "{name} hat dir geantwortet" youGotQuote: "{name} hat dich zitiert" youRenoted: "Renote deiner Notiz von {name}" - youGotPoll: "{name} hat in deiner Umfrage abgestimmt" youGotMessagingMessageFromUser: "{name} hat dir eine Chatnachricht gesendet" youGotMessagingMessageFromGroup: "In die Gruppe {name} wurde eine Chatnachricht gesendet" youWereFollowed: "ist dir gefolgt" @@ -1517,7 +1580,6 @@ _notification: renote: "Renotes" quote: "Zitationen" reaction: "Reaktionen" - pollVote: "Antworten auf Umfragen" pollEnded: "Ende von Umfragen" receiveFollowRequest: "Erhaltene Follow-Anfragen" followRequestAccepted: "Akzeptierte Follow-Anfragen" diff --git a/locales/el-GR.yml b/locales/el-GR.yml index 0267065d9e..974e66c036 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -1,6 +1,6 @@ --- _lang_: "Ελληνικά" -monthAndDay: "{μήνας}/{ημέρα}" +monthAndDay: "{day}/{month}" search: "Αναζήτηση" notifications: "Ειδοποιήσεις" username: "Όνομα μέλους" @@ -343,6 +343,8 @@ _antennaSources: userList: "Σημειώματα από καθορισμένη λίστα μελών" userGroup: "Σημειώματα από μέλη καθορισμένης ομάδας" _widgets: + profile: "Προφίλ" + instanceInfo: "Πληροφορίες του instance" notifications: "Ειδοποιήσεις" timeline: "Χρονολόγιο" calendar: "Ημερολόγιο" diff --git a/locales/en-US.yml b/locales/en-US.yml index e2a7b32be8..93d75ad739 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -920,6 +920,67 @@ like: "Like" unlike: "Unlike" numberOfLikes: "Likes" show: "Show" +neverShow: "Don't show again" +remindMeLater: "Maybe later" +didYouLikeMisskey: "Have you taken a liking to Misskey?" +pleaseDonate: "{host} uses the free software, Misskey. We would highly appreciate your donations so development of Misskey can continue!" +roles: "Roles" +role: "Role" +normalUser: "Normal user" +undefined: "Undefined" +assign: "Assign" +unassign: "Unassign" +color: "Color" +manageCustomEmojis: "Manage Custom Emojis" +youCannotCreateAnymore: "You've hit the creation limit." +_role: + new: "New role" + edit: "Edit role" + name: "Role name" + description: "Role description" + permission: "Role permissions" + descriptionOfPermission: "Moderators can perform basic moderation operations.\nAdministrators can change all settings of the instance." + assignTarget: "Assignment type" + descriptionOfAssignTarget: "Manual to manually change who is part of this role and who is not.\nConditional to have users be automatically assigned and removed from this role based on a condition." + manual: "Manual" + conditional: "Conditional" + condition: "Condition" + isConditionalRole: "This is a conditional role." + isPublic: "Public role" + descriptionOfIsPublic: "Anyone will be able to view a list of users assigned to this role. In addition, this role will be displayed in the profiles of assigned users." + options: "Role options" + baseRole: "Base role" + useBaseValue: "Use base role value" + chooseRoleToAssign: "Select the role to assign" + canEditMembersByModerator: "Allow moderators to edit the list members of this role" + descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users." + _options: + gtlAvailable: "Viewing the global timeline" + ltlAvailable: "Viewing the local timeline" + canPublicNote: "Can send public notes" + canInvite: "Create instance invite codes" + canManageCustomEmojis: "Manage Custom Emojis" + driveCapacity: "Drive capacity" + pinMax: "Maximum number of pinned notes" + antennaMax: "Maximum number of antennas" + wordMuteMax: "Maximum number of characters allowed in word mutes" + webhookMax: "Maximum number of Webhooks" + clipMax: "Maximum number of Clips" + noteEachClipsMax: "Maximum number of notes within a clip" + userListMax: "Maximum number of user lists" + userEachUserListsMax: "Maximum number of users within a user list" + _condition: + isLocal: "Local user" + isRemote: "Remote user" + createdLessThan: "Less than X has passed since account creation" + createdMoreThan: "More than X has passed since account creation" + followersLessThanOrEq: "Has X or fewer followers" + followersMoreThanOrEq: "Has X or more followers" + followingLessThanOrEq: "Follows X or fewer accounts" + followingMoreThanOrEq: "Follows X or more accounts" + and: "AND-Condition" + or: "OR-Condition" + not: "NOT-Condition" _sensitiveMediaDetection: description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server." sensitivity: "Detection sensitivity" @@ -1298,6 +1359,8 @@ _weekday: friday: "Friday" saturday: "Saturday" _widgets: + profile: "Profile" + instanceInfo: "Instance Information" memo: "Sticky notes" notifications: "Notifications" timeline: "Timeline" @@ -1324,6 +1387,7 @@ _widgets: userList: "User list" _userList: chooseList: "Select a list" + clicker: "Clicker" _cw: hide: "Hide" show: "Show content" @@ -1499,7 +1563,6 @@ _notification: youGotReply: "{name} replied to you" youGotQuote: "{name} quoted you" youRenoted: "Renote from {name}" - youGotPoll: "{name} voted on your poll" youGotMessagingMessageFromUser: "{name} sent you a chat message" youGotMessagingMessageFromGroup: "A chat message was sent to the {name} group" youWereFollowed: "followed you" @@ -1517,7 +1580,6 @@ _notification: renote: "Renotes" quote: "Quotes" reaction: "Reactions" - pollVote: "Votes on polls" pollEnded: "Polls ending" receiveFollowRequest: "Received follow requests" followRequestAccepted: "Accepted follow requests" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index c328737c4b..f246d7b21c 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -918,6 +918,7 @@ cannotLoad: "No se puede cargar." numberOfProfileView: "Número de vistas de perfil" like: "¡Muy bien!" show: "Apariencia" +color: "Color" _sensitiveMediaDetection: description: "Reduce el esfuerzo de la moderación el el servidor a través del reconocimiento automático de contenido NSFW usando 'Machine Learning'. Esto puede incrementar ligeramente la carga en el servidor." sensitivity: "Sensibilidad de detección" @@ -1296,6 +1297,8 @@ _weekday: friday: "Viernes" saturday: "Sábado" _widgets: + profile: "Perfil" + instanceInfo: "información de la instancia" memo: "Nota adhesiva" notifications: "Notificaciones" timeline: "Linea de tiempo" @@ -1487,7 +1490,6 @@ _notification: youGotReply: "Respuesta de {name}" youGotQuote: "Citado por {name}" youRenoted: "Renotado por {name}" - youGotPoll: "Encuestado por {name}" youGotMessagingMessageFromUser: "{name} comenzó un chat contigo" youGotMessagingMessageFromGroup: "Tienes un chat de {name}" youWereFollowed: "te ha seguido" @@ -1505,7 +1507,6 @@ _notification: renote: "Renotar" quote: "Citar" reaction: "Reacción" - pollVote: "Votado en la encuesta" pollEnded: "La encuesta terminó" receiveFollowRequest: "Recibió una solicitud de seguimiento" followRequestAccepted: "El seguimiento fue aceptado" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 0d7399533d..ac0429865c 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -911,7 +911,11 @@ loggedInAsBot: "Connecté actuellement en tant que bot" tools: "Outils" cannotLoad: "Chargement impossible" like: "J'aime" +numberOfLikes: "Favoris" show: "Affichage" +neverShow: "Ne plus afficher" +remindMeLater: "Peut-être plus tard" +color: "Couleur" _sensitiveMediaDetection: description: "L'apprentissage automatique peut être utilisé pour détecter automatiquement les médias sensibles à modérer. La sollicitation des serveurs augmente légèrement." sensitivity: "Sensibilité de la détection" @@ -1289,6 +1293,8 @@ _weekday: friday: "Vendredi" saturday: "Samedi" _widgets: + profile: "Profil" + instanceInfo: "Informations sur l’instance" memo: "Note collante" notifications: "Notifications" timeline: "Fil" @@ -1478,7 +1484,6 @@ _notification: youGotReply: "Réponse de {name}" youGotQuote: "Cité·e par {name}" youRenoted: "{name} vous a Renoté" - youGotPoll: "{name} a participé à votre sondage" youGotMessagingMessageFromUser: "{name} vous envoyé un message" youGotMessagingMessageFromGroup: "Un message a été envoyé au groupe {name}" youWereFollowed: "Vous suit" @@ -1496,7 +1501,6 @@ _notification: renote: "Renotes" quote: "Citations" reaction: "Réactions" - pollVote: "Votes dans des sondages" pollEnded: "Sondages se cloturant" receiveFollowRequest: "Demande d'abonnement reçue" followRequestAccepted: "Demande d'abonnement acceptée" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 3a2bf69a72..bc88f9e8f5 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -859,6 +859,7 @@ like: "Suka" unlike: "Tidak Suka" numberOfLikes: "Jumlah yang disukai" show: "Tampilkan" +color: "Warna" _emailUnavailable: used: "Alamat surel ini telah digunakan" format: "Format tidak valid." @@ -1206,6 +1207,8 @@ _weekday: friday: "Jumat" saturday: "Sabtu" _widgets: + profile: "Profil" + instanceInfo: "Informasi Instansi" memo: "Catatan memo" notifications: "Pemberitahuan" timeline: "Linimasa" @@ -1402,7 +1405,6 @@ _notification: youGotReply: "{name} membalas kamu" youGotQuote: "{name} mengutip kamu" youRenoted: "{name} me-renote kamu" - youGotPoll: "{name} memilih di angket kamu" youGotMessagingMessageFromUser: "{name} mengirimi kamu pesan" youGotMessagingMessageFromGroup: "Sebuah pesan telah dikirim ke grup {name}" youWereFollowed: "Mengikuti kamu" @@ -1419,7 +1421,6 @@ _notification: renote: "Renote" quote: "Kutip" reaction: "Reaksi" - pollVote: "Memilih di angket" pollEnded: "Jajak pendapat berakhir" receiveFollowRequest: "Permintaan mengikuti diterima" followRequestAccepted: "Permintaan mengikuti disetujui" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 3fba19985e..3251e9877c 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -28,7 +28,7 @@ timeline: "Timeline" noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo." login: "Accedi" loggingIn: "Accesso in corso..." -logout: "Esci" +logout: "Uscita" signup: "Iscriviti" uploading: "Caricamento..." save: "Salva" @@ -109,7 +109,7 @@ you: "Tu" clickToShow: "Clicca per visualizzare" sensitive: "Contenuto sensibile" add: "Aggiungi" -reaction: "Reazione" +reaction: "Reazioni" reactionSetting: "Reazioni visualizzate sul pannello" reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere." rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note" @@ -226,7 +226,7 @@ currentPassword: "Password attuale" newPassword: "Nuova Password" newPasswordRetype: "Conferma password" attachFile: "Allega file" -more: "Altri!" +more: "Di più!" featured: "Tendenze" usernameOrUserId: "Nome utente o ID utente" noSuchUser: "Nessun utente trovato" @@ -512,7 +512,7 @@ newNoteRecived: "Vedi le nuove note" sounds: "Impostazioni suoni" sound: "Impostazioni suoni" listen: "Ascolta" -none: "Niente" +none: "Nessuno" showInPage: "Visualizza in pagina" popout: "Finestra pop-out" volume: "Volume" @@ -578,7 +578,7 @@ useFullReactionPicker: "Usa la totalità del pannello di reazioni" width: "Larghezza" height: "Altezza" large: "Grande" -medium: "Predefinito" +medium: "Medio" small: "Piccolo" generateAccessToken: "Genera token di accesso" permission: "Autorizzazioni " @@ -649,7 +649,7 @@ instanceTicker: "Informazioni sull'istanza da cui vengono le note" waitingFor: "Aspettando {x}" random: "Casuale" system: "Sistema" -switchUi: "Cambiare interfaccia utente" +switchUi: "Cambiare interfaccia" desktop: "Desktop" clip: "Nota" createNew: "Crea" @@ -799,7 +799,7 @@ received: "Ricevuto" searchResult: "Risultati della Ricerca" hashtags: "Hashtag" troubleshooting: "Risoluzione problemi" -useBlurEffect: "Utilizza effetto sfocatura per l'interfaccia utente" +useBlurEffect: "Utilizza effetto sfocatura nell'interfaccia" learnMore: "Più dettagli" misskeyUpdated: "Misskey è stato aggiornato!" whatIsNew: "Visualizza le informazioni sull'aggiornamento" @@ -917,7 +917,58 @@ tools: "Strumenti" cannotLoad: "Caricamento impossibile" numberOfProfileView: "Visualizzazioni profilo" like: "Mi piace!" +unlike: "Non mi piace" +numberOfLikes: "Numero di Like" show: "Visualizza" +neverShow: "Non mostrare più" +remindMeLater: "Rimanda" +didYouLikeMisskey: "Ti piace Misskey?" +pleaseDonate: "Misskey è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!" +roles: "Ruoli" +role: "Ruolo" +normalUser: "Profilo standard" +undefined: "Indefinito" +assign: "Assegna" +unassign: "Disassegna" +color: "Colore" +manageCustomEmojis: "Gestisci le emoji personalizzate" +_role: + new: "Nuovo ruolo" + edit: "Modifica ruolo" + name: "Nome del ruolo" + description: "Descrizione del ruolo" + permission: "Permessi del ruolo" + descriptionOfPermission: "Moderatori possono svolgere le attività di moderazione basilari.\nAmministratori possono modificare la configurazione dell'istanza." + assignTarget: "Assegna il target" + descriptionOfAssignTarget: "Manuale per assegnare manualmente questo ruolo ai profili.\nCondizionale per assegnare o rimuovere automaticamente questo ruolo ai profili, secondo determinate condizioni." + manual: "Manuale" + conditional: "Condizionale" + condition: "Condizioni" + isConditionalRole: "Questo è un ruolo condizionato" + isPublic: "Ruolo pubblico" + descriptionOfIsPublic: "La lista di profili assegnati a questo ruolo è visibile a chiunque. Inoltre, il ruolo verrà mostrato nei relativi profili." + options: "Opzioni del ruolo" + baseRole: "Ruolo di base" + useBaseValue: "Eredita dal ruolo base" + chooseRoleToAssign: "Seleziona il ruolo da assegnare" + canEditMembersByModerator: "Consenti ai Moderatori di modificare i membri di questo ruolo" + descriptionOfCanEditMembersByModerator: "Se attivo, anche i Moderatori potranno assegnare o togliere questo ruolo. Altrimenti, se disattivo, potranno solo gli Amministratori." + _options: + gtlAvailable: "Disponibilità della Timeline Federata" + ltlAvailable: "Disponibilità della Timeline Locale" + canPublicNote: "Può scrivere Note con Visibilità Pubblica" + canInvite: "Genera codici di invito all'istanza" + canManageCustomEmojis: "Gestire le emoji personalizzate" + driveCapacity: "Capienza del Drive" + antennaMax: "Numero massimo di Antenne" + _condition: + isLocal: "Profilo locale" + isRemote: "Profilo remoto" + createdLessThan: "Creato meno di" + createdMoreThan: "Creato più di" + and: "E" + or: "O" + not: "NON" _sensitiveMediaDetection: description: "L'apprendimento automatico può essere utilizzato per individuare automaticamente i media sensibili da moderare. Il carico del server aumenta leggermente." sensitivity: "Sensibilità di rilevamento" @@ -1090,9 +1141,9 @@ _channel: usersCount: "{n} partecipanti" notesCount: "{n} note" _menuDisplay: - sideFull: "laro" - sideIcon: "Orizzontale (icona)" - top: "superficie" + sideFull: "Laterale" + sideIcon: "Laterale (solo icone)" + top: "In alto" hide: "Nascondere" _wordMute: muteWords: "Parole da filtrare" @@ -1194,10 +1245,10 @@ _ago: future: "Futuro" justNow: "Ora" secondsAgo: "{n}s fa" - minutesAgo: "{n}min fa" + minutesAgo: "{n} min fa" hoursAgo: "{n} ore fa" - daysAgo: "{n} giorni fa" - weeksAgo: "{n} settimane fa" + daysAgo: "{n} gg fa" + weeksAgo: "{n} sett. fa" monthsAgo: "{n} mesi fa" yearsAgo: "{n} anni fa" _time: @@ -1296,6 +1347,8 @@ _weekday: friday: "Venerdì" saturday: "Sabato" _widgets: + profile: "Profilo" + instanceInfo: "Informazioni sull'istanza" memo: "Promemoria" notifications: "Notifiche" timeline: "Timeline" @@ -1317,10 +1370,12 @@ _widgets: jobQueue: "Coda di lavoro" serverMetric: "Statistiche server" aiscript: "Console AiScript" + aiscriptApp: "App AiScript" aichan: "Mascotte Ai" userList: "Elenco utenti" _userList: chooseList: "Seleziona una lista" + clicker: "Cliccaggio" _cw: hide: "Nascondere" show: "Mostra di più" @@ -1423,7 +1478,16 @@ _timelines: social: "Sociale" global: "Federata" _play: + new: "Crea un Play" + edit: "Modifica i Play" + created: "Il Play è stato creato" + updated: "Il Play è stato aggiornato" + deleted: "Il Play è stato eliminato" + pageSetting: "Impostazioni di Play" + editThisPage: "Modifica il Play" viewSource: "Visualizza sorgente" + my: "I miei Play" + liked: "Play piaciuti" featured: "Popolari" title: "Titolo" script: "Script" @@ -1487,7 +1551,6 @@ _notification: youGotReply: "{name} ti ha risposto" youGotQuote: "{name} ha citato il tuo Nota e ha detto" youRenoted: "{name} ha rinotato" - youGotPoll: "{name} ha votato" youGotMessagingMessageFromUser: "{name} ti ha mandato un messaggio" youGotMessagingMessageFromGroup: "{name} ti ha mandato un messaggio nella chat" youWereFollowed: "Ha iniziato a seguirti" @@ -1505,7 +1568,6 @@ _notification: renote: "Rinota" quote: "Cita" reaction: "Reazioni" - pollVote: "Voti ricevuti" pollEnded: "Sondaggio chiuso." receiveFollowRequest: "Richiesta di follow ricevuta" followRequestAccepted: "Richiesta di follow accettata" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b49d872a0b..a0802dd68c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -193,7 +193,7 @@ clearQueueConfirmText: "未配達の投稿は配送されなくなります。 clearCachedFiles: "キャッシュをクリア" clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?" blockedInstances: "ブロックしたインスタンス" -blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。" +blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。サブドメインもブロックされます。" muteAndBlock: "ミュートとブロック" mutedUsers: "ミュートしたユーザー" blockedUsers: "ブロックしたユーザー" @@ -924,6 +924,68 @@ neverShow: "今後表示しない" remindMeLater: "また後で" didYouLikeMisskey: "Misskeyを気に入っていただけましたか?" pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!" +roles: "ロール" +role: "ロール" +normalUser: "一般ユーザー" +undefined: "未定義" +assign: "アサイン" +unassign: "アサインを解除" +color: "色" +manageCustomEmojis: "カスタム絵文字の管理" +youCannotCreateAnymore: "これ以上作成することはできません。" +cannotPerformTemporary: "一時的に利用できません" +cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。" + +_role: + new: "ロールの作成" + edit: "ロールの編集" + name: "ロール名" + description: "ロールの説明" + permission: "ロールの権限" + descriptionOfPermission: "モデレーターは基本的なモデレーションに関する操作を行えます。\n管理者はインスタンスの全ての設定を変更できます。" + assignTarget: "アサインターゲット" + descriptionOfAssignTarget: "マニュアルは誰がこのロールに含まれるかを手動で管理します。\nコンディショナルは条件を設定し、それに合致するユーザーが自動で含まれるようになります。" + manual: "マニュアル" + conditional: "コンディショナル" + condition: "条件" + isConditionalRole: "これはコンディショナルロールです。" + isPublic: "ロールを公開" + descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができます。また、ユーザーのプロフィールでこのロールが表示されます。" + options: "オプション" + baseRole: "ベースロール" + useBaseValue: "ベースロールの値を使用" + chooseRoleToAssign: "アサインするロールを選択" + canEditMembersByModerator: "モデレーターのメンバー編集を許可" + descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。" + _options: + gtlAvailable: "グローバルタイムラインの閲覧" + ltlAvailable: "ローカルタイムラインの閲覧" + canPublicNote: "パブリック投稿の許可" + canInvite: "インスタンス招待コードの発行" + canManageCustomEmojis: "カスタム絵文字の管理" + driveCapacity: "ドライブ容量" + pinMax: "ノートのピン留めの最大数" + antennaMax: "アンテナの作成可能数" + wordMuteMax: "ワードミュートの最大文字数" + webhookMax: "Webhookの作成可能数" + clipMax: "クリップの作成可能数" + noteEachClipsMax: "クリップ内のノートの最大数" + userListMax: "ユーザーリストの作成可能数" + userEachUserListsMax: "ユーザーリスト内のユーザーの最大数" + rateLimitFactor: "レートリミット" + descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。" + _condition: + isLocal: "ローカルユーザー" + isRemote: "リモートユーザー" + createdLessThan: "アカウント作成から~以内" + createdMoreThan: "アカウント作成から~経過" + followersLessThanOrEq: "フォロワー数が~以下" + followersMoreThanOrEq: "フォロワー数が~以上" + followingLessThanOrEq: "フォロー数が~以下" + followingMoreThanOrEq: "フォロー数が~以上" + and: "~かつ~" + or: "~または~" + not: "~ではない" _sensitiveMediaDetection: description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。" @@ -1335,6 +1397,8 @@ _weekday: saturday: "土曜日" _widgets: + profile: "プロフィール" + instanceInfo: "インスタンス情報" memo: "付箋" notifications: "通知" timeline: "タイムライン" @@ -1361,6 +1425,7 @@ _widgets: userList: "ユーザーリスト" _userList: chooseList: "リストを選択" + clicker: "クリッカー" _cw: hide: "隠す" @@ -1550,7 +1615,6 @@ _notification: youGotReply: "{name}からのリプライ" youGotQuote: "{name}による引用" youRenoted: "{name}がRenoteしました" - youGotPoll: "{name}が投票しました" youGotMessagingMessageFromUser: "{name}からのチャットがあります" youGotMessagingMessageFromGroup: "{name}のチャットがあります" youWereFollowed: "フォローされました" @@ -1569,7 +1633,6 @@ _notification: renote: "Renote" quote: "引用" reaction: "リアクション" - pollVote: "アンケートに投票された" pollEnded: "アンケートが終了" receiveFollowRequest: "フォロー申請を受け取った" followRequestAccepted: "フォローが受理された" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index f8c045db00..cc40d6c4c8 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -915,8 +915,40 @@ caption: "キャプション" loggedInAsBot: "Botアカウントでログイン中やで" tools: "ツール" cannotLoad: "読み込めへんで" +numberOfProfileView: "プロフィール表示回数" like: "ええやん!" +unlike: "いいねを解除" +numberOfLikes: "いいね数" show: "表示" +neverShow: "今後表示しない" +remindMeLater: "また後で" +didYouLikeMisskey: "Misskeyを気に入っとっただけましたん?" +pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。" +roles: "ロール" +role: "ロール" +undefined: "未定義" +assign: "アサイン" +unassign: "アサインを解除" +color: "色" +_role: + new: "ロールの作成" + edit: "ロールの編集" + name: "ロール名" + description: "ロールの説明" + isPublic: "ロールを公開" + descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができるで。そんで、ユーザーのプロフィールでこのロールが表示されるで。" + options: "オプション" + baseRole: "ベースロール" + useBaseValue: "ベースロールの値を使用" + chooseRoleToAssign: "アサインするロールを選択" + canEditMembersByModerator: "モデレーターのメンバー編集を許可" + descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになるで。オフにすると管理者のみが行えるで。" + _options: + gtlAvailable: "グローバルタイムラインの閲覧" + ltlAvailable: "ローカルタイムラインの閲覧" + canPublicNote: "パブリック投稿の許可" + driveCapacity: "ドライブ容量" + antennaMax: "アンテナの作成可能数" _sensitiveMediaDetection: description: "機械学習を使って自動でセンシティブなメディアを検出して、モデレーションに役立てることができるで。サーバーの負荷が少し増えてまうなあ。" sensitivity: "検出感度やで" @@ -1295,6 +1327,8 @@ _weekday: friday: "金曜日" saturday: "土曜日" _widgets: + profile: "プロフィール" + instanceInfo: "インスタンス情報" memo: "付箋" notifications: "通知" timeline: "タイムライン" @@ -1316,10 +1350,12 @@ _widgets: jobQueue: "ジョブキュー" serverMetric: "サーバーメトリクス" aiscript: "AiScriptコンソール" + aiscriptApp: "AiScript App" aichan: "藍" userList: "ユーザーリスト" _userList: chooseList: "リストを選ぶ" + clicker: "クリッカー" _cw: hide: "隠す" show: "続き見して!" @@ -1383,6 +1419,7 @@ _profile: changeBanner: "バナー画像を変更するで" _exportOrImport: allNotes: "全てのノート" + favoritedNotes: "お気に入りにしたノート" followingList: "フォロー" muteList: "ミュート" blockingList: "ブロック" @@ -1421,7 +1458,16 @@ _timelines: social: "ソーシャル" global: "グローバル" _play: + new: "Playの作成" + edit: "Playの編集" + created: "Playを作ったで" + updated: "Playを更新したで" + deleted: "Playを消したで" + pageSetting: "Play設定" + editThisPage: "このPlayを編集" viewSource: "ソースを表示" + my: "自分のPlay" + liked: "いいねしたPlay" featured: "人気" title: "タイトル" script: "スクリプト" @@ -1485,7 +1531,6 @@ _notification: youGotReply: "{name}からのリプライ" youGotQuote: "{name}による引用" youRenoted: "{name}がRenoteしたみたいやで" - youGotPoll: "{name}が投票したみたいやで" youGotMessagingMessageFromUser: "{name}からのチャットがあるで" youGotMessagingMessageFromGroup: "{name}のチャットがあるで" youWereFollowed: "フォローされたで" @@ -1503,7 +1548,6 @@ _notification: renote: "Renote" quote: "引用" reaction: "リアクション" - pollVote: "アンケートに投票されたで" pollEnded: "アンケートが終了したで" receiveFollowRequest: "フォロー許可してほしいみたいやで" followRequestAccepted: "フォローが受理されたで" diff --git a/locales/kab-KAB.yml b/locales/kab-KAB.yml index ea5a4be4fa..7c2e3a065e 100644 --- a/locales/kab-KAB.yml +++ b/locales/kab-KAB.yml @@ -73,6 +73,7 @@ _sfx: _permissions: "write:account": "Ẓreg talɣut n umiḍan-ik·im" _widgets: + profile: "Amaɣnu" notifications: "Ilɣuyen" _userList: chooseList: "Fren tabdart" diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml index a38d9267b1..55b72d3a6e 100644 --- a/locales/kn-IN.yml +++ b/locales/kn-IN.yml @@ -69,6 +69,7 @@ _mfm: _sfx: notification: "ಅಧಿಸೂಚನೆಗಳು" _widgets: + profile: "ಪ್ರೊಫೈಲು" notifications: "ಅಧಿಸೂಚನೆಗಳು" timeline: "ಸಮಯಸಾಲು" _cw: diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index d3a4a40b49..dfcfbca136 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -15,7 +15,7 @@ gotIt: "알겠어요" cancel: "취소" noThankYou: "나중에" enterUsername: "유저명 입력" -renotedBy: "{user}님의 리노트" +renotedBy: "{user}님이 리노트" noNotes: "노트가 없습니다" noNotifications: "표시할 알림이 없습니다" instance: "인스턴스" @@ -907,7 +907,7 @@ subscribePushNotification: "푸시 알림 켜기" unsubscribePushNotification: "푸시 알림 끄기" pushNotificationAlreadySubscribed: "푸시 알림이 이미 켜져 있습니다" pushNotificationNotSupported: "브라우저나 인스턴스에서 푸시 알림이 지원되지 않습니다" -sendPushNotificationReadMessage: "푸시 알림이니 메시지를 읽으면 푸시 알림을 삭제합니다" +sendPushNotificationReadMessage: "푸시 알림이나 메시지를 읽은 뒤 푸시 알림을 삭제" sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」이라는 알림이 잠깐 표시됩니다. 기기의 전력 소비량이 증가할 수 있습니다." windowMaximize: "최대화" windowRestore: "복구" @@ -920,6 +920,67 @@ like: "좋아요!" unlike: "좋아요 취소" numberOfLikes: "좋아요 수" show: "표시" +neverShow: "다시 보지 않기" +remindMeLater: "나중에 알림" +didYouLikeMisskey: "Misskey가 마음에 드시나요?" +pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!" +roles: "역할" +role: "역할" +normalUser: "일반 사용자" +undefined: "정의되지 않음" +assign: "할당" +unassign: "할당 취소" +color: "색" +manageCustomEmojis: "커스텀 이모지 관리" +youCannotCreateAnymore: "더 이상 생성할 수 없습니다." +_role: + new: "새 역할 생성" + edit: "역할 수정" + name: "역할 이름" + description: "역할 설명" + permission: "역할의 권한" + descriptionOfPermission: "모더레이터는 기본적인 중재와 관련된 작업을 수행할 수 있습니다.\n관리자는 인스턴스의 모든 설정을 변경할 수 있습니다." + assignTarget: "할당 대상" + descriptionOfAssignTarget: "수동을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n조건부를 선택하면 조건을 설정해 일치하는 사용자를 자동으로 포함되게 할 수 있습니다." + manual: "수동" + conditional: "조건부" + condition: "조건" + isConditionalRole: "조건부 역할입니다." + isPublic: "공개 역할" + descriptionOfIsPublic: "역할에 할당된 사용자를 누구나 볼 수 있습니다. 또한 사용자 프로필에 이 역할이 표시됩니다." + options: "옵션" + baseRole: "기본 역할" + useBaseValue: "기본값 사용" + chooseRoleToAssign: "할당할 역할 선택" + canEditMembersByModerator: "모더레이터의 역할 수정 허용" + descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 추가하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 가능합니다." + _options: + gtlAvailable: "글로벌 타임라인 보이기" + ltlAvailable: "로컬 타임라인 보이기" + canPublicNote: "공개 노트 허용" + canInvite: "인스턴스 초대 코드 발행" + canManageCustomEmojis: "커스텀 이모지 관리" + driveCapacity: "드라이브 용량" + pinMax: "고정할 수 있는 노트 수" + antennaMax: "최대 안테나 생성 허용 수" + wordMuteMax: "뮤트할 수 있는 단어의 수" + webhookMax: "생성할 수 있는 WebHook의 수" + clipMax: "생성할 수 있는 클립 수" + noteEachClipsMax: "각 클립에 추가할 수 있는 노트 수" + userListMax: "생성할 수 있는 리스트 수" + userEachUserListsMax: "리스트당 최대 사용자 수" + _condition: + isLocal: "로컬 사용자" + isRemote: "리모트 사용자" + createdLessThan: "다음 일수 이내에 가입한 유저" + createdMoreThan: "다음 일수 이상 활동한 유저" + followersLessThanOrEq: "팔로워 수가 다음 이하인 유저" + followersMoreThanOrEq: "팔로워 수가 다음 이상인 유저" + followingLessThanOrEq: "팔로잉 수가 다음 이하인 유저" + followingMoreThanOrEq: "팔로잉 수가 다음 이상인 유저" + and: "다음을 모두 만족" + or: "다음을 하나라도 만족" + not: "다음을 만족하지 않음" _sensitiveMediaDetection: description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다." sensitivity: "탐지 민감도" @@ -1298,6 +1359,8 @@ _weekday: friday: "금요일" saturday: "토요일" _widgets: + profile: "프로필" + instanceInfo: "인스턴스 정보" memo: "스티커 메모" notifications: "알림" timeline: "타임라인" @@ -1321,9 +1384,10 @@ _widgets: aiscript: "AiScript 콘솔" aiscriptApp: "AiScript 앱" aichan: "아이" - userList: "사용자 목록" + userList: "유저 리스트" _userList: chooseList: "리스트 선택" + clicker: "클리커" _cw: hide: "숨기기" show: "더 보기" @@ -1499,7 +1563,6 @@ _notification: youGotReply: "{name}님이 답글함" youGotQuote: "{name}님이 인용함" youRenoted: "{name}님이 Renote" - youGotPoll: "{name}님이 투표함" youGotMessagingMessageFromUser: "{name} 님이 보낸 채팅이 있어요" youGotMessagingMessageFromGroup: "{name}에서 보낸 채팅이 있어요" youWereFollowed: "새로운 팔로워가 있습니다" @@ -1517,7 +1580,6 @@ _notification: renote: "리노트" quote: "인용" reaction: "리액션" - pollVote: "투표 참여" pollEnded: "투표가 종료됨" receiveFollowRequest: "팔로우 요청을 받았을 때" followRequestAccepted: "팔로우 요청이 승인되었을 때" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 93ed3fa7e2..5735c6322a 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -440,6 +440,8 @@ _sfx: notification: "Meldingen" chat: "Chat" _widgets: + profile: "Profiel" + instanceInfo: "Serverinformatie" notifications: "Meldingen" timeline: "Tijdlijn" activity: "Activiteit" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 712c05bb78..bc66e43bd8 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -868,6 +868,7 @@ sendPushNotificationReadMessageCaption: "Chwilowo pojawi się powiadomienie \"{e loggedInAsBot: "Jesteś obecnie zalogowany/a jako bot" like: "Polub" show: "Wyświetlanie" +color: "Kolor" _sensitiveMediaDetection: description: "Zmniejsza wysiłek związany z moderacją serwera dzięki automatycznemu rozpoznawaniu zawartości NSFW za pomocą uczenia maszynowego. To nieznacznie zwiększy obciążenie serwera." setSensitiveFlagAutomatically: "Oznacz jako NSFW" @@ -1213,6 +1214,8 @@ _weekday: friday: "Piątek" saturday: "Sobota" _widgets: + profile: "Profil" + instanceInfo: "Informacje o instancji" memo: "Przypięte notatki" notifications: "Powiadomienia" timeline: "Oś czasu" @@ -1380,7 +1383,6 @@ _notification: youGotReply: "{name} odpowiedział(a) Tobie" youGotQuote: "{name} zacytował(a) Ciebie" youRenoted: "{name} udostępnił(a) Twój wpis" - youGotPoll: "{name} zagłosował(a) w Twojej ankiecie" youGotMessagingMessageFromUser: "{name} wysłał(a) Ci wiadomość" youGotMessagingMessageFromGroup: "Została wysłana wiadomość do grupy {name}" youWereFollowed: "Zaobserwował(a) Cię" @@ -1398,7 +1400,6 @@ _notification: renote: "Udostępnij" quote: "Cytuj" reaction: "Reakcja" - pollVote: "Głosy w ankietach" receiveFollowRequest: "Otrzymano prośbę o możliwość obserwacji" followRequestAccepted: "Przyjęto prośbę o możliwość obserwacji" groupInvited: "Zaproszono do grup" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index dd1c2954b7..c8dc097236 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -488,6 +488,8 @@ _sfx: notification: "Notificações" chat: "Chat" _widgets: + profile: "Perfil" + instanceInfo: "Informações da instância" notifications: "Notificações" timeline: "Timeline" activity: "atividade" @@ -524,7 +526,6 @@ _notification: 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" @@ -541,7 +542,6 @@ _notification: 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" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 3c85045e5d..8593fe29ef 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -667,6 +667,8 @@ _sfx: notification: "Notificări" chat: "Chat" _widgets: + profile: "Profil" + instanceInfo: "Informații despre instanță" notifications: "Notificări" timeline: "Cronologie" activity: "Activitate" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 553fa9e811..a6cbf28efd 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -866,6 +866,7 @@ windowMaximize: "Развернуть" windowRestore: "Восстановить" like: "Нравится!" show: "Отображение" +color: "Цвет" _sensitiveMediaDetection: description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно." setSensitiveFlagAutomatically: "Установить флаг NSFW" @@ -1213,6 +1214,8 @@ _weekday: friday: "Пятница" saturday: "Суббота" _widgets: + profile: "Профиль" + instanceInfo: "Информация об инстансе" memo: "Напоминания" notifications: "Уведомления" timeline: "Лента" @@ -1399,7 +1402,6 @@ _notification: youGotReply: "{name} отвечает вам." youGotQuote: "{name} цитирует вас." youRenoted: "{name} передаёт вашу заметку." - youGotPoll: "{name} участвует в вашем опросе." youGotMessagingMessageFromUser: "{name} пишет вам." youGotMessagingMessageFromGroup: "Новое сообщение в группе «{name}»." youWereFollowed: "У вас новый подписчик." @@ -1414,7 +1416,6 @@ _notification: renote: "Репосты" quote: "Цитаты" reaction: "Реакции" - pollVote: "Голосования" receiveFollowRequest: "Получен запрос на подписку" followRequestAccepted: "Запрос на подписку одобрен" groupInvited: "Приглашение в группы" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 3abfd8609d..47ff1805ad 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -913,6 +913,11 @@ tools: "Nástroje" cannotLoad: "Nedá sa načítať." like: "Páči sa mi" show: "Zobraziť" +neverShow: "Nabudúce nezobrazovať" +remindMeLater: "Pripomenúť neskôr" +didYouLikeMisskey: "Páči sa vám Misskey?" +pleaseDonate: "Misskey je bezplatný softvér, ktorý používa {host}. Prosím, prispejte, aby sme ho mohli ďalej rozvíjať!" +color: "Farba" _sensitiveMediaDetection: description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera." sensitivity: "Citlivosť detekcie" @@ -1291,6 +1296,8 @@ _weekday: friday: "Piatok" saturday: "Sobota" _widgets: + profile: "Profil" + instanceInfo: "Informácie o serveri" memo: "Prilepené poznámky" notifications: "Oznámenia" timeline: "Časová os" @@ -1480,7 +1487,6 @@ _notification: youGotReply: "{name} vám odpovedal/a" youGotQuote: "{name} vás citoval/a" youRenoted: "{name} preposlal/a vašu poznámku" - youGotPoll: "{name} hlasoval/a" youGotMessagingMessageFromUser: "{name} vám poslal/a správu" youGotMessagingMessageFromGroup: "Prišla správa do skupiny {name}" youWereFollowed: "Máte nového sledujúceho" @@ -1498,7 +1504,6 @@ _notification: renote: "Preposlať" quote: "Citovať" reaction: "Reakcie" - pollVote: "Hlasy v hlasovaniach" pollEnded: "Hlasovanie skončilo" receiveFollowRequest: "Doručené žiadosti o sledovanie" followRequestAccepted: "Schválené žiadosti o sledovanie" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index 8b87e36acd..b2647c9689 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -1,7 +1,7 @@ --- _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!🚀" +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 ny värld! 🚀" poweredByMisskeyDescription: "{name} är en tjänst driven av den öppna källkodsplatformen Misskey (benämns \"Misskey instans\")." monthAndDay: "{day}/{month}" search: "Sök" @@ -17,7 +17,7 @@ noThankYou: "Nej tack" enterUsername: "Ange användarnamn" renotedBy: "Omnoterad av {user}" noNotes: "Inga noteringar" -noNotifications: "Inga aviseringar" +noNotifications: "Inga notifikationer" instance: "Instanser" settings: "Inställningar" basicSettings: "Basinställningar" @@ -30,13 +30,13 @@ login: "Logga in" loggingIn: "Loggar in" logout: "Logga ut" signup: "Registrera" -uploading: "Uppladdning sker..." +uploading: "Laddar upp..." save: "Spara" users: "Användare" addUser: "Lägg till användare" favorite: "Lägg till i favoriter" favorites: "Favoriter" -unfavorite: "Avfavorisera" +unfavorite: "Ta bort från favoriter" favorited: "Tillagd i favoriter." alreadyFavorited: "Redan tillagd i favoriter." cantFavorite: "Gick inte att lägga till i favoriter." @@ -146,7 +146,7 @@ flagAsBotDescription: "Aktivera det här alternativet om kontot är kontrollerat 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." +flagShowTimelineRepliesDescription: "Visar användarsvar till andra användares noter i tidslinjen om aktiverad." autoAcceptFollowed: "Godkänn följarförfrågningar från användare du följer automatiskt" addAccount: "Lägg till konto" loginFailed: "Inloggningen misslyckades" @@ -253,16 +253,120 @@ explore: "Utforska" messageRead: "Läs" noMoreHistory: "Det finns ingen mer historik" startMessaging: "Starta en chatt" +nUsersRead: "läst av {n}" +agreeTo: "Jag accepterar {0}" +tos: "Användarvillkor" +home: "Hem" +remoteUserCaution: "Då denna användaren kommer från en fjärrinstans, kan informationen visad vara ofullständig." +activity: "Aktivitet" +images: "Bilder" +birthday: "Födelsedag" +yearsOld: "{age} år gammal" +registeredDate: "Gick med" +location: "Plats" +theme: "Teman" +themeForLightMode: "Tema att använda i Ljust Läge" +themeForDarkMode: "Tema att använda i Mörkt Läge" +light: "Ljust" +dark: "Mörk" +lightThemes: "Ljusa teman" +darkThemes: "Mörka teman" +syncDeviceDarkMode: "Synka Mörkt Läge med din enhets inställningar" +drive: "Drive" +fileName: "Filnamn" +selectFile: "Välj en fil" +selectFiles: "Välj filer" +selectFolder: "Välj en mapp" +selectFolders: "Välj mappar" +renameFile: "Byt namn på filen" +folderName: "Mappnamn" +createFolder: "Skapa en mapp" +renameFolder: "Byt namn på mappen" +deleteFolder: "Ta bort mappen" +addFile: "Lägg till fil" +emptyDrive: "Din Drive är tom" +emptyFolder: "Denna mappen är tom" +unableToDelete: "Kunde inte ta bort" +inputNewFileName: "Ange nytt filnamn" +inputNewDescription: "Ange ny bildtext" +inputNewFolderName: "Ange nytt mappnamn" +circularReferenceFolder: "Destinationsmappen är en undermapp av mappen du vill flytta." +hasChildFilesOrFolders: "Då denna mappen inte är tom, kan den inte tas bort." +copyUrl: "Kopiera URL" +rename: "Byt namn" +avatar: "Profilbild" +banner: "Banner" nsfw: "Känsligt innehåll" +reload: "Ladda om" +doNothing: "Ignorera" +reloadConfirm: "Vill du ladda om tidslinjen?" +accept: "Tillåt" +reject: "Neka" +normal: "Normal" +instanceName: "Instansnamn" +instanceDescription: "Instansbeskrivning" +maintainerEmail: "Administratörens epost" +tosUrl: "URL till användarvillkår" +thisYear: "Detta året" +thisMonth: "Denna månaden" +today: "Idag" +dayX: "{day}" +monthX: "{month}" +yearX: "{year}" +pages: "Sidor" +integration: "Integrationer" +connectService: "Anslut" +disconnectService: "Koppla från" +enableLocalTimeline: "Aktivera lokal tidslinje" +enableGlobalTimeline: "Aktivera global tidslinje" +enableRegistration: "Aktivera registrering av nya användare" +inMb: "I megabyte" +iconUrl: "URL till profilbilden" +bannerUrl: "URL till banner-bilden" pinnedNotes: "Fästad not" +enableHcaptcha: "Aktivera hCaptcha" +enableRecaptcha: "Aktivera reCAPTCHA" +enableTurnstile: "Aktivera Turnstile" +antennas: "Antenner" +manageAntennas: "Hantera Antenner" +antennaSource: "Antennkälla" +antennaKeywords: "Nyckelord att lyssna efter" +antennaExcludeKeywords: "Nyckelord att exkludera" +antennaKeywordsDescription: "Separera med mellanslag för en AND kondition, eller med nya linjer för en OR kondition" +notifyAntenna: "Notifiera om nya noter" +withFileAntenna: "Endast noter med filer" +enableServiceworker: "Aktivera pushnotiser i denna webbläsaren" +antennaUsersDescription: "Ange ett användarnamn per linje" +recentlyUpdatedUsers: "Nyligen aktiva användare" +recentlyRegisteredUsers: "Nyligen registrerade användare" userList: "Listor" +aboutMisskey: "Om Misskey" +administrator: "Administratör" +newPasswordIs: "Det nya lösenordet är \"{password}\"" +share: "Dela" +enable: "Aktivera" +serviceworkerInfo: "Måste vara aktiverad för pushnotiser." +enableInfiniteScroll: "Ladda mer automatiskt" +enablePlayer: "Öppna videospelare" +enableAll: "Aktivera alla" +enableEmail: "Aktivera epost-utskick" smtpHost: "Värd" smtpUser: "Användarnamn" smtpPass: "Lösenord" clearCache: "Rensa cache" +enabled: "Aktiverad" user: "Användare" +global: "Global" +squareAvatars: "Visa fyrkantiga profilbilder" searchByGoogle: "Sök" file: "Filer" +enableAutoSensitive: "Automatisk NSFW markering" +enableAutoSensitiveDescription: "Tillåter automatiskt detektering och marketing av NSFW media genom Maskininlärning när möjligt. Även om denna inställningen är avaktiverad, kan det vara aktiverat på hela instansen." +pushNotification: "Pushnotiser" +subscribePushNotification: "Aktivera pushnotiser" +unsubscribePushNotification: "Avaktivera pushnotiser" +pushNotificationAlreadySubscribed: "Pushnotiser är redan aktiverade" +pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för pushnotiser" _email: _follow: title: "följde dig" @@ -271,6 +375,9 @@ _mfm: quote: "Citat" emoji: "Anpassa emoji" search: "Sök" +_channel: + setBanner: "Välj banner" + removeBanner: "Ta bort banner" _theme: keys: mention: "Nämn" @@ -279,9 +386,19 @@ _sfx: note: "Noter" notification: "Notifikationer" chat: "Chatt" + antenna: "Antenner" +_antennaSources: + all: "Alla noter" + homeTimeline: "Noter från följda användare" + users: "Noter från specifika användare" + userList: "Noter från en specificerad lista av användare" + userGroup: "Noter från användare i en specificerad grupp" _widgets: + profile: "Profil" + instanceInfo: "Instansinformation" notifications: "Notifikationer" timeline: "Tidslinje" + activity: "Aktivitet" federation: "Federation" jobQueue: "Jobbkö" _userList: @@ -289,18 +406,29 @@ _widgets: _cw: show: "Ladda mer" _visibility: + home: "Hem" followers: "Följare" _profile: username: "Användarnamn" + changeAvatar: "Ändra profilbild" + changeBanner: "Ändra banner" _exportOrImport: + allNotes: "Alla noter" followingList: "Följer" muteList: "Tysta" blockingList: "Blockera" userLists: "Listor" _charts: federation: "Federation" +_timelines: + home: "Hem" + global: "Global" +_pages: + blocks: + image: "Bilder" _notification: youWereFollowed: "följde dig" + unreadAntennaNote: "Antenn {name}" _types: follow: "Följer" mention: "Nämn" @@ -314,5 +442,6 @@ _deck: _columns: notifications: "Notifikationer" tl: "Tidslinje" + antenna: "Antenner" list: "Listor" mentions: "Omnämningar" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 58deeff6f1..7a1cfa9236 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -8,7 +8,7 @@ search: "ค้นหา" notifications: "การเเจ้งเตือน" username: "ชื่อผู้ใช้" password: "รหัสผ่าน" -forgotPassword: "ลืมรหัสผ่าน?" +forgotPassword: "ลืมรหัสผ่านใช่ไหม" fetchingAsApObject: "กำลังดึงข้อมูล จาก เฟดิเวิร์ส..." ok: "โอเค" gotIt: "เข้าใจแล้ว !" @@ -917,7 +917,62 @@ tools: "เครื่องมือ" cannotLoad: "ไม่สามารถโหลดได้" numberOfProfileView: "มุมมองโปรไฟล์" like: "ชื่นชอบ" +unlike: "ไม่ชอบ" +numberOfLikes: "จำนวนไลค์" show: "แสดงผล" +neverShow: "ไม่ต้องแสดงข้อความนี้อีก" +remindMeLater: "ไว้ครั้งหน้าแล้วกัน" +didYouLikeMisskey: "คุณเคยชอบ Misskey ไหม?" +pleaseDonate: "{host} ใช้ซอฟต์แวร์ฟรี Misskey เราขอขอบคุณการบริจาคของคุณอย่างสูงเพื่อให้การพัฒนา Misskey สามารถดำเนินต่อไปได้นะ!" +roles: "บทบาท" +role: "บทบาท" +normalUser: "ผู้ใช้มาตรฐาน" +undefined: "ไม่ได้กำหนด" +assign: "กำหนด" +unassign: "ยังไม่มอบหมาย" +color: "สี" +manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" +_role: + new: "บทบาทใหม่" + edit: "แก้ไขบทบาท" + name: "ชื่อบทบาท" + description: "คำอธิบายบทบาท" + permission: "สิทธิ์ตามบทบาท" + descriptionOfPermission: "ผู้ดูแลกลั่นกรองเนื้อหา สามารถดำเนินการดูแลขั้นพื้นฐานได้นะ\nผู้ดูแลระบบ สามารถเปลี่ยนการตั้งค่าทั้งหมดของอินสแตนซ์ได้นะ" + assignTarget: "กำหนดเป้าหมาย" + descriptionOfAssignTarget: "แมนนวล เพื่อเปลี่ยนผู้ที่เป็นส่วนหนึ่งของบทบาทนี้และใครที่ไม่ใช่ด้วยตนเอง\nเงื่อนไข เพื่อให้ผู้ใช้ได้รับการกำหนดและนำออกจากบทบาทนี้โดยอัตโนมัติตามเงื่อนไขชุดหนึ่ง" + manual: "ปรับเอง" + conditional: "มีเงื่อนไข" + condition: "เงื่อนไข" + isConditionalRole: "นี่คือบทบาทที่มีเงื่อนไข" + isPublic: "บทบาทสาธารณะ" + descriptionOfIsPublic: "ทุกคนสามารถดูได้ว่าผู้ใช้งานนั้นได้รับมอบหมายบทบาทด้วยหรือไม่ \n\nบทบาทจะแสดงในโปรไฟล์ของผู้ใช้ด้วย" + options: "ตัวเลือกบทบาท" + baseRole: "บทบาทพื้นฐาน" + useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น" + chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด" + canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก" + descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ" + _options: + gtlAvailable: "การดูไทม์ไลน์ทั่วโลก" + ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น" + canPublicNote: "สามารถส่งโน้ตสาธารณะ" + canInvite: "สร้างรหัสเชิญอินสแตนซ์" + canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" + driveCapacity: "ความจุของไดรฟ์" + antennaMax: "จำนวนสูงสุดของเสาอากาศ" + _condition: + isLocal: "ผู้ใช้ภายใน" + isRemote: "ผู้ใช้ระยะไกล" + createdLessThan: "สร้างน้อยกว่า" + createdMoreThan: "สร้างมากกว่า" + followersLessThanOrEq: "จำนวนผู้ติดตามน้อยกว่าหรือเท่ากับ\n" + followersMoreThanOrEq: "จำนวนผู้ติดตามมากกว่าหรือเท่ากับ\n" + followingLessThanOrEq: "จำนวนบัญชีต่อไปนี้คือ น้อยกว่าหรือเท่ากับ" + followingMoreThanOrEq: "จำนวนบัญชีต่อไปนี้คือ มากกว่าหรือเท่ากับ" + and: "และ" + or: "หรือ" + not: "ไม่" _sensitiveMediaDetection: description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย" sensitivity: "การตรวจจับความไว" @@ -1296,6 +1351,8 @@ _weekday: friday: "วันศุกร์" saturday: "วันเสาร์" _widgets: + profile: "โปรไฟล์" + instanceInfo: "ข้อมูล อินสแตนซ์" memo: "โน้ตแปะ" notifications: "การเเจ้งเตือน" timeline: "ไทม์ไลน์" @@ -1317,10 +1374,12 @@ _widgets: jobQueue: "คิวงาน" serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์" aiscript: "AiScript คอนโซล" + aiscriptApp: "AiScript แอพ" aichan: "เอไอ" userList: "รายชื่อผู้ใช้" _userList: chooseList: "เลือกรายการ" + clicker: "คลิกเกอร์" _cw: hide: "ซ่อน" show: "โหลดเพิ่มเติม" @@ -1423,7 +1482,16 @@ _timelines: social: "โซเชี่ยล" global: "ทั่วโลก" _play: + new: "สร้างการเล่น" + edit: "แก้ไขเล่น" + created: "สร้างการเล่นแล้ว" + updated: "แก้ไขการเล่นแล้ว" + deleted: "ลบการเล่นแล้ว" + pageSetting: "ตั้งค่าการเล่น" + editThisPage: "แก้ไข Play นี้" viewSource: "ดูต้นฉบับ" + my: "มาย เพลย์" + liked: "ไลค์ เพลย์" featured: "เป็นที่นิยม" title: "หัวข้อ" script: "สคริปต์" @@ -1487,7 +1555,6 @@ _notification: youGotReply: "{name} ตอบกลับถึงคุณ" youGotQuote: "{name} อ้างถึงคุณ" youRenoted: "รีโน้ตจาก {name}" - youGotPoll: "{name} โหวตบนแบบสำรวจความคิดเห็นของคุณ" youGotMessagingMessageFromUser: "{name} ได้ส่งข้อความแชทถึงคุณ" youGotMessagingMessageFromGroup: "ข้อความแชทถูกส่งไปยัง {name} กลุ่ม" youWereFollowed: "ได้ติดตามคุณ" @@ -1505,7 +1572,6 @@ _notification: renote: "รีโน้ต" quote: "อ้างคำพูด" reaction: "รีแอคชั่น" - pollVote: "จำนวนโหวตที่ได้รับ" pollEnded: "โพลนี้สิ้นสุดลงแล้ว" receiveFollowRequest: "ได้รับคำขอติดตาม\n" followRequestAccepted: "ยอมรับคำขอติดตาม" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index aecb413de7..ebcdfa5bfb 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -53,6 +53,7 @@ _mfm: _sfx: notification: "Bildirim" _widgets: + profile: "Profil" notifications: "Bildirim" timeline: "Zaman çizelgesi" _profile: diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 352fb354ee..8b6e59cf1e 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -894,6 +894,7 @@ windowRestore: "Відновити" caption: "Підпис" like: "Вподобати" show: "Відображення" +color: "Колір" _sensitiveMediaDetection: sensitivity: "Чутливість детектування" setSensitiveFlagAutomatically: "Позначити як NSFW" @@ -1229,6 +1230,8 @@ _weekday: friday: "П'ятниця" saturday: "Субота" _widgets: + profile: "Профіль" + instanceInfo: "Про цей інстанс" memo: "Нагадування" notifications: "Сповіщення" timeline: "Стрічка" @@ -1415,7 +1418,6 @@ _notification: youGotReply: "{name} відповідає" youGotQuote: "{name} цитує вас" youRenoted: "{name} поширює" - youGotPoll: "{name} бере участь в опитуванні" youGotMessagingMessageFromUser: "Повідомлення від {name}" youGotMessagingMessageFromGroup: "Нове повідомлення в групі {name}" youWereFollowed: "Новий підписник" @@ -1430,7 +1432,6 @@ _notification: renote: "Поширення" quote: "Цитування" reaction: "Реакції" - pollVote: "Опитування" receiveFollowRequest: "Запити на підписку" followRequestAccepted: "Прийняті підписки" groupInvited: "Запрошення до груп" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 0070af56f0..7ea4ec5d9e 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -896,6 +896,7 @@ account: "Tài khoản của bạn" move: "Di chuyển" like: "Thích" show: "Hiển thị" +color: "Màu sắc" _sensitiveMediaDetection: description: "Giảm nỗ lực kiểm duyệt máy chủ thông qua việc tự động nhận dạng media NSFW thông qua học máy. Điều này sẽ làm tăng một chút áp lực trên máy chủ." sensitivity: "Phát hiện nhạy cảm" @@ -1271,6 +1272,8 @@ _weekday: friday: "Thứ Sáu" saturday: "Thứ Bảy" _widgets: + profile: "Trang cá nhân" + instanceInfo: "Thông tin máy chủ" memo: "Tút đã ghim" notifications: "Thông báo" timeline: "Bảng tin" @@ -1460,7 +1463,6 @@ _notification: youGotReply: "{name} trả lời bạn" youGotQuote: "{name} trích dẫn tút của bạn" youRenoted: "{name} đăng lại tút của bạn" - youGotPoll: "{name} bình chọn tút của bạn" youGotMessagingMessageFromUser: "{name} nhắn tin cho bạn" youGotMessagingMessageFromGroup: "Một tin nhắn trong nhóm {name}" youWereFollowed: "đã theo dõi bạn" @@ -1477,7 +1479,6 @@ _notification: renote: "Đăng lại" quote: "Trích dẫn" reaction: "Biểu cảm" - pollVote: "Lượt bình chọn" pollEnded: "Bình chọn kết thúc" receiveFollowRequest: "Yêu cầu theo dõi" followRequestAccepted: "Yêu cầu theo dõi được chấp nhận" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 7c3efec86c..136cb80fd8 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -13,7 +13,7 @@ fetchingAsApObject: "在联邦宇宙查询中..." ok: "OK" gotIt: "我明白了" cancel: "取消" -noThankYou: "不用" +noThankYou: "不用,谢谢" enterUsername: "输入用户名" renotedBy: "由 {user} 转贴" noNotes: "没有帖子" @@ -920,6 +920,67 @@ like: "点赞!" unlike: "取消赞" numberOfLikes: "点赞数" show: "显示" +neverShow: "不再显示" +remindMeLater: "稍后提醒我" +didYouLikeMisskey: "您喜欢Misskey吗?" +pleaseDonate: "Misskey是{host}所使用的免费软件。为了今后也能够维持Misskey的开发,请在有余力的情况下进行捐助!" +roles: "角色" +role: "角色" +normalUser: "普通用户" +undefined: "未定义" +assign: "分配" +unassign: "取消分配" +color: "颜色" +manageCustomEmojis: "管理自定义表情符号" +youCannotCreateAnymore: "抱歉,您无法再创建更多了。" +_role: + new: "创建角色" + edit: "编辑角色" + name: "角色名称" + description: "角色描述" + permission: "角色权限" + descriptionOfPermission: "监察员可以执行基本的审核操作。\n管理员可以更改实例的所有设置。" + assignTarget: "授权对象" + descriptionOfAssignTarget: "手动指手动选择谁被包括在这个角色中。\n符合条件指设置条件以自动包括符合条件的用户。" + manual: "手动" + conditional: "符合条件" + condition: "条件" + isConditionalRole: "这是一个条件控制的角色。" + isPublic: "角色公开" + descriptionOfIsPublic: "任何人都可以看到分配该角色的用户。而用户的个人资料也将显示该角色。" + options: "选项" + baseRole: "基本角色" + useBaseValue: "使用基本角色的值" + chooseRoleToAssign: "选择要分配的角色" + canEditMembersByModerator: "允许监察者编辑成员" + descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" + _options: + gtlAvailable: "查看全局时间线" + ltlAvailable: "查看本地时间线" + canPublicNote: "允许公开发帖" + canInvite: "发放实例邀请码" + canManageCustomEmojis: "管理自定义表情符号" + driveCapacity: "网盘容量" + pinMax: "帖子置顶数量限制" + antennaMax: "可创建的最大天线数量" + wordMuteMax: "屏蔽词的字数限制" + webhookMax: "Webhook 创建数量限制" + clipMax: "便签创建数量限制" + noteEachClipsMax: "单个便签内的贴文数量限制" + userListMax: "用户列表创建数量限制" + userEachUserListsMax: "单个用户列表内用户数量限制" + _condition: + isLocal: "是本地用户" + isRemote: "是远程用户" + createdLessThan: "账户创建时间少于" + createdMoreThan: "账户创建时间超过" + followersLessThanOrEq: "关注者不多于" + followersMoreThanOrEq: "关注者不少于" + followingLessThanOrEq: "关注中不多于" + followingMoreThanOrEq: "关注中不少于" + and: "符合以下全部条件" + or: "符合以下任一条件" + not: "不符合以下任何条件" _sensitiveMediaDetection: description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。" sensitivity: "检测敏感度" @@ -1298,6 +1359,8 @@ _weekday: friday: "星期五" saturday: "星期六" _widgets: + profile: "个人资料" + instanceInfo: "实例信息" memo: "便签" notifications: "通知" timeline: "时间线" @@ -1324,6 +1387,7 @@ _widgets: userList: "用户列表" _userList: chooseList: "选择列表" + clicker: "点击器" _cw: hide: "隐藏" show: "查看更多" @@ -1499,7 +1563,6 @@ _notification: youGotReply: "来自{name}的回复" youGotQuote: "来自{name}的引用" youRenoted: "来自{name}的转发" - youGotPoll: "来自{name}的投票" youGotMessagingMessageFromUser: "来自{name}的聊天" youGotMessagingMessageFromGroup: "来自{name}的群聊" youWereFollowed: "关注了你。" @@ -1517,7 +1580,6 @@ _notification: renote: "转发" quote: "引用" reaction: "回应" - pollVote: "问卷调查被投票" pollEnded: "问卷调查结束" receiveFollowRequest: "收到关注请求" followRequestAccepted: "关注请求已通过" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 661325d506..fc5f3e23c7 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -252,7 +252,7 @@ uploadFromUrlMayTakeTime: "還需要一些時間才能完成上傳。" explore: "探索" messageRead: "已讀" noMoreHistory: "沒有更多歷史紀錄" -startMessaging: "開始傳送訊息" +startMessaging: "開始聊天" nUsersRead: "{n}人已讀" agreeTo: "我同意{0}" tos: "使用條款" @@ -324,8 +324,8 @@ integration: "整合" connectService: "己連結" disconnectService: "己斷開 " enableLocalTimeline: "開啟本地時間軸" -enableGlobalTimeline: "啟用公開時間軸" -disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和協調人仍可以繼續使用,以方便您。" +enableGlobalTimeline: "啟用全域時間軸" +disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審核員仍可以繼續使用。" registration: "註冊" enableRegistration: "開啟新使用者註冊" invite: "邀請" @@ -388,7 +388,7 @@ aboutMisskey: "關於 Misskey" administrator: "管理員" token: "權杖" twoStepAuthentication: "兩階段驗證" -moderator: "板主" +moderator: "監察員" moderation: "言論調節" nUsersMentioned: "提到了{n}" securityKey: "安全金鑰" @@ -797,7 +797,7 @@ squareAvatars: "頭像以方形顯示" sent: "發送" received: "收取" searchResult: "搜尋結果" -hashtags: "#tag" +hashtags: "標籤" troubleshooting: "故障排除" useBlurEffect: "在 UI 上使用模糊效果" learnMore: "更多資訊" @@ -869,7 +869,7 @@ recommended: "推薦" check: "檢查" driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限" driveCapOverrideCaption: "如果指定0以下的值,就會被取消。" -requireAdminForView: "必須以管理者帳號登入才可以檢視。" +requireAdminForView: "必須以管理員帳號登入才可以檢視。" isSystemAccount: "由系統自動建立與管理的帳號。" typeToConfirm: "要執行這項操作,請輸入 {x} " deleteAccount: "刪除帳號" @@ -918,7 +918,64 @@ cannotLoad: "無法載入" numberOfProfileView: "個人檔案檢視次數" like: "讚" unlike: "收回讚" +numberOfLikes: "讚數" show: "檢視" +neverShow: "不再顯示" +remindMeLater: "以後再說" +didYouLikeMisskey: "您是否喜愛Misskey呢?" +pleaseDonate: "Misskey是由{host}使用的免費軟體。請贊助我們,讓開發能夠持續!" +roles: "角色" +role: "角色" +normalUser: "一般使用者" +undefined: "未定義" +assign: "指派" +unassign: "取消指派" +color: "顏色" +manageCustomEmojis: "管理自訂表情符號" +_role: + new: "建立角色" + edit: "編輯角色" + name: "角色名稱" + description: "角色描述 " + permission: "角色的權限" + descriptionOfPermission: "審核員執行與審核相關的基本操作。\n管理員能變更實例的全部設定。" + assignTarget: "指派目標" + descriptionOfAssignTarget: "手動是以手動管理這個角色包含的人員。\n符合條件是設定條件以自動包含符合條件的使用者。" + manual: "手動" + conditional: "符合條件" + condition: "條件" + isConditionalRole: "這是條件角色。" + isPublic: "角色為公開" + descriptionOfIsPublic: "任何人都可以看到被指派了角色的使用者。此外,使用者的個人檔案將顯示這個角色。" + options: "選項" + baseRole: "基本角色" + useBaseValue: "使用基本角色的值" + chooseRoleToAssign: "選擇要指派的角色" + canEditMembersByModerator: "允許編輯監察員的成員" + descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。" + _options: + gtlAvailable: "瀏覽全域時間軸" + ltlAvailable: "瀏覽本地時間軸" + canPublicNote: "允許公開貼文" + canInvite: "發行實例邀請碼" + canManageCustomEmojis: "管理自訂表情符號" + driveCapacity: "雲端硬碟容量" + pinMax: "置頂貼文的最大數量" + antennaMax: "可建立的天線數量" + webhookMax: "可建立的Webhook數量" + clipMax: "可建立的摘錄數量" + _condition: + isLocal: "本地使用者" + isRemote: "遠端使用者" + createdLessThan: "自建立帳戶開始~以內" + createdMoreThan: "自建立帳戶開始~經過" + followersLessThanOrEq: "追隨者人數在~以下" + followersMoreThanOrEq: "追隨者人數在~以上" + followingLessThanOrEq: "追隨人數在~以下" + followingMoreThanOrEq: "追隨人數在~以上" + and: "~和~" + or: "~或~" + not: "~否" _sensitiveMediaDetection: description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。" sensitivity: "檢測敏感度" @@ -1154,7 +1211,7 @@ _theme: navActive: "側邊欄文本 (活動)" navIndicator: "側邊欄指示符" link: "鏈接" - hashtag: "#tag" + hashtag: "標籤" mention: "提到" mentionMe: "提到了我" renote: "轉發貼文" @@ -1187,7 +1244,7 @@ _sfx: note: "貼文" noteMy: "我的貼文" notification: "通知" - chat: "傳送訊息" + chat: "聊天" chatBg: "聊天背景" antenna: "天線接收" channel: "頻道通知" @@ -1297,6 +1354,8 @@ _weekday: friday: "週五" saturday: "週六" _widgets: + profile: "個人檔案" + instanceInfo: "實例資訊" memo: "備忘錄" notifications: "通知" timeline: "時間軸" @@ -1318,10 +1377,12 @@ _widgets: jobQueue: "佇列" serverMetric: "服務器指標 " aiscript: "AiScript控制台" + aiscriptApp: "AiScript App" aichan: "小藍" userList: "使用者列表" _userList: chooseList: "選擇清單" + clicker: "點擊器" _cw: hide: "隱藏" show: "瀏覽更多" @@ -1424,7 +1485,16 @@ _timelines: social: "社群" global: "公開" _play: + new: "新增Play" + edit: "編輯Play" + created: "已新增Play" + updated: "已更新Play" + deleted: "已刪除Play" + pageSetting: "Play設定" + editThisPage: "編輯這個Play" viewSource: "檢視原始碼" + my: "自己的Play" + liked: "按了讚的Play" featured: "人氣" title: "標題" script: "腳本" @@ -1488,7 +1558,6 @@ _notification: youGotReply: "{name}回覆了您" youGotQuote: "{name}引用了您" youRenoted: "{name} 轉發了你的貼文" - youGotPoll: "{name}已投票" youGotMessagingMessageFromUser: "{name}發送給您的訊息" youGotMessagingMessageFromGroup: "{name}發送給您的訊息" youWereFollowed: "您有新的追隨者" @@ -1506,7 +1575,6 @@ _notification: renote: "轉發貼文" quote: "引用" reaction: "反應" - pollVote: "統計已投票數" pollEnded: "問卷調查結束" receiveFollowRequest: "已收到追隨請求" followRequestAccepted: "追隨請求已接受" diff --git a/package.json b/package.json index 1ee91c9fc1..e29758f173 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.0.0-beta.26", + "version": "13.0.0-rc.8", "codename": "indigo", "repository": { "type": "git", @@ -53,12 +53,15 @@ "devDependencies": { "@types/gulp": "4.0.10", "@types/gulp-rename": "2.0.1", - "@typescript-eslint/eslint-plugin": "5.48.0", - "@typescript-eslint/parser": "5.48.0", + "@typescript-eslint/eslint-plugin": "5.48.1", + "@typescript-eslint/parser": "5.48.1", "cross-env": "7.0.3", "cypress": "12.3.0", "eslint": "^8.31.0", "start-server-and-test": "1.15.2", "typescript": "4.9.4" + }, + "optionalDependencies": { + "@tensorflow/tfjs-core": "^4.2.0" } } diff --git a/packages/backend/assets/emoji-unknown.png b/packages/backend/assets/emoji-unknown.png new file mode 100644 index 0000000000..ab29bef2b1 Binary files /dev/null and b/packages/backend/assets/emoji-unknown.png differ diff --git a/packages/backend/assets/notification-badges/LICENSE b/packages/backend/assets/notification-badges/LICENSE deleted file mode 100644 index 841c4c682b..0000000000 --- a/packages/backend/assets/notification-badges/LICENSE +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index d1492856de..0000000000 Binary files a/packages/backend/assets/notification-badges/at.png and /dev/null differ diff --git a/packages/backend/assets/notification-badges/check.png b/packages/backend/assets/notification-badges/check.png deleted file mode 100644 index baeb76babf..0000000000 Binary files a/packages/backend/assets/notification-badges/check.png and /dev/null differ diff --git a/packages/backend/assets/notification-badges/clipboard-check-solid.png b/packages/backend/assets/notification-badges/clipboard-check-solid.png deleted file mode 100644 index d8cdfa9da4..0000000000 Binary files a/packages/backend/assets/notification-badges/clipboard-check-solid.png and /dev/null differ diff --git a/packages/backend/assets/notification-badges/clock.png b/packages/backend/assets/notification-badges/clock.png deleted file mode 100644 index 9323f8f307..0000000000 Binary files a/packages/backend/assets/notification-badges/clock.png and /dev/null differ diff --git a/packages/backend/assets/notification-badges/comments.png b/packages/backend/assets/notification-badges/comments.png deleted file mode 100644 index bc8a1c35b4..0000000000 Binary files a/packages/backend/assets/notification-badges/comments.png and /dev/null differ diff --git a/packages/backend/assets/notification-badges/id-card-alt.png b/packages/backend/assets/notification-badges/id-card-alt.png deleted file mode 100644 index 67e1410e34..0000000000 Binary files a/packages/backend/assets/notification-badges/id-card-alt.png and /dev/null differ diff --git a/packages/backend/assets/notification-badges/plus.png b/packages/backend/assets/notification-badges/plus.png deleted file mode 100644 index 05362c122b..0000000000 Binary files a/packages/backend/assets/notification-badges/plus.png and /dev/null differ diff --git a/packages/backend/assets/notification-badges/poll-h.png b/packages/backend/assets/notification-badges/poll-h.png deleted file mode 100644 index 3b7ded6659..0000000000 Binary files a/packages/backend/assets/notification-badges/poll-h.png and /dev/null differ diff --git a/packages/backend/assets/notification-badges/quote-right.png b/packages/backend/assets/notification-badges/quote-right.png deleted file mode 100644 index 0fa4837654..0000000000 Binary files a/packages/backend/assets/notification-badges/quote-right.png and /dev/null differ diff --git a/packages/backend/assets/notification-badges/reply.png b/packages/backend/assets/notification-badges/reply.png deleted file mode 100644 index 77021f71a7..0000000000 Binary files a/packages/backend/assets/notification-badges/reply.png and /dev/null differ diff --git a/packages/backend/assets/notification-badges/retweet.png b/packages/backend/assets/notification-badges/retweet.png deleted file mode 100644 index dc61060481..0000000000 Binary files a/packages/backend/assets/notification-badges/retweet.png and /dev/null differ diff --git a/packages/backend/assets/notification-badges/satellite.png b/packages/backend/assets/notification-badges/satellite.png deleted file mode 100644 index 0e1831e8a0..0000000000 Binary files a/packages/backend/assets/notification-badges/satellite.png and /dev/null differ diff --git a/packages/backend/assets/notification-badges/user-plus.png b/packages/backend/assets/notification-badges/user-plus.png deleted file mode 100644 index 9d376d04d6..0000000000 Binary files a/packages/backend/assets/notification-badges/user-plus.png and /dev/null differ diff --git a/packages/backend/assets/tabler-badges/LICENSE b/packages/backend/assets/tabler-badges/LICENSE new file mode 100644 index 0000000000..cab2551f67 --- /dev/null +++ b/packages/backend/assets/tabler-badges/LICENSE @@ -0,0 +1,24 @@ +Tabler Icons +https://github.com/tabler/tabler-icons/blob/master/LICENSE +==== +MIT License + +Copyright (c) 2020-2022 Paweł Kuna + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/backend/assets/tabler-badges/antenna.png b/packages/backend/assets/tabler-badges/antenna.png new file mode 100644 index 0000000000..013c7f4e61 Binary files /dev/null and b/packages/backend/assets/tabler-badges/antenna.png differ diff --git a/packages/backend/assets/tabler-badges/arrow-back-up.png b/packages/backend/assets/tabler-badges/arrow-back-up.png new file mode 100644 index 0000000000..a253384c72 Binary files /dev/null and b/packages/backend/assets/tabler-badges/arrow-back-up.png differ diff --git a/packages/backend/assets/tabler-badges/at.png b/packages/backend/assets/tabler-badges/at.png new file mode 100644 index 0000000000..cbf8df4925 Binary files /dev/null and b/packages/backend/assets/tabler-badges/at.png differ diff --git a/packages/backend/assets/tabler-badges/chart-arrows.png b/packages/backend/assets/tabler-badges/chart-arrows.png new file mode 100644 index 0000000000..b2b8a2d993 Binary files /dev/null and b/packages/backend/assets/tabler-badges/chart-arrows.png differ diff --git a/packages/backend/assets/tabler-badges/circle-check.png b/packages/backend/assets/tabler-badges/circle-check.png new file mode 100644 index 0000000000..6464d5133c Binary files /dev/null and b/packages/backend/assets/tabler-badges/circle-check.png differ diff --git a/packages/backend/assets/tabler-badges/messages.png b/packages/backend/assets/tabler-badges/messages.png new file mode 100644 index 0000000000..fa5072ebba Binary files /dev/null and b/packages/backend/assets/tabler-badges/messages.png differ diff --git a/packages/backend/assets/notification-badges/null.png b/packages/backend/assets/tabler-badges/null.png similarity index 100% rename from packages/backend/assets/notification-badges/null.png rename to packages/backend/assets/tabler-badges/null.png diff --git a/packages/backend/assets/tabler-badges/plus.png b/packages/backend/assets/tabler-badges/plus.png new file mode 100644 index 0000000000..f13a86f4cd Binary files /dev/null and b/packages/backend/assets/tabler-badges/plus.png differ diff --git a/packages/backend/assets/tabler-badges/quote.png b/packages/backend/assets/tabler-badges/quote.png new file mode 100644 index 0000000000..e0fc6f3fb4 Binary files /dev/null and b/packages/backend/assets/tabler-badges/quote.png differ diff --git a/packages/backend/assets/tabler-badges/repeat.png b/packages/backend/assets/tabler-badges/repeat.png new file mode 100644 index 0000000000..ab548043f7 Binary files /dev/null and b/packages/backend/assets/tabler-badges/repeat.png differ diff --git a/packages/backend/assets/tabler-badges/user-plus.png b/packages/backend/assets/tabler-badges/user-plus.png new file mode 100644 index 0000000000..2ae96f0b73 Binary files /dev/null and b/packages/backend/assets/tabler-badges/user-plus.png differ diff --git a/packages/backend/assets/tabler-badges/users.png b/packages/backend/assets/tabler-badges/users.png new file mode 100644 index 0000000000..7862963327 Binary files /dev/null and b/packages/backend/assets/tabler-badges/users.png differ diff --git a/packages/backend/migration/1673336077243-PollChoiceLength.js b/packages/backend/migration/1673336077243-PollChoiceLength.js new file mode 100644 index 0000000000..810c626e04 --- /dev/null +++ b/packages/backend/migration/1673336077243-PollChoiceLength.js @@ -0,0 +1,11 @@ +export class PollChoiceLength1673336077243 { + name = 'PollChoiceLength1673336077243' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "poll" ALTER COLUMN "choices" TYPE character varying(256) array`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "poll" ALTER COLUMN "choices" TYPE character varying(128) array`); + } +} diff --git a/packages/backend/migration/1673500412259-Role.js b/packages/backend/migration/1673500412259-Role.js new file mode 100644 index 0000000000..a8acedf5b7 --- /dev/null +++ b/packages/backend/migration/1673500412259-Role.js @@ -0,0 +1,37 @@ +export class Role1673500412259 { + name = 'Role1673500412259' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "role" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "description" character varying(1024) NOT NULL, "isPublic" boolean NOT NULL DEFAULT false, "isModerator" boolean NOT NULL DEFAULT false, "isAdministrator" boolean NOT NULL DEFAULT false, "options" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_b36bcfe02fc8de3c57a8b2391c2" PRIMARY KEY ("id")); COMMENT ON COLUMN "role"."createdAt" IS 'The created date of the Role.'; COMMENT ON COLUMN "role"."updatedAt" IS 'The updated date of the Role.'`); + await queryRunner.query(`CREATE TABLE "role_assignment" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "roleId" character varying(32) NOT NULL, CONSTRAINT "PK_7e79671a8a5db18936173148cb4" PRIMARY KEY ("id")); COMMENT ON COLUMN "role_assignment"."createdAt" IS 'The created date of the RoleAssignment.'; COMMENT ON COLUMN "role_assignment"."userId" IS 'The user ID.'; COMMENT ON COLUMN "role_assignment"."roleId" IS 'The role ID.'`); + await queryRunner.query(`CREATE INDEX "IDX_db5b72c16227c97ca88734d5c2" ON "role_assignment" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_f0de67fd09cd3cd0aabca79994" ON "role_assignment" ("roleId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0953deda7ce6e1448e935859e5" ON "role_assignment" ("userId", "roleId") `); + await queryRunner.query(`ALTER TABLE "user" RENAME COLUMN "isAdmin" TO "isRoot"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isModerator"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "driveCapacityOverrideMb"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "disableLocalTimeline"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "disableGlobalTimeline"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "localDriveCapacityMb"`); + await queryRunner.query(`ALTER TABLE "meta" ADD "defaultRoleOverride" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`ALTER TABLE "role_assignment" ADD CONSTRAINT "FK_db5b72c16227c97ca88734d5c2b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "role_assignment" ADD CONSTRAINT "FK_f0de67fd09cd3cd0aabca79994d" FOREIGN KEY ("roleId") REFERENCES "role"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "role_assignment" DROP CONSTRAINT "FK_f0de67fd09cd3cd0aabca79994d"`); + await queryRunner.query(`ALTER TABLE "role_assignment" DROP CONSTRAINT "FK_db5b72c16227c97ca88734d5c2b"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "defaultRoleOverride"`); + await queryRunner.query(`ALTER TABLE "meta" ADD "localDriveCapacityMb" integer NOT NULL DEFAULT '1024'`); + await queryRunner.query(`ALTER TABLE "meta" ADD "disableGlobalTimeline" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "meta" ADD "disableLocalTimeline" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "user" ADD "driveCapacityOverrideMb" integer`); + await queryRunner.query(`ALTER TABLE "user" ADD "isModerator" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "user" RENAME COLUMN "isRoot" TO "isAdmin"`); + await queryRunner.query(`DROP INDEX "public"."IDX_0953deda7ce6e1448e935859e5"`); + await queryRunner.query(`DROP INDEX "public"."IDX_f0de67fd09cd3cd0aabca79994"`); + await queryRunner.query(`DROP INDEX "public"."IDX_db5b72c16227c97ca88734d5c2"`); + await queryRunner.query(`DROP TABLE "role_assignment"`); + await queryRunner.query(`DROP TABLE "role"`); + } +} diff --git a/packages/backend/migration/1673515526953-RoleColor.js b/packages/backend/migration/1673515526953-RoleColor.js new file mode 100644 index 0000000000..343eedf346 --- /dev/null +++ b/packages/backend/migration/1673515526953-RoleColor.js @@ -0,0 +1,11 @@ +export class RoleColor1673515526953 { + name = 'RoleColor1673515526953' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" ADD "color" character varying(256)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "color"`); + } +} diff --git a/packages/backend/migration/1673522856499-RoleIroiro.js b/packages/backend/migration/1673522856499-RoleIroiro.js new file mode 100644 index 0000000000..a1e64d49fe --- /dev/null +++ b/packages/backend/migration/1673522856499-RoleIroiro.js @@ -0,0 +1,13 @@ +export class RoleIroiro1673522856499 { + name = 'RoleIroiro1673522856499' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isSilenced"`); + await queryRunner.query(`ALTER TABLE "role" ADD "canEditMembersByModerator" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "canEditMembersByModerator"`); + await queryRunner.query(`ALTER TABLE "user" ADD "isSilenced" boolean NOT NULL DEFAULT false`); + } +} diff --git a/packages/backend/migration/1673524604156-RoleLastUsedAt.js b/packages/backend/migration/1673524604156-RoleLastUsedAt.js new file mode 100644 index 0000000000..786ef07f5e --- /dev/null +++ b/packages/backend/migration/1673524604156-RoleLastUsedAt.js @@ -0,0 +1,13 @@ +export class RoleLastUsedAt1673524604156 { + name = 'RoleLastUsedAt1673524604156' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" ADD "lastUsedAt" TIMESTAMP WITH TIME ZONE NOT NULL`); + await queryRunner.query(`COMMENT ON COLUMN "role"."lastUsedAt" IS 'The last used date of the Role.'`); + } + + async down(queryRunner) { + await queryRunner.query(`COMMENT ON COLUMN "role"."lastUsedAt" IS 'The last used date of the Role.'`); + await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "lastUsedAt"`); + } +} diff --git a/packages/backend/migration/1673570377815-RoleConditional.js b/packages/backend/migration/1673570377815-RoleConditional.js new file mode 100644 index 0000000000..11ae4f00c6 --- /dev/null +++ b/packages/backend/migration/1673570377815-RoleConditional.js @@ -0,0 +1,15 @@ +export class RoleConditional1673570377815 { + name = 'RoleConditional1673570377815' + + async up(queryRunner) { + await queryRunner.query(`CREATE TYPE "public"."role_target_enum" AS ENUM('manual', 'conditional')`); + await queryRunner.query(`ALTER TABLE "role" ADD "target" "public"."role_target_enum" NOT NULL DEFAULT 'manual'`); + await queryRunner.query(`ALTER TABLE "role" ADD "condFormula" jsonb NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "condFormula"`); + await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "target"`); + await queryRunner.query(`DROP TYPE "public"."role_target_enum"`); + } +} diff --git a/packages/backend/migration/1673575973645-MetaClean.js b/packages/backend/migration/1673575973645-MetaClean.js new file mode 100644 index 0000000000..11be4c1cdd --- /dev/null +++ b/packages/backend/migration/1673575973645-MetaClean.js @@ -0,0 +1,11 @@ +export class MetaClean1673575973645 { + name = 'MetaClean1673575973645' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "remoteDriveCapacityMb"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "remoteDriveCapacityMb" integer NOT NULL DEFAULT '32'`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 6c1a217b60..72123bdf12 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -21,17 +21,17 @@ "@tensorflow/tfjs-node": "4.1.0" }, "dependencies": { - "@bull-board/api": "^4.10.1", - "@bull-board/fastify": "^4.10.1", - "@bull-board/ui": "^4.10.1", + "@bull-board/api": "^4.10.2", + "@bull-board/fastify": "^4.10.2", + "@bull-board/ui": "^4.10.2", "@discordapp/twemoji": "14.0.2", "@fastify/accepts": "4.1.0", "@fastify/cookie": "^8.3.0", "@fastify/cors": "8.2.0", "@fastify/http-proxy": "^8.4.0", - "@fastify/multipart": "7.3.0", - "@fastify/static": "6.6.0", - "@fastify/view": "7.3.0", + "@fastify/multipart": "7.4.0", + "@fastify/static": "6.6.1", + "@fastify/view": "7.4.0", "@nestjs/common": "9.2.1", "@nestjs/core": "9.2.1", "@nestjs/testing": "9.2.1", @@ -41,7 +41,7 @@ "ajv": "8.12.0", "archiver": "5.3.1", "autwh": "0.1.0", - "aws-sdk": "2.1289.0", + "aws-sdk": "2.1295.0", "bcryptjs": "2.4.3", "blurhash": "2.0.4", "bull": "4.10.2", @@ -58,7 +58,7 @@ "escape-regexp": "0.0.1", "fastify": "4.11.0", "feed": "4.2.2", - "file-type": "18.0.0", + "file-type": "18.1.0", "fluent-ffmpeg": "2.1.2", "form-data": "^4.0.0", "got": "12.5.3", @@ -67,17 +67,17 @@ "ip-cidr": "3.0.11", "is-svg": "4.3.2", "js-yaml": "4.1.0", - "jsdom": "20.0.3", + "jsdom": "21.0.0", "json5": "2.2.3", "json5-loader": "4.0.1", "jsonld": "8.1.0", "jsrsasign": "10.6.1", - "mfm-js": "0.23.0", + "mfm-js": "0.23.3", "mime-types": "2.1.35", "misskey-js": "0.0.14", "ms": "3.0.0-canary.1", "nested-property": "4.0.0", - "nodemailer": "6.8.0", + "nodemailer": "6.9.0", "nsfwjs": "2.4.2", "oauth": "^0.10.0", "os-utils": "0.0.14", @@ -87,7 +87,7 @@ "probe-image-size": "7.2.3", "promise-limit": "2.7.0", "pug": "3.0.2", - "punycode": "2.1.1", + "punycode": "2.2.0", "pureimage": "0.3.15", "qrcode": "1.5.1", "random-seed": "0.3.0", @@ -109,7 +109,7 @@ "stringz": "2.1.0", "summaly": "2.7.0", "syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2", - "systeminformation": "5.17.1", + "systeminformation": "5.17.3", "tinycolor2": "1.5.2", "tmp": "0.2.1", "tsc-alias": "1.8.2", @@ -117,18 +117,18 @@ "twemoji-parser": "14.0.0", "typeorm": "0.3.11", "ulid": "2.3.0", - "undici": "^5.14.0", + "undici": "^5.15.0", "unzipper": "0.10.11", "uuid": "9.0.0", "vary": "1.1.2", "web-push": "3.5.0", "websocket": "1.0.34", - "ws": "8.11.0", + "ws": "8.12.0", "xev": "3.0.2" }, "devDependencies": { - "@redocly/openapi-core": "1.0.0-beta.117", - "@swc/core": "1.3.25", + "@redocly/openapi-core": "1.0.0-beta.120", + "@swc/core": "1.3.26", "@swc/jest": "0.2.24", "@types/accepts": "1.3.5", "@types/archiver": "5.3.1", @@ -172,11 +172,11 @@ "@types/web-push": "3.3.2", "@types/websocket": "1.0.5", "@types/ws": "8.5.4", - "@typescript-eslint/eslint-plugin": "5.48.0", - "@typescript-eslint/parser": "5.48.0", + "@typescript-eslint/eslint-plugin": "5.48.1", + "@typescript-eslint/parser": "5.48.1", "cross-env": "7.0.3", "eslint": "8.31.0", - "eslint-plugin-import": "2.26.0", + "eslint-plugin-import": "2.27.4", "execa": "6.1.0", "jest": "29.3.1", "jest-mock": "^29.3.1", diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index d6f326f616..be755f7dab 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -15,8 +15,9 @@ import type { Packed } from '@/misc/schema.js'; import { DI } from '@/di-symbols.js'; import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; import { UtilityService } from '@/core/UtilityService.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class AntennaService implements OnApplicationShutdown { @@ -73,7 +74,7 @@ export class AntennaService implements OnApplicationShutdown { const obj = JSON.parse(data); if (obj.channel === 'internal') { - const { type, body } = obj.message; + const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'antennaCreated': this.antennas.push(body); @@ -135,7 +136,7 @@ export class AntennaService implements OnApplicationShutdown { this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna); this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', { antenna: { id: antenna.id, name: antenna.name }, - note: await this.noteEntityService.pack(note) + note: await this.noteEntityService.pack(note), }); } }, 2000); @@ -144,27 +145,19 @@ export class AntennaService implements OnApplicationShutdown { // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている - /** - * noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい - */ @bindThis - public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise { + public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise { if (note.visibility === 'specified') return false; - + if (note.visibility === 'followers') return false; + // アンテナ作成者がノート作成者にブロックされていたらスキップ const blockings = await this.blockingCache.fetch(noteUser.id, () => this.blockingsRepository.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId))); if (blockings.some(blocking => blocking === antenna.userId)) return false; - if (note.visibility === 'followers') { - if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; - if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; - } - if (!antenna.withReplies && note.replyId != null) return false; if (antenna.src === 'home') { - if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; - if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; + // TODO } else if (antenna.src === 'list') { const listUsers = (await this.userListJoiningsRepository.findBy({ userListId: antenna.userListId!, diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index 6b5cb4db32..1e98914052 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -1,7 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { UsersRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import { Injectable } from '@nestjs/common'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; @@ -13,9 +10,6 @@ type CaptchaResponse = { @Injectable() export class CaptchaService { constructor( - @Inject(DI.config) - private config: Config, - private httpRequestService: HttpRequestService, ) { } @@ -32,9 +26,6 @@ export class CaptchaService { { method: 'POST', body: params, - headers: { - 'User-Agent': this.config.userAgent, - }, }, { noOkError: true, diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 2f17fa389a..0ae1ee32b2 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -35,6 +35,7 @@ import { PushNotificationService } from './PushNotificationService.js'; import { QueryService } from './QueryService.js'; import { ReactionService } from './ReactionService.js'; import { RelayService } from './RelayService.js'; +import { RoleService } from './RoleService.js'; import { S3Service } from './S3Service.js'; import { SignupService } from './SignupService.js'; import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js'; @@ -97,6 +98,7 @@ import { UserGroupInvitationEntityService } from './entities/UserGroupInvitation import { UserListEntityService } from './entities/UserListEntityService.js'; import { FlashEntityService } from './entities/FlashEntityService.js'; import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js'; +import { RoleEntityService } from './entities/RoleEntityService.js'; import { ApAudienceService } from './activitypub/ApAudienceService.js'; import { ApDbResolverService } from './activitypub/ApDbResolverService.js'; import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js'; @@ -158,6 +160,7 @@ const $PushNotificationService: Provider = { provide: 'PushNotificationService', const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService }; const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService }; const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService }; +const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService }; const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service }; const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService }; const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService }; @@ -220,6 +223,7 @@ const $UserGroupInvitationEntityService: Provider = { provide: 'UserGroupInvitat const $UserListEntityService: Provider = { provide: 'UserListEntityService', useExisting: UserListEntityService }; const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService }; const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService }; +const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService }; const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService }; const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService }; @@ -283,6 +287,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting QueryService, ReactionService, RelayService, + RoleService, S3Service, SignupService, TwoFactorAuthenticationService, @@ -344,6 +349,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting UserListEntityService, FlashEntityService, FlashLikeEntityService, + RoleEntityService, ApAudienceService, ApDbResolverService, ApDeliverManagerService, @@ -402,6 +408,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $QueryService, $ReactionService, $RelayService, + $RoleService, $S3Service, $SignupService, $TwoFactorAuthenticationService, @@ -463,6 +470,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $UserListEntityService, $FlashEntityService, $FlashLikeEntityService, + $RoleEntityService, $ApAudienceService, $ApDbResolverService, $ApDeliverManagerService, @@ -522,6 +530,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting QueryService, ReactionService, RelayService, + RoleService, S3Service, SignupService, TwoFactorAuthenticationService, @@ -582,6 +591,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting UserListEntityService, FlashEntityService, FlashLikeEntityService, + RoleEntityService, ApAudienceService, ApDbResolverService, ApDeliverManagerService, @@ -640,6 +650,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $QueryService, $ReactionService, $RelayService, + $RoleService, $S3Service, $SignupService, $TwoFactorAuthenticationService, @@ -700,6 +711,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $UserListEntityService, $FlashEntityService, $FlashLikeEntityService, + $RoleEntityService, $ApAudienceService, $ApDbResolverService, $ApDeliverManagerService, diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts index 1e753f65cc..8f887d90f9 100644 --- a/packages/backend/src/core/CreateSystemUserService.ts +++ b/packages/backend/src/core/CreateSystemUserService.ts @@ -53,7 +53,7 @@ export class CreateSystemUserService { usernameLower: username.toLowerCase(), host: null, token: secret, - isAdmin: false, + isRoot: false, isLocked: true, isExplorable: false, isBot: true, diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index e42c738707..0ac12857c9 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -23,6 +23,9 @@ export class DeleteAccountService { id: string; host: string | null; }): Promise { + const _user = await this.usersRepository.findOneByOrFail({ id: user.id }); + if (_user.isRoot) throw new Error('cannot delete a root account'); + // 物理削除する前にDelete activityを送信する await this.userSuspendService.doPostSuspend(user).catch(e => {}); diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 44bfdb9ffe..d63310a045 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -65,15 +65,7 @@ export class DownloadService { const operationTimeout = 60 * 1000; const maxSize = this.config.maxFileSize ?? 262144000; - const response = await this.undiciFetcher.fetch( - url, - { - method: 'GET', - headers: { - 'User-Agent': this.config.userAgent, - }, - } - ); + const response = await this.undiciFetcher.fetch(url); if (response.body === null) { throw new StatusError('No body', 400, 'No body'); diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index bbdb5fae83..9002c96a65 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -32,11 +32,12 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { FileInfoService } from '@/core/FileInfoService.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; import type S3 from 'aws-sdk/clients/s3.js'; type AddFileArgs = { /** User who wish to add file */ - user: { id: User['id']; host: User['host']; driveCapacityOverrideMb: User['driveCapacityOverrideMb'] } | null; + user: { id: User['id']; host: User['host'] } | null; /** File path */ path: string; /** Name */ @@ -62,7 +63,7 @@ type AddFileArgs = { type UploadFromUrlArgs = { url: string; - user: { id: User['id']; host: User['host']; driveCapacityOverrideMb: User['driveCapacityOverrideMb'] } | null; + user: { id: User['id']; host: User['host'] } | null; folderId?: DriveFolder['id'] | null; uri?: string | null; sensitive?: boolean; @@ -106,6 +107,7 @@ export class DriveService { private videoProcessingService: VideoProcessingService, private globalEventService: GlobalEventService, private queueService: QueueService, + private roleService: RoleService, private driveChart: DriveChart, private perUserDriveChart: PerUserDriveChart, private instanceChart: InstanceChart, @@ -373,8 +375,19 @@ export class DriveService { partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024, }); - const result = await upload.promise(); - if (result) this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`); + await upload.promise() + .then( + result => { + if (result) { + this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`); + } else { + this.registerLogger.error(`Upload Result Empty: key = ${key}, filename = ${filename}`); + } + }, + err => { + this.registerLogger.error(`Upload Failed: key = ${key}, filename = ${filename}`, err); + }, + ); } @bindThis @@ -460,19 +473,16 @@ export class DriveService { } } + this.registerLogger.debug(`ADD DRIVE FILE: user ${user?.id ?? 'not set'}, name ${detectedName}, tmp ${path}`); + //#region Check drive usage if (user && !isLink) { const usage = await this.driveFileEntityService.calcDriveUsageOf(user); - const u = await this.usersRepository.findOneBy({ id: user.id }); - const instance = await this.metaService.fetch(); - let driveCapacity = 1024 * 1024 * (this.userEntityService.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); - - if (this.userEntityService.isLocalUser(user) && u?.driveCapacityOverrideMb != null) { - driveCapacity = 1024 * 1024 * u.driveCapacityOverrideMb; - this.registerLogger.debug('drive capacity override applied'); - this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); - } + const role = await this.roleService.getUserRoleOptions(user.id); + const driveCapacity = 1024 * 1024 * role.driveCapacityMb; + this.registerLogger.debug('drive capacity override applied'); + this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); this.registerLogger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index f6756ad465..e7d106a285 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -428,13 +428,13 @@ export class FileInfoService { .raw() .ensureAlpha() .resize(64, 64, { fit: 'inside' }) - .toBuffer((err, buffer, { width, height }) => { + .toBuffer((err, buffer, info) => { if (err) return reject(err); let hash; try { - hash = encode(new Uint8ClampedArray(buffer), width, height, 5, 5); + hash = encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5); } catch (e) { return reject(e); } diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 632e8f98ae..8639b5713d 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -120,6 +120,10 @@ export class UndiciFetcher { const res = await undici.fetch(url, { dispatcher: this.getAgentByUrl(new URL(url), privateOptions.bypassProxy), ...options, + headers: { + 'User-Agent': this.userAgent ?? '', + ...(options.headers ?? {}), + }, }).catch((err) => { this.logger?.error('fetch error', err); throw new StatusError('Resource Unreachable', 500, 'Resource Unreachable'); @@ -136,7 +140,6 @@ export class UndiciFetcher { url, { headers: Object.assign({ - 'User-Agent': this.userAgent, Accept: accept, }, headers ?? {}), } @@ -151,7 +154,6 @@ export class UndiciFetcher { url, { headers: Object.assign({ - 'User-Agent': this.userAgent, Accept: accept, }, headers ?? {}), } @@ -219,7 +221,7 @@ export class HttpRequestService { }, } - this.maxSockets = Math.max(256, this.config.deliverJobConcurrency ?? 128); + this.maxSockets = Math.max(64, this.config.deliverJobConcurrency ?? 128); this.defaultFetcher = new UndiciFetcher(this.getStandardUndiciFetcherOption(), this.logger); @@ -269,11 +271,6 @@ export class HttpRequestService { //#endregion } - /** - * Get http agent by URL - * @param url URL - * @param bypassProxy Allways bypass proxy - */ @bindThis public getStandardUndiciFetcherOption(opts: undici.Agent.Options = {}, proxyOpts: undici.Agent.Options = {}) { return { @@ -290,6 +287,7 @@ export class HttpRequestService { } } } : {}), + userAgent: this.config.userAgent, } } diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts index ff05779aee..4b792c083d 100644 --- a/packages/backend/src/core/MetaService.ts +++ b/packages/backend/src/core/MetaService.ts @@ -4,8 +4,9 @@ import Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import { Meta } from '@/models/entities/Meta.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class MetaService implements OnApplicationShutdown { @@ -40,7 +41,7 @@ export class MetaService implements OnApplicationShutdown { const obj = JSON.parse(data); if (obj.channel === 'internal') { - const { type, body } = obj.message; + const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'metaUpdated': { this.cache = body; diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 6038840406..112b84fdf9 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -42,6 +42,7 @@ import { NoteReadService } from '@/core/NoteReadService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { bindThis } from '@/decorators.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { RoleService } from '@/core/RoleService.js'; const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); @@ -186,6 +187,7 @@ export class NoteCreateService { private remoteUserResolveService: RemoteUserResolveService, private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, + private roleService: RoleService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private activeUsersChart: ActiveUsersChart, @@ -197,7 +199,6 @@ export class NoteCreateService { id: User['id']; username: User['username']; host: User['host']; - isSilenced: User['isSilenced']; createdAt: User['createdAt']; isBot: User['isBot']; }, data: Option, silent = false): Promise { @@ -224,9 +225,10 @@ export class NoteCreateService { if (data.channel != null) data.visibleUsers = []; if (data.channel != null) data.localOnly = true; - // サイレンス - if (user.isSilenced && data.visibility === 'public' && data.channel == null) { - data.visibility = 'home'; + if (data.visibility === 'public' && data.channel == null) { + if ((await this.roleService.getUserRoleOptions(user.id)).canPublicNote === false) { + data.visibility = 'home'; + } } // Renote対象が「ホームまたは全体」以外の公開範囲ならreject @@ -418,7 +420,6 @@ export class NoteCreateService { id: User['id']; username: User['username']; host: User['host']; - isSilenced: User['isSilenced']; createdAt: User['createdAt']; isBot: User['isBot']; }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { diff --git a/packages/backend/src/core/NotePiningService.ts b/packages/backend/src/core/NotePiningService.ts index f8997574a7..bc038e17a7 100644 --- a/packages/backend/src/core/NotePiningService.ts +++ b/packages/backend/src/core/NotePiningService.ts @@ -12,6 +12,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; @Injectable() export class NotePiningService { @@ -30,6 +31,7 @@ export class NotePiningService { private userEntityService: UserEntityService, private idService: IdService, + private roleService: RoleService, private relayService: RelayService, private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, @@ -55,7 +57,7 @@ export class NotePiningService { const pinings = await this.userNotePiningsRepository.findBy({ userId: user.id }); - if (pinings.length >= 5) { + if (pinings.length >= (await this.roleService.getUserRoleOptions(user.id)).pinLimit) { throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.'); } diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts index 3cc9b0cc9b..abc598ab76 100644 --- a/packages/backend/src/core/PollService.ts +++ b/packages/backend/src/core/PollService.ts @@ -92,13 +92,6 @@ export class PollService { choice: choice, userId: user.id, }); - - // Notify - this.createNotificationService.createNotification(note.userId, 'pollVote', { - notifierId: user.id, - noteId: note.id, - choice: choice, - }); } @bindThis diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts new file mode 100644 index 0000000000..9fd612c96e --- /dev/null +++ b/packages/backend/src/core/RoleService.ts @@ -0,0 +1,286 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; +import { In } from 'typeorm'; +import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; +import { Cache } from '@/misc/cache.js'; +import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/models/entities/User.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; +import { UserCacheService } from '@/core/UserCacheService.js'; +import type { RoleCondFormulaValue } from '@/models/entities/Role.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +export type RoleOptions = { + gtlAvailable: boolean; + ltlAvailable: boolean; + canPublicNote: boolean; + canInvite: boolean; + canManageCustomEmojis: boolean; + driveCapacityMb: number; + pinLimit: number; + antennaLimit: number; + wordMuteLimit: number; + webhookLimit: number; + clipLimit: number; + noteEachClipsLimit: number; + userListLimit: number; + userEachUserListsLimit: number; + rateLimitFactor: number; +}; + +export const DEFAULT_ROLE: RoleOptions = { + gtlAvailable: true, + ltlAvailable: true, + canPublicNote: true, + canInvite: false, + canManageCustomEmojis: false, + driveCapacityMb: 100, + pinLimit: 5, + antennaLimit: 5, + wordMuteLimit: 200, + webhookLimit: 3, + clipLimit: 10, + noteEachClipsLimit: 200, + userListLimit: 10, + userEachUserListsLimit: 50, + rateLimitFactor: 1, +}; + +@Injectable() +export class RoleService implements OnApplicationShutdown { + private rolesCache: Cache; + private roleAssignmentByUserIdCache: Cache; + + constructor( + @Inject(DI.redisSubscriber) + private redisSubscriber: Redis.Redis, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + + private metaService: MetaService, + private userCacheService: UserCacheService, + private userEntityService: UserEntityService, + ) { + //this.onMessage = this.onMessage.bind(this); + + this.rolesCache = new Cache(Infinity); + this.roleAssignmentByUserIdCache = new Cache(Infinity); + + this.redisSubscriber.on('message', this.onMessage); + } + + @bindThis + private async onMessage(_: string, data: string): Promise { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message as StreamMessages['internal']['payload']; + switch (type) { + case 'roleCreated': { + const cached = this.rolesCache.get(null); + if (cached) { + body.createdAt = new Date(body.createdAt); + body.updatedAt = new Date(body.updatedAt); + body.lastUsedAt = new Date(body.lastUsedAt); + cached.push(body); + } + break; + } + case 'roleUpdated': { + const cached = this.rolesCache.get(null); + if (cached) { + const i = cached.findIndex(x => x.id === body.id); + if (i > -1) { + body.createdAt = new Date(body.createdAt); + body.updatedAt = new Date(body.updatedAt); + body.lastUsedAt = new Date(body.lastUsedAt); + cached[i] = body; + } + } + break; + } + case 'roleDeleted': { + const cached = this.rolesCache.get(null); + if (cached) { + this.rolesCache.set(null, cached.filter(x => x.id !== body.id)); + } + break; + } + case 'userRoleAssigned': { + const cached = this.roleAssignmentByUserIdCache.get(body.userId); + if (cached) { + body.createdAt = new Date(body.createdAt); + cached.push(body); + } + break; + } + case 'userRoleUnassigned': { + const cached = this.roleAssignmentByUserIdCache.get(body.userId); + if (cached) { + this.roleAssignmentByUserIdCache.set(body.userId, cached.filter(x => x.id !== body.id)); + } + break; + } + default: + break; + } + } + } + + @bindThis + private evalCond(user: User, value: RoleCondFormulaValue): boolean { + try { + switch (value.type) { + case 'and': { + return value.values.every(v => this.evalCond(user, v)); + } + case 'or': { + return value.values.some(v => this.evalCond(user, v)); + } + case 'not': { + return !this.evalCond(user, value.value); + } + case 'isLocal': { + return this.userEntityService.isLocalUser(user); + } + case 'isRemote': { + return this.userEntityService.isRemoteUser(user); + } + case 'createdLessThan': { + return user.createdAt.getTime() > (Date.now() - (value.sec * 1000)); + } + case 'createdMoreThan': { + return user.createdAt.getTime() < (Date.now() - (value.sec * 1000)); + } + case 'followersLessThanOrEq': { + return user.followersCount <= value.value; + } + case 'followersMoreThanOrEq': { + return user.followersCount >= value.value; + } + case 'followingLessThanOrEq': { + return user.followingCount <= value.value; + } + case 'followingMoreThanOrEq': { + return user.followingCount >= value.value; + } + default: + return false; + } + } catch (err) { + // TODO: log error + return false; + } + } + + @bindThis + public async getUserRoles(userId: User['id']) { + const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); + const assignedRoleIds = assigns.map(x => x.roleId); + const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); + const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id)); + const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null; + const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula)); + return [...assignedRoles, ...matchedCondRoles]; + } + + @bindThis + public async getUserRoleOptions(userId: User['id'] | null): Promise { + const meta = await this.metaService.fetch(); + const baseRoleOptions = { ...DEFAULT_ROLE, ...meta.defaultRoleOverride }; + + if (userId == null) return baseRoleOptions; + + const roles = await this.getUserRoles(userId); + + function getOptionValues(option: keyof RoleOptions) { + if (roles.length === 0) return [baseRoleOptions[option]]; + return roles.map(role => (role.options[option] && (role.options[option].useDefault !== true)) ? role.options[option].value : baseRoleOptions[option]); + } + + return { + gtlAvailable: getOptionValues('gtlAvailable').some(x => x === true), + ltlAvailable: getOptionValues('ltlAvailable').some(x => x === true), + canPublicNote: getOptionValues('canPublicNote').some(x => x === true), + canInvite: getOptionValues('canInvite').some(x => x === true), + canManageCustomEmojis: getOptionValues('canManageCustomEmojis').some(x => x === true), + driveCapacityMb: Math.max(...getOptionValues('driveCapacityMb')), + pinLimit: Math.max(...getOptionValues('pinLimit')), + antennaLimit: Math.max(...getOptionValues('antennaLimit')), + wordMuteLimit: Math.max(...getOptionValues('wordMuteLimit')), + webhookLimit: Math.max(...getOptionValues('webhookLimit')), + clipLimit: Math.max(...getOptionValues('clipLimit')), + noteEachClipsLimit: Math.max(...getOptionValues('noteEachClipsLimit')), + userListLimit: Math.max(...getOptionValues('userListLimit')), + userEachUserListsLimit: Math.max(...getOptionValues('userEachUserListsLimit')), + rateLimitFactor: Math.max(...getOptionValues('rateLimitFactor')), + }; + } + + @bindThis + public async isModerator(user: { id: User['id']; isRoot: User['isRoot'] } | null): Promise { + if (user == null) return false; + return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isModerator || r.isAdministrator); + } + + @bindThis + public async isAdministrator(user: { id: User['id']; isRoot: User['isRoot'] } | null): Promise { + if (user == null) return false; + return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator); + } + + @bindThis + public async getModeratorIds(includeAdmins = true): Promise { + const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); + const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator); + const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ + roleId: In(moderatorRoles.map(r => r.id)), + }) : []; + // TODO: isRootなアカウントも含める + return assigns.map(a => a.userId); + } + + @bindThis + public async getModerators(includeAdmins = true): Promise { + const ids = await this.getModeratorIds(includeAdmins); + const users = ids.length > 0 ? await this.usersRepository.findBy({ + id: In(ids), + }) : []; + return users; + } + + @bindThis + public async getAdministratorIds(): Promise { + const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); + const administratorRoles = roles.filter(r => r.isAdministrator); + const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ + roleId: In(administratorRoles.map(r => r.id)), + }) : []; + // TODO: isRootなアカウントも含める + return assigns.map(a => a.userId); + } + + @bindThis + public async getAdministrators(): Promise { + const ids = await this.getAdministratorIds(); + const users = ids.length > 0 ? await this.usersRepository.findBy({ + id: In(ids), + }) : []; + return users; + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined) { + this.redisSubscriber.off('message', this.onMessage); + } +} diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 9cf203566d..90a7186909 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -11,10 +11,10 @@ import { IdService } from '@/core/IdService.js'; import { UserKeypair } from '@/models/entities/UserKeypair.js'; import { UsedUsername } from '@/models/entities/UsedUsername.js'; import generateUserToken from '@/misc/generate-native-user-token.js'; -import UsersChart from './chart/charts/users.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { UtilityService } from './UtilityService.js'; import { bindThis } from '@/decorators.js'; +import UsersChart from './chart/charts/users.js'; +import { UtilityService } from './UtilityService.js'; @Injectable() export class SignupService { @@ -112,7 +112,7 @@ export class SignupService { usernameLower: username.toLowerCase(), host: this.utilityService.toPunyNullable(host), token: secret, - isAdmin: (await this.usersRepository.countBy({ + isRoot: (await this.usersRepository.countBy({ host: IsNull(), })) === 0, })); diff --git a/packages/backend/src/core/UserCacheService.ts b/packages/backend/src/core/UserCacheService.ts index 423c8993e3..29a64f5848 100644 --- a/packages/backend/src/core/UserCacheService.ts +++ b/packages/backend/src/core/UserCacheService.ts @@ -2,11 +2,12 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import type { UsersRepository } from '@/models/index.js'; import { Cache } from '@/misc/cache.js'; -import type { CacheableLocalUser, CacheableUser, ILocalUser } from '@/models/entities/User.js'; +import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class UserCacheService implements OnApplicationShutdown { @@ -39,11 +40,9 @@ export class UserCacheService implements OnApplicationShutdown { const obj = JSON.parse(data); if (obj.channel === 'internal') { - const { type, body } = obj.message; + const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'userChangeSuspendedState': - case 'userChangeSilencedState': - case 'userChangeModeratorState': case 'remoteUserUpdated': { const user = await this.usersRepository.findOneByOrFail({ id: body.id }); this.userByIdCache.set(user.id, user); @@ -64,12 +63,24 @@ export class UserCacheService implements OnApplicationShutdown { this.localUserByNativeTokenCache.set(body.newToken, user); break; } + case 'follow': { + const follower = this.userByIdCache.get(body.followerId); + if (follower) follower.followingCount++; + const followee = this.userByIdCache.get(body.followeeId); + if (followee) followee.followersCount++; + break; + } default: break; } } } + @bindThis + public findById(userId: User['id']) { + return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId })); + } + @bindThis public onApplicationShutdown(signal?: string | undefined) { this.redisSubscriber.off('message', this.onMessage); diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 52834c375e..f1ce311cea 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -62,6 +62,7 @@ export class UserFollowingService { private federatedInstanceService: FederatedInstanceService, private webhookService: WebhookService, private apRendererService: ApRendererService, + private globalEventService: GlobalEventService, private perUserFollowingChart: PerUserFollowingChart, private instanceChart: InstanceChart, ) { @@ -195,6 +196,8 @@ export class UserFollowingService { } if (alreadyFollowed) return; + + this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id }); //#region Increment counts await Promise.all([ @@ -314,6 +317,8 @@ export class UserFollowingService { follower: {id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, ): Promise { + this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id }); + //#region Decrement following / followers counts await Promise.all([ this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1), diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index 054387ff8e..18c9787fa8 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; @Injectable() export class UserListService { @@ -23,13 +24,21 @@ export class UserListService { private userEntityService: UserEntityService, private idService: IdService, private userFollowingService: UserFollowingService, + private roleService: RoleService, private globalEventServie: GlobalEventService, private proxyAccountService: ProxyAccountService, ) { } @bindThis - public async push(target: User, list: UserList) { + public async push(target: User, list: UserList, me: User) { + const currentCount = await this.userListJoiningsRepository.countBy({ + userListId: list.id, + }); + if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).userEachUserListsLimit) { + throw new Error('Too many users'); + } + await this.userListJoiningsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 1412e6e9aa..d00708a442 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -24,6 +24,12 @@ export class UtilityService { return this.toPuny(this.config.host) === this.toPuny(host); } + @bindThis + public isBlockedHost(blockedHosts: string[], host: string | null): boolean { + if (host == null) return false; + return blockedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); + } + @bindThis public extractDbHost(uri: string): string { const url = new URL(uri); diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts index 91a39f1359..36110490a0 100644 --- a/packages/backend/src/core/WebhookService.ts +++ b/packages/backend/src/core/WebhookService.ts @@ -3,8 +3,9 @@ import Redis from 'ioredis'; import type { WebhooksRepository } from '@/models/index.js'; import type { Webhook } from '@/models/entities/Webhook.js'; import { DI } from '@/di-symbols.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class WebhookService implements OnApplicationShutdown { @@ -39,7 +40,7 @@ export class WebhookService implements OnApplicationShutdown { const obj = JSON.parse(data); if (obj.channel === 'internal') { - const { type, body } = obj.message; + const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'webhookCreated': if (body.active) { diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 1f28fb3a07..1d0c2d5da4 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -159,7 +159,7 @@ export class ApDbResolverService { if (key == null) return null; return { - user: await this.userCacheService.userByIdCache.fetch(key.userId, () => this.usersRepository.findOneByOrFail({ id: key.userId })) as CacheableRemoteUser, + user: await this.userCacheService.findById(key.userId) as CacheableRemoteUser, key, }; } diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 79a917426a..76c8bf68df 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -291,7 +291,7 @@ export class ApInboxService { // アナウンス先をブロックしてたら中断 const meta = await this.metaService.fetch(); - if (meta.blockedHosts.includes(this.utilityService.extractDbHost(uri))) return; + if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return; const unlock = await this.appLockService.getApLock(uri); diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index c686a7bfd8..d44d06a442 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -6,7 +6,9 @@ import type { Config } from '@/config.js'; import type { User } from '@/models/entities/User.js'; import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js'; import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js'; +import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; +import type Logger from '@/logger.js'; type Request = { url: string; @@ -29,6 +31,7 @@ type PrivateKey = { @Injectable() export class ApRequestService { private undiciFetcher: UndiciFetcher; + private logger: Logger; constructor( @Inject(DI.config) @@ -36,10 +39,12 @@ export class ApRequestService { private userKeypairStoreService: UserKeypairStoreService, private httpRequestService: HttpRequestService, + private loggerService: LoggerService, ) { + this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption({ maxRedirections: 0, - })); + }), this.logger ); } @bindThis @@ -153,7 +158,6 @@ export class ApRequestService { url, body, additionalHeaders: { - 'User-Agent': this.config.userAgent, }, }); @@ -183,7 +187,6 @@ export class ApRequestService { }, url, additionalHeaders: { - 'User-Agent': this.config.userAgent, }, }); diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 2f8c97d375..e51ae37954 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -12,12 +12,15 @@ import { isCollectionOrOrderedCollection } from './type.js'; import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; import { ApRequestService } from './ApRequestService.js'; +import { LoggerService } from '@/core/LoggerService.js'; import type { IObject, ICollection, IOrderedCollection } from './type.js'; +import type Logger from '@/logger.js'; export class Resolver { private history: Set; private user?: ILocalUser; private undiciFetcher: UndiciFetcher; + private logger: Logger; constructor( private config: Config, @@ -32,12 +35,14 @@ export class Resolver { private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, private apDbResolverService: ApDbResolverService, + private loggerService: LoggerService, private recursionLimit = 100, ) { this.history = new Set(); + this.logger = this.loggerService?.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption({ maxRedirections: 0, - })); + }), this.logger); } @bindThis @@ -91,7 +96,7 @@ export class Resolver { } const meta = await this.metaService.fetch(); - if (meta.blockedHosts.includes(host)) { + if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) { throw new Error('Instance is blocked'); } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index e1d93a08b0..c9192f53b7 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -324,7 +324,7 @@ export class ApNoteService { // ブロックしてたら中断 const meta = await this.metaService.fetch(); - if (meta.blockedHosts.includes(this.utilityService.extractDbHost(uri))) throw { statusCode: 451 }; + if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw { statusCode: 451 }; const unlock = await this.appLockService.getApLock(uri); diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts index b8012809f7..ae4eb6e48d 100644 --- a/packages/backend/src/core/chart/charts/federation.ts +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -61,21 +61,21 @@ export default class FederationChart extends Chart { this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT IN (:...blocked)', { blocked: meta.blockedHosts }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followerHost)') .where('following.followerHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT IN (:...blocked)', { blocked: meta.blockedHosts }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT IN (:...blocked)', { blocked: meta.blockedHosts }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`) .setParameters(pubsubSubQuery.getParameters()) @@ -84,7 +84,7 @@ export default class FederationChart extends Chart { this.instancesRepository.createQueryBuilder('instance') .select('COUNT(instance.id)') .where(`instance.host IN (${ subInstancesQuery.getQuery() })`) - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere('instance.isSuspended = false') .andWhere('instance.isNotResponding = false') .getRawOne() @@ -92,7 +92,7 @@ export default class FederationChart extends Chart { this.instancesRepository.createQueryBuilder('instance') .select('COUNT(instance.id)') .where(`instance.host IN (${ pubInstancesQuery.getQuery() })`) - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere('instance.isSuspended = false') .andWhere('instance.isNotResponding = false') .getRawOne() diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 8a2dc70eda..2a4e09519f 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -22,23 +22,25 @@ export class EmojiEntityService { @bindThis public async pack( src: Emoji['id'] | Emoji, + opts: { omitHost?: boolean; omitId?: boolean; } = {}, ): Promise> { const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); return { - id: emoji.id, + id: opts.omitId ? undefined : emoji.id, aliases: emoji.aliases, name: emoji.name, category: emoji.category, - host: emoji.host, + host: opts.omitHost ? undefined : emoji.host, }; } @bindThis public packMany( emojis: any[], + opts: { omitHost?: boolean; omitId?: boolean; } = {}, ) { - return Promise.all(emojis.map(x => this.pack(x))); + return Promise.all(emojis.map(x => this.pack(x, opts))); } } diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 81d02bb331..c8bb74debf 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -7,8 +7,8 @@ import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { Instance } from '@/models/entities/Instance.js'; import { MetaService } from '@/core/MetaService.js'; +import { UtilityService } from '../UtilityService.js'; import { bindThis } from '@/decorators.js'; -import { UserEntityService } from './UserEntityService.js'; @Injectable() export class InstanceEntityService { @@ -17,6 +17,8 @@ export class InstanceEntityService { private instancesRepository: InstancesRepository, private metaService: MetaService, + + private utilityService: UtilityService, ) { } @@ -35,7 +37,7 @@ export class InstanceEntityService { followersCount: instance.followersCount, isNotResponding: instance.isNotResponding, isSuspended: instance.isSuspended, - isBlocked: meta.blockedHosts.includes(instance.host), + isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host), softwareName: instance.softwareName, softwareVersion: instance.softwareVersion, openRegistrations: instance.openRegistrations, diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 208d653877..a1c2c9cffb 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -98,7 +98,7 @@ export class NotificationEntityService implements OnModuleInit { }), reaction: notification.reaction, } : {}), - ...(notification.type === 'pollVote' ? { + ...(notification.type === 'pollVote' ? { // TODO: そのうち消す note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { detail: true, _hint_: options._hintForEachNotes_, diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts new file mode 100644 index 0000000000..7db7ed6695 --- /dev/null +++ b/packages/backend/src/core/entities/RoleEntityService.ts @@ -0,0 +1,83 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/schema.js'; +import type { User } from '@/models/entities/User.js'; +import type { Role } from '@/models/entities/Role.js'; +import { bindThis } from '@/decorators.js'; +import { DEFAULT_ROLE } from '@/core/RoleService.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class RoleEntityService { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + + private userEntityService: UserEntityService, + ) { + } + + @bindThis + public async pack( + src: Role['id'] | Role, + me?: { id: User['id'] } | null | undefined, + options?: { + detail?: boolean; + }, + ) { + const opts = Object.assign({ + detail: true, + }, options); + + const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src }); + + const assigns = await this.roleAssignmentsRepository.findBy({ + roleId: role.id, + }); + + const roleOptions = { ...role.options }; + for (const [k, v] of Object.entries(DEFAULT_ROLE)) { + if (roleOptions[k] == null) roleOptions[k] = { + useDefault: true, + value: v, + }; + } + + return await awaitAll({ + id: role.id, + createdAt: role.createdAt.toISOString(), + updatedAt: role.updatedAt.toISOString(), + name: role.name, + description: role.description, + color: role.color, + target: role.target, + condFormula: role.condFormula, + isPublic: role.isPublic, + isAdministrator: role.isAdministrator, + isModerator: role.isModerator, + canEditMembersByModerator: role.canEditMembersByModerator, + options: roleOptions, + usersCount: assigns.length, + ...(opts.detail ? { + users: this.userEntityService.packMany(assigns.map(x => x.userId), me), + } : {}), + }); + } + + @bindThis + public packMany( + roles: any[], + me: { id: User['id'] }, + options?: { + detail?: boolean; + }, + ) { + return Promise.all(roles.map(x => this.pack(x, me, options))); + } +} + diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index a123746220..6b754150cf 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -13,6 +13,8 @@ import type { Instance } from '@/models/entities/Instance.js'; import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository } from '@/models/index.js'; +import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { AntennaService } from '../AntennaService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js'; @@ -41,7 +43,6 @@ function isRemoteUser(user: T): user is T & { function isRemoteUser(user: User | { host: User['host'] }): boolean { return !isLocalUser(user); } -import { bindThis } from '@/decorators.js'; @Injectable() export class UserEntityService implements OnModuleInit { @@ -50,6 +51,7 @@ export class UserEntityService implements OnModuleInit { private pageEntityService: PageEntityService; private customEmojiService: CustomEmojiService; private antennaService: AntennaService; + private roleService: RoleService; private userInstanceCache: Cache; constructor( @@ -120,6 +122,7 @@ export class UserEntityService implements OnModuleInit { //private pageEntityService: PageEntityService, //private customEmojiService: CustomEmojiService, //private antennaService: AntennaService, + //private roleService: RoleService, ) { this.userInstanceCache = new Cache(1000 * 60 * 60 * 3); } @@ -130,6 +133,7 @@ export class UserEntityService implements OnModuleInit { this.pageEntityService = this.moduleRef.get('PageEntityService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.antennaService = this.moduleRef.get('AntennaService'); + this.roleService = this.moduleRef.get('RoleService'); } //#region Validators @@ -383,6 +387,9 @@ export class UserEntityService implements OnModuleInit { (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : null; + const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null; + const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null; + const falsy = opts.detail ? false : undefined; const packed = { @@ -392,8 +399,6 @@ export class UserEntityService implements OnModuleInit { host: user.host, avatarUrl: this.getAvatarUrlSync(user), avatarBlurhash: user.avatar?.blurhash ?? null, - isAdmin: user.isAdmin ?? falsy, - isModerator: user.isModerator ?? falsy, isBot: user.isBot ?? falsy, isCat: user.isCat ?? falsy, instance: user.host ? this.userInstanceCache.fetch(user.host, @@ -418,7 +423,7 @@ export class UserEntityService implements OnModuleInit { bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner, false) : null, bannerBlurhash: user.banner?.blurhash ?? null, isLocked: user.isLocked, - isSilenced: user.isSilenced ?? falsy, + isSilenced: this.roleService.getUserRoleOptions(user.id).then(r => !r.canPublicNote), isSuspended: user.isSuspended ?? falsy, description: profile!.description, location: profile!.location, @@ -443,14 +448,21 @@ export class UserEntityService implements OnModuleInit { userId: user.id, }).then(result => result >= 1) : false, - ...(isMe || opts.includeSecrets ? { - driveCapacityOverrideMb: user.driveCapacityOverrideMb, - } : {}), + roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).map(role => ({ + id: role.id, + name: role.name, + color: role.color, + description: role.description, + isModerator: role.isModerator, + isAdministrator: role.isAdministrator, + }))), } : {}), ...(opts.detail && isMe ? { avatarId: user.avatarId, bannerId: user.bannerId, + isModerator: isModerator, + isAdmin: isAdmin, injectFeaturedNote: profile!.injectFeaturedNote, receiveAnnouncementEmail: profile!.receiveAnnouncementEmail, alwaysMarkNsfw: profile!.alwaysMarkNsfw, @@ -484,6 +496,7 @@ export class UserEntityService implements OnModuleInit { } : {}), ...(opts.includeSecrets ? { + role: this.roleService.getUserRoleOptions(user.id), email: profile!.email, emailVerified: profile!.emailVerified, securityKeysList: profile!.twoFactorEnabled diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 9719d773ca..3fb0cd4dae 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -69,6 +69,8 @@ export const DI = { adsRepository: Symbol('adsRepository'), passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'), retentionAggregationsRepository: Symbol('retentionAggregationsRepository'), + rolesRepository: Symbol('rolesRepository'), + roleAssignmentsRepository: Symbol('roleAssignmentsRepository'), flashsRepository: Symbol('flashsRepository'), flashLikesRepository: Symbol('flashLikesRepository'), //#endregion diff --git a/packages/backend/src/misc/sql-like-escape.ts b/packages/backend/src/misc/sql-like-escape.ts new file mode 100644 index 0000000000..8470dca3de --- /dev/null +++ b/packages/backend/src/misc/sql-like-escape.ts @@ -0,0 +1,3 @@ +export function sqlLikeEscape(s: string) { + return s.replace(/([%_])/g, '\\$1'); +} diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index a5d5a63931..2a235bc6fc 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash } from './index.js'; +import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -400,6 +400,18 @@ const $flashLikesRepository: Provider = { inject: [DI.db], }; +const $rolesRepository: Provider = { + provide: DI.rolesRepository, + useFactory: (db: DataSource) => db.getRepository(Role), + inject: [DI.db], +}; + +const $roleAssignmentsRepository: Provider = { + provide: DI.roleAssignmentsRepository, + useFactory: (db: DataSource) => db.getRepository(RoleAssignment), + inject: [DI.db], +}; + @Module({ imports: [ ], @@ -468,6 +480,8 @@ const $flashLikesRepository: Provider = { $adsRepository, $passwordResetRequestsRepository, $retentionAggregationsRepository, + $rolesRepository, + $roleAssignmentsRepository, $flashsRepository, $flashLikesRepository, ], @@ -536,6 +550,8 @@ const $flashLikesRepository: Provider = { $adsRepository, $passwordResetRequestsRepository, $retentionAggregationsRepository, + $rolesRepository, + $roleAssignmentsRepository, $flashsRepository, $flashLikesRepository, ], diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts index fb25e370d2..e724ba9a49 100644 --- a/packages/backend/src/models/entities/Meta.ts +++ b/packages/backend/src/models/entities/Meta.ts @@ -42,16 +42,6 @@ export class Meta { }) public disableRegistration: boolean; - @Column('boolean', { - default: false, - }) - public disableLocalTimeline: boolean; - - @Column('boolean', { - default: false, - }) - public disableGlobalTimeline: boolean; - @Column('boolean', { default: false, }) @@ -227,18 +217,6 @@ export class Meta { }) public enableSensitiveMediaDetectionForVideos: boolean; - @Column('integer', { - default: 1024, - comment: 'Drive capacity of a local user (MB)', - }) - public localDriveCapacityMb: number; - - @Column('integer', { - default: 32, - comment: 'Drive capacity of a remote user (MB)', - }) - public remoteDriveCapacityMb: number; - @Column('varchar', { length: 128, nullable: true, @@ -476,4 +454,9 @@ export class Meta { default: true, }) public enableActiveEmailValidation: boolean; + + @Column('jsonb', { + default: { }, + }) + public defaultRoleOverride: Record; } diff --git a/packages/backend/src/models/entities/Notification.ts b/packages/backend/src/models/entities/Notification.ts index 53a7dda43a..6679cdb809 100644 --- a/packages/backend/src/models/entities/Notification.ts +++ b/packages/backend/src/models/entities/Notification.ts @@ -55,11 +55,11 @@ export class Notification { * 通知の種類。 * follow - フォローされた * mention - 投稿で自分が言及された - * reply - (自分または自分がWatchしている)投稿が返信された - * renote - (自分または自分がWatchしている)投稿がRenoteされた - * quote - (自分または自分がWatchしている)投稿が引用Renoteされた - * reaction - (自分または自分がWatchしている)投稿にリアクションされた - * pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された + * reply - 投稿に返信された + * renote - 投稿がRenoteされた + * quote - 投稿が引用Renoteされた + * reaction - 投稿にリアクションされた + * pollVote - 投稿のアンケートに投票された (廃止) * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した * receiveFollowRequest - フォローリクエストされた * followRequestAccepted - 自分の送ったフォローリクエストが承認された diff --git a/packages/backend/src/models/entities/Poll.ts b/packages/backend/src/models/entities/Poll.ts index 6641b435eb..ee1d646020 100644 --- a/packages/backend/src/models/entities/Poll.ts +++ b/packages/backend/src/models/entities/Poll.ts @@ -24,7 +24,7 @@ export class Poll { public multiple: boolean; @Column('varchar', { - length: 128, array: true, default: '{}', + length: 256, array: true, default: '{}', }) public choices: string[]; diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/entities/Role.ts new file mode 100644 index 0000000000..a18df40d0c --- /dev/null +++ b/packages/backend/src/models/entities/Role.ts @@ -0,0 +1,143 @@ +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; + +type CondFormulaValueAnd = { + type: 'and'; + values: RoleCondFormulaValue[]; +}; + +type CondFormulaValueOr = { + type: 'or'; + values: RoleCondFormulaValue[]; +}; + +type CondFormulaValueNot = { + type: 'not'; + value: RoleCondFormulaValue; +}; + +type CondFormulaValueIsLocal = { + type: 'isLocal'; +}; + +type CondFormulaValueIsRemote = { + type: 'isRemote'; +}; + +type CondFormulaValueCreatedLessThan = { + type: 'createdLessThan'; + sec: number; +}; + +type CondFormulaValueCreatedMoreThan = { + type: 'createdMoreThan'; + sec: number; +}; + +type CondFormulaValueFollowersLessThanOrEq = { + type: 'followersLessThanOrEq'; + value: number; +}; + +type CondFormulaValueFollowersMoreThanOrEq = { + type: 'followersMoreThanOrEq'; + value: number; +}; + +type CondFormulaValueFollowingLessThanOrEq = { + type: 'followingLessThanOrEq'; + value: number; +}; + +type CondFormulaValueFollowingMoreThanOrEq = { + type: 'followingMoreThanOrEq'; + value: number; +}; + +export type RoleCondFormulaValue = + CondFormulaValueAnd | + CondFormulaValueOr | + CondFormulaValueNot | + CondFormulaValueIsLocal | + CondFormulaValueIsRemote | + CondFormulaValueCreatedLessThan | + CondFormulaValueCreatedMoreThan | + CondFormulaValueFollowersLessThanOrEq | + CondFormulaValueFollowersMoreThanOrEq | + CondFormulaValueFollowingLessThanOrEq | + CondFormulaValueFollowingMoreThanOrEq; + +@Entity() +export class Role { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the Role.', + }) + public createdAt: Date; + + @Column('timestamp with time zone', { + comment: 'The updated date of the Role.', + }) + public updatedAt: Date; + + @Column('timestamp with time zone', { + comment: 'The last used date of the Role.', + }) + public lastUsedAt: Date; + + @Column('varchar', { + length: 256, + }) + public name: string; + + @Column('varchar', { + length: 1024, + }) + public description: string; + + @Column('varchar', { + length: 256, nullable: true, + }) + public color: string | null; + + @Column('enum', { + enum: ['manual', 'conditional'], + default: 'manual', + }) + public target: 'manual' | 'conditional'; + + @Column('jsonb', { + default: { }, + }) + public condFormula: RoleCondFormulaValue; + + @Column('boolean', { + default: false, + }) + public isPublic: boolean; + + @Column('boolean', { + default: false, + }) + public isModerator: boolean; + + @Column('boolean', { + default: false, + }) + public isAdministrator: boolean; + + @Column('boolean', { + default: false, + }) + public canEditMembersByModerator: boolean; + + @Column('jsonb', { + default: { }, + }) + public options: Record; +} diff --git a/packages/backend/src/models/entities/RoleAssignment.ts b/packages/backend/src/models/entities/RoleAssignment.ts new file mode 100644 index 0000000000..e86f2a8999 --- /dev/null +++ b/packages/backend/src/models/entities/RoleAssignment.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { Role } from './Role.js'; +import { User } from './User.js'; + +@Entity() +@Index(['userId', 'roleId'], { unique: true }) +export class RoleAssignment { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the RoleAssignment.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The user ID.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The role ID.', + }) + public roleId: Role['id']; + + @ManyToOne(type => Role, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public role: Role | null; +} diff --git a/packages/backend/src/models/entities/User.ts b/packages/backend/src/models/entities/User.ts index 73736f0150..8bd5c9700d 100644 --- a/packages/backend/src/models/entities/User.ts +++ b/packages/backend/src/models/entities/User.ts @@ -112,12 +112,6 @@ export class User { }) public isSuspended: boolean; - @Column('boolean', { - default: false, - comment: 'Whether the User is silenced.', - }) - public isSilenced: boolean; - @Column('boolean', { default: false, comment: 'Whether the User is locked.', @@ -138,15 +132,9 @@ export class User { @Column('boolean', { default: false, - comment: 'Whether the User is the admin.', + comment: 'Whether the User is the root.', }) - public isAdmin: boolean; - - @Column('boolean', { - default: false, - comment: 'Whether the User is a moderator.', - }) - public isModerator: boolean; + public isRoot: boolean; @Index() @Column('boolean', { @@ -218,12 +206,6 @@ export class User { }) public token: string | null; - @Column('integer', { - nullable: true, - comment: 'Overrides user drive capacity limit', - }) - public driveCapacityOverrideMb: number | null; - constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index b132475747..50697597ad 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -62,6 +62,8 @@ import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js'; import { Webhook } from '@/models/entities/Webhook.js'; import { Channel } from '@/models/entities/Channel.js'; import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js'; +import { Role } from '@/models/entities/Role.js'; +import { RoleAssignment } from '@/models/entities/RoleAssignment.js'; import { Flash } from '@/models/entities/Flash.js'; import { FlashLike } from '@/models/entities/FlashLike.js'; import type { Repository } from 'typeorm'; @@ -131,6 +133,8 @@ export { Webhook, Channel, RetentionAggregation, + Role, + RoleAssignment, Flash, FlashLike, }; @@ -199,5 +203,7 @@ export type UserSecurityKeysRepository = Repository; export type WebhooksRepository = Repository; export type ChannelsRepository = Repository; export type RetentionAggregationsRepository = Repository; +export type RolesRepository = Repository; +export type RoleAssignmentsRepository = Repository; export type FlashsRepository = Repository; export type FlashLikesRepository = Repository; diff --git a/packages/backend/src/models/schema/emoji.ts b/packages/backend/src/models/schema/emoji.ts index 9a52609b68..d897a0fc05 100644 --- a/packages/backend/src/models/schema/emoji.ts +++ b/packages/backend/src/models/schema/emoji.ts @@ -3,7 +3,7 @@ export const packedEmojiSchema = { properties: { id: { type: 'string', - optional: false, nullable: false, + optional: true, nullable: false, format: 'id', example: 'xxxxxxxxxx', }, @@ -26,12 +26,8 @@ export const packedEmojiSchema = { }, host: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, description: 'The local host is represented with `null`.', }, - url: { - type: 'string', - optional: true, nullable: false, - }, }, } as const; diff --git a/packages/backend/src/postgre.ts b/packages/backend/src/postgre.ts index 4f6b157d80..c55cb78a6a 100644 --- a/packages/backend/src/postgre.ts +++ b/packages/backend/src/postgre.ts @@ -70,6 +70,8 @@ import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js'; import { Webhook } from '@/models/entities/Webhook.js'; import { Channel } from '@/models/entities/Channel.js'; import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js'; +import { Role } from '@/models/entities/Role.js'; +import { RoleAssignment } from '@/models/entities/RoleAssignment.js'; import { Flash } from '@/models/entities/Flash.js'; import { FlashLike } from '@/models/entities/FlashLike.js'; @@ -186,6 +188,8 @@ export const entities = [ Webhook, UserIp, RetentionAggregation, + Role, + RoleAssignment, Flash, FlashLike, ...charts, diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index c5e4a66517..10fcb5684f 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -56,7 +56,7 @@ export class DeliverProcessorService { // ブロックしてたら中断 const meta = await this.metaService.fetch(); - if (meta.blockedHosts.includes(this.utilityService.toPuny(host))) { + if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.toPuny(host))) { return 'skip (blocked)'; } diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index 1bec77b837..a9672250c8 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -10,10 +10,10 @@ import { DownloadService } from '@/core/DownloadService.js'; import { UserListService } from '@/core/UserListService.js'; import { IdService } from '@/core/IdService.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DbUserImportJobData } from '../types.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ImportUserListsProcessorService { @@ -102,7 +102,7 @@ export class ImportUserListsProcessorService { if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue; - this.userListService.push(target, list!); + this.userListService.push(target, list!, user); } catch (e) { this.logger.warn(`Error in line:${linenum} ${e}`); } diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index d033637849..f814368a7a 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -76,7 +76,7 @@ export class InboxProcessorService { // ブロックしてたら中断 const meta = await this.metaService.fetch(); - if (meta.blockedHosts.includes(host)) { + if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) { return `Blocked request: ${host}`; } @@ -158,7 +158,7 @@ export class InboxProcessorService { // ブロックしてたら中断 const ldHost = this.utilityService.extractDbHost(authUser.user.uri); - if (meta.blockedHosts.includes(ldHost)) { + if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) { return `Blocked request: ${ldHost}`; } } else { diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 86d87872b2..19380d13a4 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -8,6 +8,9 @@ import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Cache } from '@/misc/cache.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import NotesChart from '@/core/chart/charts/notes.js'; +import UsersChart from '@/core/chart/charts/users.js'; +import { DEFAULT_ROLE } from '@/core/RoleService.js'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; const nodeinfo2_1path = '/nodeinfo/2.1'; @@ -27,6 +30,8 @@ export class NodeinfoServerService { private userEntityService: UserEntityService, private metaService: MetaService, + private notesChart: NotesChart, + private usersChart: UsersChart, ) { //this.createServer = this.createServer.bind(this); } @@ -46,22 +51,31 @@ export class NodeinfoServerService { public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { const nodeinfo2 = async () => { const now = Date.now(); + + const notesChart = await this.notesChart.getChart('hour', 1, null); + const localPosts = notesChart.local.total[0]; + + const usersChart = await this.usersChart.getChart('hour', 1, null); + const total = usersChart.local.total[0]; + const [ meta, - total, - activeHalfyear, - activeMonth, - localPosts, + //activeHalfyear, + //activeMonth, ] = await Promise.all([ this.metaService.fetch(true), - this.usersRepository.count({ where: { host: IsNull() } }), - this.usersRepository.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 15552000000)) } }), - this.usersRepository.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 2592000000)) } }), - this.notesRepository.count({ where: { userHost: IsNull() } }), + // 重い + //this.usersRepository.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 15552000000)) } }), + //this.usersRepository.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 2592000000)) } }), ]); + const activeHalfyear = null; + const activeMonth = null; + const proxyAccount = meta.proxyAccountId ? await this.userEntityService.pack(meta.proxyAccountId).catch(() => null) : null; + const baseRoleOptions = { ...DEFAULT_ROLE, ...meta.defaultRoleOverride }; + return { software: { name: 'misskey', @@ -91,8 +105,8 @@ export class NodeinfoServerService { repositoryUrl: meta.repositoryUrl, feedbackUrl: meta.feedbackUrl, disableRegistration: meta.disableRegistration, - disableLocalTimeline: meta.disableLocalTimeline, - disableGlobalTimeline: meta.disableGlobalTimeline, + disableLocalTimeline: !baseRoleOptions.ltlAvailable, + disableGlobalTimeline: !baseRoleOptions.gtlAvailable, emailRequiredForSignup: meta.emailRequiredForSignup, enableHcaptcha: meta.enableHcaptcha, enableRecaptcha: meta.enableRecaptcha, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index beb26e43d1..fac8497b5e 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -1,12 +1,11 @@ import cluster from 'node:cluster'; import * as fs from 'node:fs'; -import * as http from 'node:http'; import { Inject, Injectable } from '@nestjs/common'; import Fastify from 'fastify'; import { IsNull } from 'typeorm'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { Config } from '@/config.js'; -import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { envOption } from '@/env.js'; @@ -39,6 +38,9 @@ export class ServerService { @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + private userEntityService: UserEntityService, private apiServerService: ApiServerService, private streamingApiServerService: StreamingApiServerService, @@ -77,6 +79,43 @@ export class ServerService { fastify.register(this.nodeinfoServerService.createServer); fastify.register(this.wellKnownServerService.createServer); + fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { + const path = request.params.path; + + if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) { + reply.code(404); + return; + } + + reply.header('Cache-Control', 'public, max-age=86400'); + + const name = path.split('@')[0].replace('.webp', ''); + const host = path.split('@')[1]?.replace('.webp', ''); + + const emoji = await this.emojisRepository.findOneBy({ + // `@.` is the spec of ReactionService.decodeReaction + host: (host == null || host === '.') ? IsNull() : host, + name: name, + }); + + reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); + + if (emoji == null) { + return await reply.redirect('/static-assets/emoji-unknown.png'); + } + + const url = new URL('/proxy/emoji.webp', this.config.url); + // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) + url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); + url.searchParams.set('emoji', '1'); + if ('static' in request.query) url.searchParams.set('static', '1'); + + return await reply.redirect( + 301, + url.toString(), + ); + }); + fastify.get<{ Params: { acct: string } }>('/avatar/@:acct', async (request, reply) => { const { username, host } = Acct.parse(request.params.acct); const user = await this.usersRepository.findOne({ diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 68f43c7dfc..dcc9342a82 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -12,6 +12,7 @@ import type { UserIpsRepository } from '@/models/index.js'; import { MetaService } from '@/core/MetaService.js'; import { createTemp } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from './error.js'; import { RateLimiterService } from './RateLimiterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; @@ -41,6 +42,7 @@ export class ApiCallService implements OnApplicationShutdown { private metaService: MetaService, private authenticateService: AuthenticateService, private rateLimiterService: RateLimiterService, + private roleService: RoleService, private apiLoggerService: ApiLoggerService, ) { this.logger = this.apiLoggerService.logger; @@ -202,7 +204,6 @@ export class ApiCallService implements OnApplicationShutdown { request: FastifyRequest<{ Body: Record | undefined, Querystring: Record }>, ) { const isSecure = user != null && token == null; - const isModerator = user != null && (user.isModerator || user.isAdmin); if (ep.meta.secure && !isSecure) { throw new ApiError(accessDenied); @@ -223,8 +224,11 @@ export class ApiCallService implements OnApplicationShutdown { limit.key = ep.name; } + // TODO: 毎リクエスト計算するのもあれだしキャッシュしたい + const factor = user ? (await this.roleService.getUserRoleOptions(user.id)).rateLimitFactor : 1; + // Rate limit - await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor).catch(err => { + await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor).catch(err => { throw new ApiError({ message: 'Rate limit exceeded. Please try again later.', code: 'RATE_LIMIT_EXCEEDED', @@ -234,30 +238,51 @@ export class ApiCallService implements OnApplicationShutdown { }); } - if (ep.meta.requireCredential && user == null) { - throw new ApiError({ - message: 'Credential required.', - code: 'CREDENTIAL_REQUIRED', - id: '1384574d-a912-4b81-8601-c7b1c4085df1', - httpStatusCode: 401, - }); + if (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin) { + if (user == null) { + throw new ApiError({ + message: 'Credential required.', + code: 'CREDENTIAL_REQUIRED', + id: '1384574d-a912-4b81-8601-c7b1c4085df1', + httpStatusCode: 401, + }); + } else if (user!.isSuspended) { + throw new ApiError({ + message: 'Your account has been suspended.', + code: 'YOUR_ACCOUNT_SUSPENDED', + id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370', + httpStatusCode: 403, + }); + } } - if (ep.meta.requireCredential && user!.isSuspended) { - throw new ApiError({ - message: 'Your account has been suspended.', - code: 'YOUR_ACCOUNT_SUSPENDED', - id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370', - httpStatusCode: 403, - }); + if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) { + const myRoles = await this.roleService.getUserRoles(user!.id); + if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) { + throw new ApiError({ + message: 'You are not assigned to a moderator role.', + code: 'ROLE_PERMISSION_DENIED', + id: 'd33d5333-db36-423d-a8f9-1a2b9549da41', + }); + } + if (ep.meta.requireAdmin && !myRoles.some(r => r.isAdministrator)) { + throw new ApiError({ + message: 'You are not assigned to an administrator role.', + code: 'ROLE_PERMISSION_DENIED', + id: 'c3d38592-54c0-429d-be96-5636b0431a61', + }); + } } - if (ep.meta.requireAdmin && !user!.isAdmin) { - throw new ApiError(accessDenied, { reason: 'You are not the admin.' }); - } - - if (ep.meta.requireModerator && !isModerator) { - throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); + if (ep.meta.requireRoleOption != null && !user!.isRoot) { + const myRole = await this.roleService.getUserRoleOptions(user!.id); + if (!myRole[ep.meta.requireRoleOption]) { + throw new ApiError({ + message: 'You are not assigned to a required role.', + code: 'ROLE_PERMISSION_DENIED', + id: '7f86f06f-7e15-4057-8561-f4b6d4ac755a', + }); + } } if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) { diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index ee09349d80..b29c9616cc 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -36,8 +36,8 @@ export class ApiServerService { private userEntityService: UserEntityService, private apiCallService: ApiCallService, - private signupApiServiceService: SignupApiService, - private signinApiServiceService: SigninApiService, + private signupApiService: SignupApiService, + private signinApiService: SigninApiService, private githubServerService: GithubServerService, private discordServerService: DiscordServerService, private twitterServerService: TwitterServerService, @@ -116,7 +116,7 @@ export class ApiServerService { 'g-recaptcha-response'?: string; 'turnstile-response'?: string; } - }>('/signup', (request, reply) => this.signupApiServiceService.signup(request, reply)); + }>('/signup', (request, reply) => this.signupApiService.signup(request, reply)); fastify.post<{ Body: { @@ -129,9 +129,9 @@ export class ApiServerService { credentialId?: string; challengeId?: string; }; - }>('/signin', (request, reply) => this.signinApiServiceService.signin(request, reply)); + }>('/signin', (request, reply) => this.signinApiService.signin(request, reply)); - fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiServiceService.signupPending(request, reply)); + fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply)); fastify.register(this.discordServerService.create); fastify.register(this.githubServerService.create); diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 60beca4f47..aa88a9dd13 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -37,9 +37,7 @@ import * as ep___admin_federation_updateInstance from './endpoints/admin/federat import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js'; -import * as ep___admin_invite from './endpoints/admin/invite.js'; -import * as ep___admin_moderators_add from './endpoints/admin/moderators/add.js'; -import * as ep___admin_moderators_remove from './endpoints/admin/moderators/remove.js'; +import * as ep___invite from './endpoints/invite.js'; import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; @@ -55,13 +53,19 @@ import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; import * as ep___admin_showUser from './endpoints/admin/show-user.js'; import * as ep___admin_showUsers from './endpoints/admin/show-users.js'; -import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js'; import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js'; -import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js'; import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js'; +import * as ep___admin_roles_create from './endpoints/admin/roles/create.js'; +import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js'; +import * as ep___admin_roles_list from './endpoints/admin/roles/list.js'; +import * as ep___admin_roles_show from './endpoints/admin/roles/show.js'; +import * as ep___admin_roles_update from './endpoints/admin/roles/update.js'; +import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; +import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; +import * as ep___admin_roles_updateDefaultRoleOverride from './endpoints/admin/roles/update-default-role-override.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; import * as ep___antennas_delete from './endpoints/antennas/delete.js'; @@ -220,6 +224,7 @@ import * as ep___messaging_messages_create from './endpoints/messaging/messages/ import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js'; import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js'; import * as ep___meta from './endpoints/meta.js'; +import * as ep___emojis from './endpoints/emojis.js'; import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; import * as ep___mute_create from './endpoints/mute/create.js'; import * as ep___mute_delete from './endpoints/mute/delete.js'; @@ -325,7 +330,6 @@ import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_stats from './endpoints/users/stats.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; -import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js'; import * as ep___retention from './endpoints/retention.js'; import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; @@ -367,9 +371,7 @@ const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federati const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default }; const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default }; const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default }; -const $admin_invite: Provider = { provide: 'ep:admin/invite', useClass: ep___admin_invite.default }; -const $admin_moderators_add: Provider = { provide: 'ep:admin/moderators/add', useClass: ep___admin_moderators_add.default }; -const $admin_moderators_remove: Provider = { provide: 'ep:admin/moderators/remove', useClass: ep___admin_moderators_remove.default }; +const $invite: Provider = { provide: 'ep:invite', useClass: ep___invite.default }; const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default }; const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default }; const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default }; @@ -385,13 +387,19 @@ const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default }; const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default }; const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default }; -const $admin_silenceUser: Provider = { provide: 'ep:admin/silence-user', useClass: ep___admin_silenceUser.default }; const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default }; -const $admin_unsilenceUser: Provider = { provide: 'ep:admin/unsilence-user', useClass: ep___admin_unsilenceUser.default }; const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default }; const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default }; const $admin_deleteAccount: Provider = { provide: 'ep:admin/delete-account', useClass: ep___admin_deleteAccount.default }; const $admin_updateUserNote: Provider = { provide: 'ep:admin/update-user-note', useClass: ep___admin_updateUserNote.default }; +const $admin_roles_create: Provider = { provide: 'ep:admin/roles/create', useClass: ep___admin_roles_create.default }; +const $admin_roles_delete: Provider = { provide: 'ep:admin/roles/delete', useClass: ep___admin_roles_delete.default }; +const $admin_roles_list: Provider = { provide: 'ep:admin/roles/list', useClass: ep___admin_roles_list.default }; +const $admin_roles_show: Provider = { provide: 'ep:admin/roles/show', useClass: ep___admin_roles_show.default }; +const $admin_roles_update: Provider = { provide: 'ep:admin/roles/update', useClass: ep___admin_roles_update.default }; +const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useClass: ep___admin_roles_assign.default }; +const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default }; +const $admin_roles_updateDefaultRoleOverride: Provider = { provide: 'ep:admin/roles/update-default-role-override', useClass: ep___admin_roles_updateDefaultRoleOverride.default }; const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default }; const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default }; const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default }; @@ -550,6 +558,7 @@ const $messaging_messages_create: Provider = { provide: 'ep:messaging/messages/c const $messaging_messages_delete: Provider = { provide: 'ep:messaging/messages/delete', useClass: ep___messaging_messages_delete.default }; const $messaging_messages_read: Provider = { provide: 'ep:messaging/messages/read', useClass: ep___messaging_messages_read.default }; const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default }; +const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default }; const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default }; const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default }; const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default }; @@ -654,7 +663,6 @@ const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by- const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default }; const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default }; const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default }; -const $admin_driveCapOverride: Provider = { provide: 'ep:admin/drive-capacity-override', useClass: ep___admin_driveCapOverride.default }; const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default }; @@ -701,9 +709,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_getIndexStats, $admin_getTableStats, $admin_getUserIps, - $admin_invite, - $admin_moderators_add, - $admin_moderators_remove, + $invite, $admin_promo_create, $admin_queue_clear, $admin_queue_deliverDelayed, @@ -719,13 +725,19 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_showModerationLogs, $admin_showUser, $admin_showUsers, - $admin_silenceUser, $admin_suspendUser, - $admin_unsilenceUser, $admin_unsuspendUser, $admin_updateMeta, $admin_deleteAccount, $admin_updateUserNote, + $admin_roles_create, + $admin_roles_delete, + $admin_roles_list, + $admin_roles_show, + $admin_roles_update, + $admin_roles_assign, + $admin_roles_unassign, + $admin_roles_updateDefaultRoleOverride, $announcements, $antennas_create, $antennas_delete, @@ -884,6 +896,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $messaging_messages_delete, $messaging_messages_read, $meta, + $emojis, $miauth_genToken, $mute_create, $mute_delete, @@ -988,7 +1001,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_search, $users_show, $users_stats, - $admin_driveCapOverride, $fetchRss, $retention, ], @@ -1029,9 +1041,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_getIndexStats, $admin_getTableStats, $admin_getUserIps, - $admin_invite, - $admin_moderators_add, - $admin_moderators_remove, + $invite, $admin_promo_create, $admin_queue_clear, $admin_queue_deliverDelayed, @@ -1047,13 +1057,19 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_showModerationLogs, $admin_showUser, $admin_showUsers, - $admin_silenceUser, $admin_suspendUser, - $admin_unsilenceUser, $admin_unsuspendUser, $admin_updateMeta, $admin_deleteAccount, $admin_updateUserNote, + $admin_roles_create, + $admin_roles_delete, + $admin_roles_list, + $admin_roles_show, + $admin_roles_update, + $admin_roles_assign, + $admin_roles_unassign, + $admin_roles_updateDefaultRoleOverride, $announcements, $antennas_create, $antennas_delete, @@ -1212,6 +1228,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $messaging_messages_delete, $messaging_messages_read, $meta, + $emojis, $miauth_genToken, $mute_create, $mute_delete, @@ -1314,7 +1331,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_search, $users_show, $users_stats, - $admin_driveCapOverride, $fetchRss, $retention, ], diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts index c893b60baf..a9c34e363a 100644 --- a/packages/backend/src/server/api/RateLimiterService.ts +++ b/packages/backend/src/server/api/RateLimiterService.ts @@ -26,7 +26,7 @@ export class RateLimiterService { } @bindThis - public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string) { + public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string, factor = 1) { return new Promise((ok, reject) => { if (this.disabled) ok(); @@ -34,7 +34,7 @@ export class RateLimiterService { const min = (): void => { const minIntervalLimiter = new Limiter({ id: `${actor}:${limitation.key}:min`, - duration: limitation.minInterval, + duration: limitation.minInterval * factor, max: 1, db: this.redisClient, }); @@ -62,8 +62,8 @@ export class RateLimiterService { const max = (): void => { const limiter = new Limiter({ id: `${actor}:${limitation.key}`, - duration: limitation.duration, - max: limitation.max, + duration: limitation.duration * factor, + max: limitation.max / factor, db: this.redisClient, }); diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index bba81250ab..4b676bb8b9 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -12,8 +12,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { EmailService } from '@/core/EmailService.js'; import { ILocalUser } from '@/models/entities/User.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; -import { SigninService } from './SigninService.js'; import { bindThis } from '@/decorators.js'; +import { SigninService } from './SigninService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() @@ -193,7 +193,7 @@ export class SignupApiService { emailVerifyCode: null, }); - this.signinService.signin(request, reply, account as ILocalUser); + return this.signinService.signin(request, reply, account as ILocalUser); } catch (err) { throw new FastifyReplyError(400, err); } diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index d4f8be5b85..f50a3b5dd2 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -36,9 +36,7 @@ import * as ep___admin_federation_updateInstance from './endpoints/admin/federat import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js'; -import * as ep___admin_invite from './endpoints/admin/invite.js'; -import * as ep___admin_moderators_add from './endpoints/admin/moderators/add.js'; -import * as ep___admin_moderators_remove from './endpoints/admin/moderators/remove.js'; +import * as ep___invite from './endpoints/invite.js'; import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; @@ -54,13 +52,19 @@ import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; import * as ep___admin_showUser from './endpoints/admin/show-user.js'; import * as ep___admin_showUsers from './endpoints/admin/show-users.js'; -import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js'; import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js'; -import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js'; import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js'; +import * as ep___admin_roles_create from './endpoints/admin/roles/create.js'; +import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js'; +import * as ep___admin_roles_list from './endpoints/admin/roles/list.js'; +import * as ep___admin_roles_show from './endpoints/admin/roles/show.js'; +import * as ep___admin_roles_update from './endpoints/admin/roles/update.js'; +import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; +import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; +import * as ep___admin_roles_updateDefaultRoleOverride from './endpoints/admin/roles/update-default-role-override.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; import * as ep___antennas_delete from './endpoints/antennas/delete.js'; @@ -219,6 +223,7 @@ import * as ep___messaging_messages_create from './endpoints/messaging/messages/ import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js'; import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js'; import * as ep___meta from './endpoints/meta.js'; +import * as ep___emojis from './endpoints/emojis.js'; import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; import * as ep___mute_create from './endpoints/mute/create.js'; import * as ep___mute_delete from './endpoints/mute/delete.js'; @@ -324,7 +329,6 @@ import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_stats from './endpoints/users/stats.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; -import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js'; import * as ep___retention from './endpoints/retention.js'; const eps = [ @@ -364,9 +368,7 @@ const eps = [ ['admin/get-index-stats', ep___admin_getIndexStats], ['admin/get-table-stats', ep___admin_getTableStats], ['admin/get-user-ips', ep___admin_getUserIps], - ['admin/invite', ep___admin_invite], - ['admin/moderators/add', ep___admin_moderators_add], - ['admin/moderators/remove', ep___admin_moderators_remove], + ['invite', ep___invite], ['admin/promo/create', ep___admin_promo_create], ['admin/queue/clear', ep___admin_queue_clear], ['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed], @@ -382,13 +384,19 @@ const eps = [ ['admin/show-moderation-logs', ep___admin_showModerationLogs], ['admin/show-user', ep___admin_showUser], ['admin/show-users', ep___admin_showUsers], - ['admin/silence-user', ep___admin_silenceUser], ['admin/suspend-user', ep___admin_suspendUser], - ['admin/unsilence-user', ep___admin_unsilenceUser], ['admin/unsuspend-user', ep___admin_unsuspendUser], ['admin/update-meta', ep___admin_updateMeta], ['admin/delete-account', ep___admin_deleteAccount], ['admin/update-user-note', ep___admin_updateUserNote], + ['admin/roles/create', ep___admin_roles_create], + ['admin/roles/delete', ep___admin_roles_delete], + ['admin/roles/list', ep___admin_roles_list], + ['admin/roles/show', ep___admin_roles_show], + ['admin/roles/update', ep___admin_roles_update], + ['admin/roles/assign', ep___admin_roles_assign], + ['admin/roles/unassign', ep___admin_roles_unassign], + ['admin/roles/update-default-role-override', ep___admin_roles_updateDefaultRoleOverride], ['announcements', ep___announcements], ['antennas/create', ep___antennas_create], ['antennas/delete', ep___antennas_delete], @@ -547,6 +555,7 @@ const eps = [ ['messaging/messages/delete', ep___messaging_messages_delete], ['messaging/messages/read', ep___messaging_messages_read], ['meta', ep___meta], + ['emojis', ep___emojis], ['miauth/gen-token', ep___miauth_genToken], ['mute/create', ep___mute_create], ['mute/delete', ep___mute_delete], @@ -651,7 +660,6 @@ const eps = [ ['users/search', ep___users_search], ['users/show', ep___users_show], ['users/stats', ep___users_stats], - ['admin/drive-capacity-override', ep___admin_driveCapOverride], ['fetch-rss', ep___fetchRss], ['retention', ep___retention], ]; @@ -678,14 +686,16 @@ export interface IEndpointMeta { readonly requireCredential?: boolean; /** - * 管理者のみ使えるエンドポイントか否か + * isModeratorなロールを必要とするか + */ + readonly requireModerator?: boolean; + + /** + * isAdministratorなロールを必要とするか */ readonly requireAdmin?: boolean; - /** - * 管理者またはモデレーターのみ使えるエンドポイントか否か - */ - readonly requireModerator?: boolean; + readonly requireRoleOption?: string; /** * エンドポイントのリミテーションに関するやつ diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index c76ece9e05..bac8ae16e5 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -47,7 +47,7 @@ export default class extends Endpoint { const noUsers = (await this.usersRepository.countBy({ host: IsNull(), })) === 0; - if (!noUsers && !me?.isAdmin) throw new Error('access denied'); + if (!noUsers && !me?.isRoot) throw new Error('access denied'); const { account, secret } = await this.signupService.signup({ username: ps.username, diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index b7081987ca..e9f72676f0 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -11,7 +11,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireAdmin: true, } as const; export const paramDef = { @@ -41,12 +41,8 @@ export default class extends Endpoint { throw new Error('user not found'); } - if (user.isAdmin) { - throw new Error('cannot suspend admin'); - } - - if (user.isModerator) { - throw new Error('cannot suspend moderator'); + if (user.isRoot) { + throw new Error('cannot delete a root account'); } if (this.userEntityService.isLocalUser(user)) { diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts index 195300666e..08e3c96ca9 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts @@ -38,7 +38,7 @@ export const paramDef = { @Injectable() export default class extends Endpoint { constructor( - @Inject(DI.usersRepository) + @Inject(DI.adsRepository) private adsRepository: AdsRepository, ) { super(meta, paramDef, async (ps, me) => { diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts index 22b78bf19d..c193ed3fb3 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts @@ -8,7 +8,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireAdmin: true, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts b/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts deleted file mode 100644 index 665e2a8cce..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { DI } from '@/di-symbols.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - overrideMb: { type: 'number', nullable: true }, - }, - required: ['userId', 'overrideMb'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private userEntityService: UserEntityService, - private moderationLogService: ModerationLogService, - ) { - super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - if (!this.userEntityService.isLocalUser(user)) { - throw new Error('user is not local user'); - } - - /*if (user.isAdmin) { - throw new Error('cannot suspend admin'); - } - if (user.isModerator) { - throw new Error('cannot suspend moderator'); - }*/ - - await this.usersRepository.update(user.id, { - driveCapacityOverrideMb: ps.overrideMb, - }); - - this.moderationLogService.insertModerationLog(me, 'change-drive-capacity-override', { - targetId: user.id, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts index 6180eeae2b..6376cb153c 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { DriveFilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -159,6 +160,8 @@ export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const file = ps.fileId ? await this.driveFilesRepository.findOneBy({ id: ps.fileId }) : await this.driveFilesRepository.findOne({ @@ -175,6 +178,8 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchFile); } + const isModerator = await this.roleService.isModerator(me); + return { id: file.id, userId: file.userId, @@ -202,8 +207,8 @@ export default class extends Endpoint { name: file.name, md5: file.md5, createdAt: file.createdAt.toISOString(), - requestIp: me.isAdmin ? file.requestIp : null, - requestHeaders: me.isAdmin ? file.requestHeaders : null, + requestIp: isModerator ? file.requestIp : null, + requestHeaders: isModerator ? file.requestHeaders : null, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts index 7c24e8baa8..d114fd3d55 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts @@ -8,7 +8,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireRoleOption: 'canManageCustomEmojis', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index c4e1987d73..52ccb74447 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -14,7 +14,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireRoleOption: 'canManageCustomEmojis', errors: { noSuchFile: { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index 2cdd9c36bd..4d1fdd989d 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -14,7 +14,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireRoleOption: 'canManageCustomEmojis', errors: { noSuchEmoji: { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts index 8b2031e6dd..27aa4fb1b1 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts @@ -9,7 +9,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireRoleOption: 'canManageCustomEmojis', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index dd7cd4cede..2531246569 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -10,7 +10,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireRoleOption: 'canManageCustomEmojis', errors: { noSuchEmoji: { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts index 6fe492cb75..15f468c180 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts @@ -5,7 +5,7 @@ import { QueueService } from '@/core/QueueService.js'; export const meta = { secure: true, requireCredential: true, - requireModerator: true, + requireRoleOption: 'canManageCustomEmojis', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index c03d27878c..131c9ef223 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -5,12 +5,13 @@ import { QueryService } from '@/core/QueryService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireRoleOption: 'canManageCustomEmojis', res: { type: 'array', @@ -92,7 +93,7 @@ export default class extends Endpoint { } if (ps.query) { - q.andWhere('emoji.name like :query', { query: '%' + ps.query + '%' }); + q.andWhere('emoji.name like :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); } const emojis = await q diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index 271b142126..ef2bc936c3 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -5,12 +5,13 @@ import type { Emoji } from '@/models/entities/Emoji.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; +//import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireRoleOption: 'canManageCustomEmojis', res: { type: 'array', @@ -82,7 +83,7 @@ export default class extends Endpoint { let emojis: Emoji[]; if (ps.query) { - //q.andWhere('emoji.name ILIKE :q', { q: `%${ps.query}%` }); + //q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); //const emojis = await q.take(ps.limit).getMany(); emojis = await q.getMany(); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts index 99512a26b3..a70cd8d787 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts @@ -8,7 +8,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireRoleOption: 'canManageCustomEmojis', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts index 697999cc7c..b33e5662bb 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts @@ -8,7 +8,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireRoleOption: 'canManageCustomEmojis', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts index 00a5b162bf..05834bc572 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts @@ -8,7 +8,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireRoleOption: 'canManageCustomEmojis', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index c576950ac7..19645cb515 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -9,7 +9,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireRoleOption: 'canManageCustomEmojis', errors: { noSuchEmoji: { diff --git a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts index e53d0bfcea..8ffd2b01e7 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts @@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, - requireModerator: true, + requireAdmin: true, tags: ['admin'], } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts index 41014cb167..09d61bd741 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts @@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, - requireModerator: true, + requireAdmin: true, tags: ['admin'], diff --git a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts index 947a673def..bfcc8a700b 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts @@ -7,7 +7,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireAdmin: true, + requireModerator: true, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 794ea3d5c9..fd08a5f847 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -4,6 +4,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; +import { DEFAULT_ROLE } from '@/core/RoleService.js'; export const meta = { tags: ['meta'], @@ -15,14 +16,6 @@ export const meta = { type: 'object', optional: false, nullable: false, properties: { - driveCapacityPerLocalUserMb: { - type: 'number', - optional: false, nullable: false, - }, - driveCapacityPerRemoteUserMb: { - type: 'number', - optional: false, nullable: false, - }, cacheRemoteFiles: { type: 'boolean', optional: false, nullable: false, @@ -377,10 +370,6 @@ export default class extends Endpoint { repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, disableRegistration: instance.disableRegistration, - disableLocalTimeline: instance.disableLocalTimeline, - disableGlobalTimeline: instance.disableGlobalTimeline, - driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, - driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, @@ -451,6 +440,7 @@ export default class extends Endpoint { deeplIsPro: instance.deeplIsPro, enableIpLogging: instance.enableIpLogging, enableActiveEmailValidation: instance.enableActiveEmailValidation, + baseRole: { ...DEFAULT_ROLE, ...instance.defaultRoleOverride }, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts deleted file mode 100644 index 2fc5a35e8e..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireAdmin: true, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private globalEventService: GlobalEventService, - ) { - super(meta, paramDef, async (ps) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - if (user.isAdmin) { - throw new Error('cannot mark as moderator if admin user'); - } - - await this.usersRepository.update(user.id, { - isModerator: true, - }); - - this.globalEventService.publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: true }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index f7d27be9cb..d263f99f6e 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -50,8 +50,8 @@ export default class extends Endpoint { throw new Error('user not found'); } - if (user.isAdmin) { - throw new Error('cannot reset password of admin'); + if (user.isRoot) { + throw new Error('cannot reset password of root'); } const passwd = rndstr('a-zA-Z0-9', 8); diff --git a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts new file mode 100644 index 0000000000..7bfb2f6625 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts @@ -0,0 +1,96 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { RoleService } from '@/core/RoleService.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireModerator: true, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: '6503c040-6af4-4ed9-bf07-f2dd16678eab', + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '558ea170-f653-4700-94d0-5a818371d0df', + }, + + accessDenied: { + message: 'Only administrators can edit members of the role.', + code: 'ACCESS_DENIED', + id: '25b5bc31-dc79-4ebd-9bd2-c84978fd052c', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id' }, + }, + required: [ + 'roleId', + 'userId', + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + + private globalEventService: GlobalEventService, + private roleService: RoleService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + + if (!role.canEditMembersByModerator && !(await this.roleService.isAdministrator(me))) { + throw new ApiError(meta.errors.accessDenied); + } + + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + if (user == null) { + throw new ApiError(meta.errors.noSuchUser); + } + + const date = new Date(); + const created = await this.roleAssignmentsRepository.insert({ + id: this.idService.genId(), + createdAt: date, + roleId: role.id, + userId: user.id, + }).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0])); + + this.rolesRepository.update(ps.roleId, { + lastUsedAt: new Date(), + }); + + this.globalEventService.publishInternalEvent('userRoleAssigned', created); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts new file mode 100644 index 0000000000..a9216a6386 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts @@ -0,0 +1,81 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RolesRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { IdService } from '@/core/IdService.js'; +import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireAdmin: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string' }, + description: { type: 'string' }, + color: { type: 'string', nullable: true }, + target: { type: 'string' }, + condFormula: { type: 'object' }, + isPublic: { type: 'boolean' }, + isModerator: { type: 'boolean' }, + isAdministrator: { type: 'boolean' }, + canEditMembersByModerator: { type: 'boolean' }, + options: { + type: 'object', + }, + }, + required: [ + 'name', + 'description', + 'color', + 'target', + 'condFormula', + 'isPublic', + 'isModerator', + 'isAdministrator', + 'canEditMembersByModerator', + 'options', + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private globalEventService: GlobalEventService, + private idService: IdService, + private roleEntityService: RoleEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const date = new Date(); + const created = await this.rolesRepository.insert({ + id: this.idService.genId(), + createdAt: date, + updatedAt: date, + lastUsedAt: date, + name: ps.name, + description: ps.description, + color: ps.color, + target: ps.target, + condFormula: ps.condFormula, + isPublic: ps.isPublic, + isAdministrator: ps.isAdministrator, + isModerator: ps.isModerator, + canEditMembersByModerator: ps.canEditMembersByModerator, + options: ps.options, + }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); + + this.globalEventService.publishInternalEvent('roleCreated', created); + + return await this.roleEntityService.pack(created, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/roles/delete.ts b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts new file mode 100644 index 0000000000..b56ebdb3ee --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts @@ -0,0 +1,53 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RolesRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireAdmin: true, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: 'de0d6ecd-8e0a-4253-88ff-74bc89ae3d45', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + }, + required: [ + 'roleId', + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps) => { + const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + await this.rolesRepository.delete({ + id: ps.roleId, + }); + this.globalEventService.publishInternalEvent('roleDeleted', role); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/roles/list.ts b/packages/backend/src/server/api/endpoints/admin/roles/list.ts new file mode 100644 index 0000000000..458a8d535b --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/list.ts @@ -0,0 +1,39 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RolesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + }, + required: [ + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private roleEntityService: RoleEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const roles = await this.rolesRepository.find({ + order: { lastUsedAt: 'DESC' }, + }); + return await this.roleEntityService.packMany(roles, me, { detail: false }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/roles/show.ts b/packages/backend/src/server/api/endpoints/admin/roles/show.ts new file mode 100644 index 0000000000..c83f96191d --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/show.ts @@ -0,0 +1,50 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RolesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireModerator: true, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: '07dc7d34-c0d8-49b7-96c6-db3ce64ee0b3', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + }, + required: [ + 'roleId', + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private roleEntityService: RoleEntityService, + ) { + super(meta, paramDef, async (ps) => { + const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + return await this.roleEntityService.pack(role); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts new file mode 100644 index 0000000000..141cc5ee89 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts @@ -0,0 +1,101 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { RoleService } from '@/core/RoleService.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireModerator: true, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: '6e519036-a70d-4c76-b679-bc8fb18194e2', + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '2b730f78-1179-461b-88ad-d24c9af1a5ce', + }, + + notAssigned: { + message: 'Not assigned.', + code: 'NOT_ASSIGNED', + id: 'b9060ac7-5c94-4da4-9f55-2047c953df44', + }, + + accessDenied: { + message: 'Only administrators can edit members of the role.', + code: 'ACCESS_DENIED', + id: '24636eee-e8c1-493e-94b2-e16ad401e262', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id' }, + }, + required: [ + 'roleId', + 'userId', + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + + private globalEventService: GlobalEventService, + private roleService: RoleService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + + if (!role.canEditMembersByModerator && !(await this.roleService.isAdministrator(me))) { + throw new ApiError(meta.errors.accessDenied); + } + + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + if (user == null) { + throw new ApiError(meta.errors.noSuchUser); + } + + const roleAssignment = await this.roleAssignmentsRepository.findOneBy({ userId: user.id, roleId: role.id }); + if (roleAssignment == null) { + throw new ApiError(meta.errors.notAssigned); + } + + await this.roleAssignmentsRepository.delete(roleAssignment.id); + + this.rolesRepository.update(ps.roleId, { + lastUsedAt: new Date(), + }); + + this.globalEventService.publishInternalEvent('userRoleUnassigned', roleAssignment); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts b/packages/backend/src/server/api/endpoints/admin/roles/update-default-role-override.ts similarity index 53% rename from packages/backend/src/server/api/endpoints/admin/moderators/remove.ts rename to packages/backend/src/server/api/endpoints/admin/roles/update-default-role-override.ts index f0d7a3f12d..35da04efd2 100644 --- a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update-default-role-override.ts @@ -1,11 +1,13 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { RolesRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { MetaService } from '@/core/MetaService.js'; export const meta = { - tags: ['admin'], + tags: ['admin', 'role'], requireCredential: true, requireAdmin: true, @@ -14,32 +16,27 @@ export const meta = { export const paramDef = { type: 'object', properties: { - userId: { type: 'string', format: 'misskey:id' }, + options: { + type: 'object', + }, }, - required: ['userId'], + required: [ + 'options', + ], } as const; // eslint-disable-next-line import/no-default-export @Injectable() export default class extends Endpoint { constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - + private metaService: MetaService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - await this.usersRepository.update(user.id, { - isModerator: false, + await this.metaService.update({ + defaultRoleOverride: ps.options, }); - - this.globalEventService.publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: false }); + this.globalEventService.publishInternalEvent('defaultRoleOverrideUpdated', ps.options); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts new file mode 100644 index 0000000000..4ca5124eda --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -0,0 +1,88 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RolesRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireAdmin: true, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: 'cd23ef55-09ad-428a-ac61-95a45e124b32', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + name: { type: 'string' }, + description: { type: 'string' }, + color: { type: 'string', nullable: true }, + target: { type: 'string' }, + condFormula: { type: 'object' }, + isPublic: { type: 'boolean' }, + isModerator: { type: 'boolean' }, + isAdministrator: { type: 'boolean' }, + canEditMembersByModerator: { type: 'boolean' }, + options: { + type: 'object', + }, + }, + required: [ + 'roleId', + 'name', + 'description', + 'color', + 'target', + 'condFormula', + 'isPublic', + 'isModerator', + 'isAdministrator', + 'canEditMembersByModerator', + 'options', + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps) => { + const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + + const date = new Date(); + await this.rolesRepository.update(ps.roleId, { + updatedAt: date, + name: ps.name, + description: ps.description, + color: ps.color, + target: ps.target, + condFormula: ps.condFormula, + isPublic: ps.isPublic, + isModerator: ps.isModerator, + isAdministrator: ps.isAdministrator, + canEditMembersByModerator: ps.canEditMembersByModerator, + options: ps.options, + }); + const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId }); + this.globalEventService.publishInternalEvent('roleUpdated', updated); + }); + } +} 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 e4031cf960..3f4ec299af 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -2,6 +2,8 @@ import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, SigninsRepository, UserProfilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; export const meta = { tags: ['admin'], @@ -35,6 +37,9 @@ export default class extends Endpoint { @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, + + private roleService: RoleService, + private roleEntityService: RoleEntityService, ) { super(meta, paramDef, async (ps, me) => { const [user, profile] = await Promise.all([ @@ -46,15 +51,16 @@ export default class extends Endpoint { throw new Error('user not found'); } + const isModerator = await this.roleService.isModerator(user); + const isSilenced = !(await this.roleService.getUserRoleOptions(user.id)).canPublicNote; + const _me = await this.usersRepository.findOneByOrFail({ id: me.id }); - if ((_me.isModerator && !_me.isAdmin) && user.isAdmin) { + if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) { throw new Error('cannot show info of admin'); } - if (!_me.isAdmin) { + if (!await this.roleService.isAdministrator(_me)) { return { - isModerator: user.isModerator, - isSilenced: user.isSilenced, isSuspended: user.isSuspended, }; } @@ -66,6 +72,8 @@ export default class extends Endpoint { const signins = await this.signinsRepository.findBy({ userId: user.id }); + const roles = await this.roleService.getUserRoles(user.id); + return { email: profile.email, emailVerified: profile.emailVerified, @@ -80,12 +88,13 @@ export default class extends Endpoint { mutedWords: profile.mutedWords, mutedInstances: profile.mutedInstances, mutingNotificationTypes: profile.mutingNotificationTypes, - isModerator: user.isModerator, - isSilenced: user.isSilenced, + isModerator: isModerator, + isSilenced: isSilenced, isSuspended: user.isSuspended, lastActiveDate: user.lastActiveDate, moderationNote: profile.moderationNote, signins, + roles: await this.roleEntityService.packMany(roles, me, { detail: false }), }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 33e1be8041..426973f282 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -3,6 +3,8 @@ import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; +import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin'], @@ -27,7 +29,7 @@ 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', '+lastActiveDate', '-lastActiveDate'] }, - state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' }, + state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'suspended'], default: 'all' }, origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, username: { type: 'string', nullable: true, default: null }, hostname: { @@ -48,18 +50,33 @@ export default class extends Endpoint { private usersRepository: UsersRepository, private userEntityService: UserEntityService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const query = this.usersRepository.createQueryBuilder('user'); switch (ps.state) { case 'available': query.where('user.isSuspended = FALSE'); break; - case 'admin': query.where('user.isAdmin = TRUE'); break; - case 'moderator': query.where('user.isModerator = TRUE'); break; - case 'adminOrModerator': query.where('user.isAdmin = TRUE OR user.isModerator = TRUE'); break; case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break; - case 'silenced': query.where('user.isSilenced = TRUE'); break; case 'suspended': query.where('user.isSuspended = TRUE'); break; + case 'admin': { + const adminIds = await this.roleService.getAdministratorIds(); + if (adminIds.length === 0) return []; + query.where('user.id IN (:...adminIds)', { adminIds: adminIds }); + break; + } + case 'moderator': { + const moderatorIds = await this.roleService.getModeratorIds(false); + if (moderatorIds.length === 0) return []; + query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds }); + break; + } + case 'adminOrModerator': { + const adminOrModeratorIds = await this.roleService.getModeratorIds(); + if (adminOrModeratorIds.length === 0) return []; + query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds }); + break; + } } switch (ps.origin) { @@ -68,7 +85,7 @@ export default class extends Endpoint { } if (ps.username) { - query.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }); + query.andWhere('user.usernameLower like :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }); } if (ps.hostname) { diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts deleted file mode 100644 index b9dbd211e0..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import type { UsersRepository } from '@/models/index.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private moderationLogService: ModerationLogService, - private globalEventService: GlobalEventService, - ) { - super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - if (user.isAdmin) { - throw new Error('cannot silence admin'); - } - - await this.usersRepository.update(user.id, { - isSilenced: true, - }); - - this.globalEventService.publishInternalEvent('userChangeSilencedState', { id: user.id, isSilenced: true }); - - this.moderationLogService.insertModerationLog(me, 'silence', { - targetId: user.id, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 9fc1391570..3ad6c7c484 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -9,6 +9,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin'], @@ -41,6 +42,7 @@ export default class extends Endpoint { private userEntityService: UserEntityService, private userFollowingService: UserFollowingService, private userSuspendService: UserSuspendService, + private roleService: RoleService, private moderationLogService: ModerationLogService, private globalEventService: GlobalEventService, ) { @@ -51,12 +53,8 @@ export default class extends Endpoint { throw new Error('user not found'); } - if (user.isAdmin) { - throw new Error('cannot suspend admin'); - } - - if (user.isModerator) { - throw new Error('cannot suspend moderator'); + if (await this.roleService.isModerator(user)) { + throw new Error('cannot suspend moderator account'); } await this.usersRepository.update(user.id, { diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts deleted file mode 100644 index 3a9d410de0..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private moderationLogService: ModerationLogService, - private globalEventService: GlobalEventService, - ) { - super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - await this.usersRepository.update(user.id, { - isSilenced: false, - }); - - this.globalEventService.publishInternalEvent('userChangeSilencedState', { id: user.id, isSilenced: false }); - - this.moderationLogService.insertModerationLog(me, 'unsilence', { - targetId: user.id, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 795b8460f3..aacd634ed8 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -19,8 +19,6 @@ export const paramDef = { type: 'object', properties: { disableRegistration: { type: 'boolean', nullable: true }, - disableLocalTimeline: { type: 'boolean', nullable: true }, - disableGlobalTimeline: { type: 'boolean', nullable: true }, useStarForReactionFallback: { type: 'boolean', nullable: true }, pinnedUsers: { type: 'array', nullable: true, items: { type: 'string', @@ -42,8 +40,6 @@ export const paramDef = { description: { type: 'string', nullable: true }, defaultLightTheme: { type: 'string', nullable: true }, defaultDarkTheme: { type: 'string', nullable: true }, - localDriveCapacityMb: { type: 'integer' }, - remoteDriveCapacityMb: { type: 'integer' }, cacheRemoteFiles: { type: 'boolean' }, emailRequiredForSignup: { type: 'boolean' }, enableHcaptcha: { type: 'boolean' }, @@ -130,14 +126,6 @@ export default class extends Endpoint { set.disableRegistration = ps.disableRegistration; } - if (typeof ps.disableLocalTimeline === 'boolean') { - set.disableLocalTimeline = ps.disableLocalTimeline; - } - - if (typeof ps.disableGlobalTimeline === 'boolean') { - set.disableGlobalTimeline = ps.disableGlobalTimeline; - } - if (typeof ps.useStarForReactionFallback === 'boolean') { set.useStarForReactionFallback = ps.useStarForReactionFallback; } @@ -151,7 +139,7 @@ export default class extends Endpoint { } if (Array.isArray(ps.blockedHosts)) { - set.blockedHosts = ps.blockedHosts.filter(Boolean); + set.blockedHosts = ps.blockedHosts.filter(Boolean).map(x => x.toLowerCase()); } if (ps.themeColor !== undefined) { @@ -194,14 +182,6 @@ export default class extends Endpoint { set.defaultDarkTheme = ps.defaultDarkTheme; } - if (ps.localDriveCapacityMb !== undefined) { - set.localDriveCapacityMb = ps.localDriveCapacityMb; - } - - if (ps.remoteDriveCapacityMb !== undefined) { - set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb; - } - if (ps.cacheRemoteFiles !== undefined) { set.cacheRemoteFiles = ps.cacheRemoteFiles; } diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 2378660ec8..08625250c8 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -5,6 +5,7 @@ import type { UserListsRepository, UserGroupJoiningsRepository, AntennasReposito import { GlobalEventService } from '@/core/GlobalEventService.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -83,6 +84,7 @@ export default class extends Endpoint { private userGroupJoiningsRepository: UserGroupJoiningsRepository, private antennaEntityService: AntennaEntityService, + private roleService: RoleService, private idService: IdService, private globalEventService: GlobalEventService, ) { @@ -90,7 +92,7 @@ export default class extends Endpoint { const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id, }); - if (currentAntennasCount > 5) { + if (currentAntennasCount > (await this.roleService.getUserRoleOptions(me.id)).antennaLimit) { throw new ApiError(meta.errors.tooManyAntennas); } diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 1068a2eec7..9470dd3cbb 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -117,7 +117,7 @@ export default class extends Endpoint { private async fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise | null> { // ブロックしてたら中断 const fetchedMeta = await this.metaService.fetch(); - if (fetchedMeta.blockedHosts.includes(this.utilityService.extractDbHost(uri))) return null; + if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null; let local = await this.mergePack(me, ...await Promise.all([ this.apDbResolverService.getUserFromApId(uri), diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index c468010bce..d9ba99f209 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -5,15 +5,15 @@ import type { UsersRepository, BlockingsRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['account'], limit: { duration: ms('1hour'), - max: 100, + max: 20, }, requireCredential: true, diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts index 10f8b24629..dff8a9d10d 100644 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ b/packages/backend/src/server/api/endpoints/channels/create.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { ChannelsRepository, DriveFilesRepository } from '@/models/index.js'; import type { Channel } from '@/models/entities/Channel.js'; @@ -14,6 +15,11 @@ export const meta = { kind: 'write:channels', + limit: { + duration: ms('1hour'), + max: 10, + }, + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index a242124e6a..3cf096c242 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -1,10 +1,12 @@ import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['account', 'notes', 'clips'], @@ -13,6 +15,11 @@ export const meta = { kind: 'write:account', + limit: { + duration: ms('1hour'), + max: 20, + }, + errors: { noSuchClip: { message: 'No such clip.', @@ -31,6 +38,12 @@ export const meta = { code: 'ALREADY_CLIPPED', id: '734806c4-542c-463a-9311-15c512803965', }, + + tooManyClipNotes: { + message: 'You cannot add notes to the clip any more.', + code: 'TOO_MANY_CLIP_NOTES', + id: 'f0dba960-ff73-4615-8df4-d6ac5d9dc118', + }, }, } as const; @@ -54,6 +67,7 @@ export default class extends Endpoint { private clipNotesRepository: ClipNotesRepository, private idService: IdService, + private roleService: RoleService, private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { @@ -80,6 +94,13 @@ export default class extends Endpoint { throw new ApiError(meta.errors.alreadyClipped); } + const currentCount = await this.clipNotesRepository.countBy({ + clipId: clip.id, + }); + if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).noteEachClipsLimit) { + throw new ApiError(meta.errors.tooManyClipNotes); + } + await this.clipNotesRepository.insert({ id: this.idService.genId(), noteId: note.id, diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts index d300203a21..abc0288c89 100644 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ b/packages/backend/src/server/api/endpoints/clips/create.ts @@ -4,6 +4,8 @@ import { IdService } from '@/core/IdService.js'; import type { ClipsRepository } from '@/models/index.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '@/server/api/error.js'; export const meta = { tags: ['clips'], @@ -17,6 +19,14 @@ export const meta = { optional: false, nullable: false, ref: 'Clip', }, + + errors: { + tooManyClips: { + message: 'You cannot create clip any more.', + code: 'TOO_MANY_CLIPS', + id: '920f7c2d-6208-4b76-8082-e632020f5883', + }, + }, } as const; export const paramDef = { @@ -37,9 +47,17 @@ export default class extends Endpoint { private clipsRepository: ClipsRepository, private clipEntityService: ClipEntityService, + private roleService: RoleService, private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { + const currentCount = await this.clipsRepository.countBy({ + userId: me.id, + }); + if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).clipLimit) { + throw new ApiError(meta.errors.tooManyClips); + } + const clip = await this.clipsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index 6f40225f15..2a06792dcf 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['drive', 'account'], @@ -38,6 +39,7 @@ export default class extends Endpoint { constructor( private metaService: MetaService, private driveFileEntityService: DriveFileEntityService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const instance = await this.metaService.fetch(true); @@ -45,8 +47,10 @@ export default class extends Endpoint { // Calculate drive usage const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id); + const myRole = await this.roleService.getUserRoleOptions(me.id); + return { - capacity: 1024 * 1024 * (me.driveCapacityOverrideMb ?? instance.localDriveCapacityMb), + capacity: 1024 * 1024 * myRole.driveCapacityMb, usage: usage, }; }); 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 6abbab0270..b3bdef41d3 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -90,7 +90,7 @@ export default class extends Endpoint { } } - const meta = await this.metaService.fetch(); + const instance = await this.metaService.fetch(); try { // Create file @@ -102,8 +102,8 @@ export default class extends Endpoint { folderId: ps.folderId, force: ps.force, sensitive: ps.isSensitive, - requestIp: meta.enableIpLogging ? ip : null, - requestHeaders: meta.enableIpLogging ? headers : null, + requestIp: instance.enableIpLogging ? ip : null, + requestHeaders: instance.enableIpLogging ? headers : null, }); return await this.driveFileEntityService.pack(driveFile, { self: true }); } catch (err) { @@ -116,7 +116,7 @@ export default class extends Endpoint { } throw new ApiError(); } finally { - cleanup!(); + cleanup!(); } }); } 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 be7b050907..2ced97ee02 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -4,6 +4,7 @@ import type { DriveFilesRepository } from '@/models/index.js'; import { DriveService } from '@/core/DriveService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -46,6 +47,7 @@ export default class extends Endpoint { private driveFilesRepository: DriveFilesRepository, private driveService: DriveService, + private roleService: RoleService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { @@ -55,7 +57,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchFile); } - if ((!me.isAdmin && !me.isModerator) && (file.userId !== me.id)) { + if (!await this.roleService.isModerator(me) && (file.userId !== me.id)) { throw new ApiError(meta.errors.accessDenied); } 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 474d599cb6..e0a07a3640 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -4,6 +4,7 @@ import type { DriveFilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -62,6 +63,7 @@ export default class extends Endpoint { private driveFilesRepository: DriveFilesRepository, private driveFileEntityService: DriveFileEntityService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { let file: DriveFile | null = null; @@ -84,7 +86,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchFile); } - if ((!me.isAdmin && !me.isModerator) && (file.userId !== me.id)) { + if (!await this.roleService.isModerator(me) && (file.userId !== me.id)) { throw new ApiError(meta.errors.accessDenied); } 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 9e2c767277..0fe57de6a8 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -5,6 +5,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -72,6 +73,7 @@ export default class extends Endpoint { private driveFoldersRepository: DriveFoldersRepository, private driveFileEntityService: DriveFileEntityService, + private roleService: RoleService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { @@ -81,7 +83,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchFile); } - if ((!me.isAdmin && !me.isModerator) && (file.userId !== me.id)) { + if (!await this.roleService.isModerator(me) && (file.userId !== me.id)) { throw new ApiError(meta.errors.accessDenied); } diff --git a/packages/backend/src/server/api/endpoints/drive/folders/create.ts b/packages/backend/src/server/api/endpoints/drive/folders/create.ts index e7c11a8c13..39c9c6bc58 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/create.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { DriveFoldersRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; @@ -14,6 +15,11 @@ export const meta = { kind: 'write:drive', + limit: { + duration: ms('1hour'), + max: 10, + }, + errors: { noSuchFolder: { message: 'No such folder.', diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts new file mode 100644 index 0000000000..97dcfde596 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -0,0 +1,90 @@ +import { IsNull, MoreThan } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import type { EmojisRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; +import type { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['meta'], + + requireCredential: false, + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + emojis: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + name: { + type: 'string', + optional: false, nullable: false, + }, + aliases: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, + category: { + type: 'string', + optional: false, nullable: true, + }, + }, + }, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + }, + required: [], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + + private emojiEntityService: EmojiEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const emojis = await this.emojisRepository.find({ + where: { + host: IsNull(), + }, + order: { + category: 'ASC', + name: 'ASC', + }, + cache: { + id: 'meta_emojis', + milliseconds: 3600000, // 1 hour + }, + }); + + return { + emojis: await this.emojiEntityService.packMany(emojis, { + omitId: true, + omitHost: true, + }), + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 5e2f204661..4c19988eb8 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -4,6 +4,7 @@ import type { InstancesRepository } from '@/models/index.js'; import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['federation'], @@ -120,7 +121,7 @@ export default class extends Endpoint { } if (ps.host) { - query.andWhere('instance.host like :host', { host: '%' + ps.host.toLowerCase() + '%' }); + query.andWhere('instance.host like :host', { host: '%' + sqlLikeEscape(ps.host.toLowerCase()) + '%' }); } const instances = await query.take(ps.limit).skip(ps.offset).getMany(); diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index 558ec887db..ae6a87513d 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -37,10 +37,9 @@ export default class extends Endpoint { ps.url, { method: 'GET', - headers: Object.assign({ - 'User-Agent': config.userAgent, + headers: { Accept: 'application/rss+xml, */*', - }), + }, // timeout: 5000, } ); diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index f879429372..411c39110a 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -6,15 +6,15 @@ import { IdentifiableError } from '@/misc/identifiable-error.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['following', 'users'], limit: { duration: ms('1hour'), - max: 100, + max: 50, }, requireCredential: true, diff --git a/packages/backend/src/server/api/endpoints/following/requests/list.ts b/packages/backend/src/server/api/endpoints/following/requests/list.ts index 5b11633e6f..d68248fab9 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/list.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts @@ -1,5 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; import type { FollowRequestsRepository } from '@/models/index.js'; import { FollowRequestEntityService } from '@/core/entities/FollowRequestEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -40,7 +41,11 @@ export const meta = { export const paramDef = { type: 'object', - properties: {}, + properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, required: [], } as const; @@ -52,13 +57,17 @@ export default class extends Endpoint { private followRequestsRepository: FollowRequestsRepository, private followRequestEntityService: FollowRequestEntityService, + private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const reqs = await this.followRequestsRepository.findBy({ - followeeId: me.id, - }); + const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId) + .andWhere('request.followeeId = :meId', { meId: me.id }); - return await Promise.all(reqs.map(req => this.followRequestEntityService.pack(req))); + const requests = await query + .take(ps.limit) + .getMany(); + + return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req))); }); } } diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts index 2842308510..3d9d471502 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -18,7 +18,7 @@ export const meta = { limit: { duration: ms('1hour'), - max: 300, + max: 20, }, res: { diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index 7f787ea38f..4f5f979767 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { HashtagsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['hashtags'], @@ -37,7 +38,7 @@ export default class extends Endpoint { ) { super(meta, paramDef, async (ps, me) => { const hashtags = await this.hashtagsRepository.createQueryBuilder('tag') - .where('tag.name like :q', { q: ps.query.toLowerCase() + '%' }) + .where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' }) .orderBy('tag.count', 'DESC') .groupBy('tag.id') .take(ps.limit) diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index c2231f5f75..fe09eca674 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -17,6 +17,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import { HashtagService } from '@/core/HashtagService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -62,6 +63,12 @@ export const meta = { code: 'INVALID_REGEXP', id: '0d786918-10df-41cd-8f33-8dec7d9a89a5', }, + + tooManyMutedWords: { + message: 'Too many muted words.', + code: 'TOO_MANY_MUTED_WORDS', + id: '010665b1-a211-42d2-bc64-8f6609d79785', + }, }, res: { @@ -144,6 +151,7 @@ export default class extends Endpoint { private userFollowingService: UserFollowingService, private accountUpdateService: AccountUpdateService, private hashtagService: HashtagService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, _user, token) => { const user = await this.usersRepository.findOneByOrFail({ id: _user.id }); @@ -163,6 +171,12 @@ export default class extends Endpoint { if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; if (ps.mutedWords !== undefined) { + // TODO: ちゃんと数える + const length = JSON.stringify(ps.mutedWords).length; + if (length > (await this.roleService.getUserRoleOptions(user.id)).wordMuteLimit) { + throw new ApiError(meta.errors.tooManyMutedWords); + } + // validate regular expression syntax ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => { const regexp = x.match(/^\/(.+)\/(.*)$/); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index 584c2ba6a4..3d89b77a7b 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -5,6 +5,8 @@ import type { WebhooksRepository } from '@/models/index.js'; import { webhookEventTypes } from '@/models/entities/Webhook.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '@/server/api/error.js'; export const meta = { tags: ['webhooks'], @@ -12,6 +14,14 @@ export const meta = { requireCredential: true, kind: 'write:account', + + errors: { + tooManyWebhooks: { + message: 'You cannot create webhook any more.', + code: 'TOO_MANY_WEBHOOKS', + id: '87a9bb19-111e-4e37-81d3-a3e7426453b0', + }, + }, } as const; export const paramDef = { @@ -38,8 +48,16 @@ export default class extends Endpoint { private idService: IdService, private globalEventService: GlobalEventService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { + const currentWebhooksCount = await this.webhooksRepository.countBy({ + userId: me.id, + }); + if (currentWebhooksCount > (await this.roleService.getUserRoleOptions(me.id)).webhookLimit) { + throw new ApiError(meta.errors.tooManyWebhooks); + } + const webhook = await this.webhooksRepository.insert({ id: this.idService.genId(), createdAt: new Date(), diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts index 50098f96e7..8ec308eda7 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -67,7 +67,11 @@ export default class extends Endpoint { active: ps.active, }); - this.globalEventService.publishInternalEvent('webhookUpdated', webhook); + const updated = await this.webhooksRepository.findOneByOrFail({ + id: ps.webhookId, + }); + + this.globalEventService.publishInternalEvent('webhookUpdated', updated); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/invite.ts b/packages/backend/src/server/api/endpoints/invite.ts similarity index 93% rename from packages/backend/src/server/api/endpoints/admin/invite.ts rename to packages/backend/src/server/api/endpoints/invite.ts index bc42bf792a..9b03cf4bb6 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite.ts +++ b/packages/backend/src/server/api/endpoints/invite.ts @@ -6,10 +6,10 @@ import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; export const meta = { - tags: ['admin'], + tags: ['meta'], requireCredential: true, - requireModerator: true, + requireRoleOption: 'canInvite', res: { type: 'object', @@ -41,7 +41,7 @@ export default class extends Endpoint { private idService: IdService, ) { - super(meta, paramDef, async () => { + super(meta, paramDef, async (ps, me) => { const code = rndstr({ length: 8, chars: '2-9A-HJ-NP-Z', // [0-9A-Z] w/o [01IO] (32 patterns) diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts index 00e65b4875..e9ffc7a9eb 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { BlockingsRepository, UserGroupJoiningsRepository, DriveFilesRepository, UserGroupsRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; @@ -15,6 +16,11 @@ export const meta = { kind: 'write:messaging', + limit: { + duration: ms('1hour'), + max: 120, + }, + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 05da011979..f46a32dfe7 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -4,10 +4,10 @@ import type { AdsRepository, EmojisRepository, UsersRepository } from '@/models/ import { MAX_NOTE_TEXT_LENGTH, DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; +import { DEFAULT_ROLE } from '@/core/RoleService.js'; export const meta = { tags: ['meta'], @@ -78,22 +78,6 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - disableLocalTimeline: { - type: 'boolean', - optional: false, nullable: false, - }, - disableGlobalTimeline: { - type: 'boolean', - optional: false, nullable: false, - }, - driveCapacityPerLocalUserMb: { - type: 'number', - optional: false, nullable: false, - }, - driveCapacityPerRemoteUserMb: { - type: 'number', - optional: false, nullable: false, - }, cacheRemoteFiles: { type: 'boolean', optional: false, nullable: false, @@ -152,43 +136,6 @@ export const meta = { type: 'number', optional: false, nullable: false, }, - emojis: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - aliases: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - }, - }, - category: { - type: 'string', - optional: false, nullable: true, - }, - host: { - type: 'string', - optional: false, nullable: true, - description: 'The local host is represented with `null`.', - }, - url: { - type: 'string', - optional: false, nullable: false, - format: 'url', - }, - }, - }, - }, ads: { type: 'array', optional: false, nullable: false, @@ -326,30 +273,12 @@ export default class extends Endpoint { @Inject(DI.adsRepository) private adsRepository: AdsRepository, - @Inject(DI.emojisRepository) - private emojisRepository: EmojisRepository, - private userEntityService: UserEntityService, - private emojiEntityService: EmojiEntityService, private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const instance = await this.metaService.fetch(true); - const emojis = await this.emojisRepository.find({ - where: { - host: IsNull(), - }, - order: { - category: 'ASC', - name: 'ASC', - }, - cache: { - id: 'meta_emojis', - milliseconds: 3600000, // 1 hour - }, - }); - const ads = await this.adsRepository.find({ where: { expiresAt: MoreThan(new Date()), @@ -370,10 +299,6 @@ export default class extends Endpoint { repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, disableRegistration: instance.disableRegistration, - disableLocalTimeline: instance.disableLocalTimeline, - disableGlobalTimeline: instance.disableGlobalTimeline, - driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, - driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, @@ -390,7 +315,6 @@ export default class extends Endpoint { backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため - emojis: await this.emojiEntityService.packMany(emojis), defaultLightTheme: instance.defaultLightTheme, defaultDarkTheme: instance.defaultDarkTheme, ads: ads.map(ad => ({ @@ -410,6 +334,8 @@ export default class extends Endpoint { translatorAvailable: instance.deeplAuthKey != null, + baseRole: { ...DEFAULT_ROLE, ...instance.defaultRoleOverride }, + ...(ps.detail ? { pinnedPages: instance.pinnedPages, pinnedClipId: instance.pinnedClipId, @@ -426,8 +352,6 @@ export default class extends Endpoint { response.proxyAccountName = proxyAccount ? proxyAccount.username : null; response.features = { registration: !instance.disableRegistration, - localTimeLine: !instance.disableLocalTimeline, - globalTimeLine: !instance.disableGlobalTimeline, emailRequiredForSignup: instance.emailRequiredForSignup, elasticsearch: this.config.elasticsearch ? true : false, hcaptcha: instance.enableHcaptcha, diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index 5ead470314..9099eea52e 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -1,12 +1,13 @@ import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; import type { MutingsRepository } from '@/models/index.js'; import type { Muting } from '@/models/entities/Muting.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['account'], @@ -15,6 +16,11 @@ export const meta = { kind: 'write:mutes', + limit: { + duration: ms('1hour'), + max: 20, + }, + errors: { noSuchUser: { message: 'No such user.', diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index 3c6e7bf768..16c4c01387 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -4,8 +4,9 @@ import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteDeleteService } from '@/core/NoteDeleteService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], @@ -51,6 +52,7 @@ export default class extends Endpoint { private usersRepository: UsersRepository, private getterService: GetterService, + private roleService: RoleService, private noteDeleteService: NoteDeleteService, ) { super(meta, paramDef, async (ps, me) => { @@ -59,7 +61,7 @@ export default class extends Endpoint { throw err; }); - if ((!me.isAdmin && !me.isModerator) && (note.userId !== me.id)) { + if (!await this.roleService.isModerator(me) && (note.userId !== me.id)) { throw new ApiError(meta.errors.accessDenied); } 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 e742c1bb35..acf22a5ad4 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; import type { NoteFavoritesRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -13,6 +14,11 @@ export const meta = { kind: 'write:favorites', + limit: { + duration: ms('1hour'), + max: 20, + }, + errors: { noSuchNote: { message: 'No such note.', 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 b6eaccb5ac..081563493d 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -6,6 +6,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -57,14 +58,13 @@ export default class extends Endpoint { private noteEntityService: NoteEntityService, private queryService: QueryService, private metaService: MetaService, + private roleService: RoleService, private activeUsersChart: ActiveUsersChart, ) { super(meta, paramDef, async (ps, me) => { - const m = await this.metaService.fetch(); - if (m.disableGlobalTimeline) { - if (me == null || (!me.isAdmin && !me.isModerator)) { - throw new ApiError(meta.errors.gtlDisabled); - } + const role = await this.roleService.getUserRoleOptions(me ? me.id : null); + if (!role.gtlAvailable) { + throw new ApiError(meta.errors.gtlDisabled); } //#region Construct query 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 58bbf223a1..b2c504448e 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -7,6 +7,7 @@ import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -66,11 +67,12 @@ export default class extends Endpoint { private noteEntityService: NoteEntityService, private queryService: QueryService, private metaService: MetaService, + private roleService: RoleService, private activeUsersChart: ActiveUsersChart, ) { super(meta, paramDef, async (ps, me) => { - const m = await this.metaService.fetch(); - if (m.disableLocalTimeline && (!me.isAdmin && !me.isModerator)) { + const role = await this.roleService.getUserRoleOptions(me.id); + if (!role.ltlAvailable) { throw new ApiError(meta.errors.stlDisabled); } 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 d3594814b0..6361edc310 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -7,6 +7,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -62,14 +63,13 @@ export default class extends Endpoint { private noteEntityService: NoteEntityService, private queryService: QueryService, private metaService: MetaService, + private roleService: RoleService, private activeUsersChart: ActiveUsersChart, ) { super(meta, paramDef, async (ps, me) => { - const m = await this.metaService.fetch(); - if (m.disableLocalTimeline) { - if (me == null || (!me.isAdmin && !me.isModerator)) { - throw new ApiError(meta.errors.ltlDisabled); - } + const role = await this.roleService.getUserRoleOptions(me ? me.id : null); + if (!role.ltlAvailable) { + throw new ApiError(meta.errors.ltlDisabled); } //#region Construct query 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 793d7c5408..d583dfb936 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -162,13 +162,6 @@ export default class extends Endpoint { userId: me.id, }); - // Notify - this.createNotificationService.createNotification(note.userId, 'pollVote', { - notifierId: me.id, - noteId: note.id, - choice: ps.choice, - }); - // リモート投票の場合リプライ送信 if (note.userHost != null) { const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser; diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index 27b477e141..8eb031dfe3 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -6,6 +6,7 @@ import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['notes'], @@ -70,7 +71,7 @@ export default class extends Endpoint { } query - .andWhere('note.text ILIKE :q', { q: `%${ps.query}%` }) + .andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') 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 140614d36e..abea069da8 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,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; import type { NotesRepository, NoteThreadMutingsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -14,6 +15,11 @@ export const meta = { kind: 'write:account', + limit: { + duration: ms('1hour'), + max: 10, + }, + errors: { noSuchNote: { message: 'No such note.', diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 71c50ebba5..ab19771676 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -89,7 +89,6 @@ export default class extends Endpoint { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', - 'User-Agent': config.userAgent, Accept: 'application/json, */*', }, body: params.toString(), diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index eae8f18403..4015bf1f29 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -17,7 +17,7 @@ export const meta = { limit: { duration: ms('1hour'), - max: 300, + max: 10, }, res: { diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts index 96b22b0261..8bd0311dce 100644 --- a/packages/backend/src/server/api/endpoints/stats.ts +++ b/packages/backend/src/server/api/endpoints/stats.ts @@ -3,6 +3,8 @@ import { IsNull } from 'typeorm'; import type { InstancesRepository, NoteReactionsRepository, NotesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; +import NotesChart from '@/core/chart/charts/notes.js'; +import UsersChart from '@/core/chart/charts/users.js'; export const meta = { requireCredential: false, @@ -66,21 +68,24 @@ export default class extends Endpoint { @Inject(DI.noteReactionsRepository) private noteReactionsRepository: NoteReactionsRepository, + + private notesChart: NotesChart, + private usersChart: UsersChart, ) { super(meta, paramDef, async () => { + const notesChart = await this.notesChart.getChart('hour', 1, null); + const notesCount = notesChart.local.total[0] + notesChart.remote.total[0]; + const originalNotesCount = notesChart.local.total[0]; + + const usersChart = await this.usersChart.getChart('hour', 1, null); + const usersCount = usersChart.local.total[0] + usersChart.remote.total[0]; + const originalUsersCount = usersChart.local.total[0]; + const [ - notesCount, - originalNotesCount, - usersCount, - originalUsersCount, reactionsCount, //originalReactionsCount, instances, ] = await Promise.all([ - this.notesRepository.count({ cache: 3600000 }), // 1 hour - this.notesRepository.count({ where: { userHost: IsNull() }, cache: 3600000 }), - this.usersRepository.count({ cache: 3600000 }), - this.usersRepository.count({ where: { host: IsNull() }, cache: 3600000 }), this.noteReactionsRepository.count({ cache: 3600000 }), // 1 hour //this.noteReactionsRepository.count({ where: { userHost: IsNull() }, cache: 3600000 }), this.instancesRepository.count({ cache: 3600000 }), diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index b015129a7a..8becb68a34 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -27,7 +27,7 @@ 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' }, + state: { type: 'string', enum: ['all', 'alive'], default: 'all' }, origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' }, hostname: { type: 'string', @@ -54,9 +54,6 @@ export default class extends Endpoint { query.where('user.isExplorable = TRUE'); switch (ps.state) { - case 'admin': query.andWhere('user.isAdmin = TRUE'); break; - case 'moderator': query.andWhere('user.isModerator = TRUE'); break; - case 'adminOrModerator': query.andWhere('user.isAdmin = TRUE OR user.isModerator = TRUE'); break; case 'alive': query.andWhere('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break; } 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 c1f4f48445..24dbf5ca3c 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/create.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/create.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import type { UserGroup } from '@/models/entities/UserGroup.js'; @@ -16,6 +17,11 @@ export const meta = { description: 'Create a new group.', + limit: { + duration: ms('1hour'), + max: 10, + }, + 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 99f0751ea8..22e5e3ce78 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -5,6 +5,8 @@ import type { UserList } from '@/models/entities/UserList.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['lists'], @@ -20,6 +22,14 @@ export const meta = { optional: false, nullable: false, ref: 'UserList', }, + + errors: { + tooManyUserLists: { + message: 'You cannot create user list any more.', + code: 'TOO_MANY_USERLISTS', + id: '0cf21a28-7715-4f39-a20d-777bfdb8d138', + }, + }, } as const; export const paramDef = { @@ -39,8 +49,16 @@ export default class extends Endpoint { private userListEntityService: UserListEntityService, private idService: IdService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { + const currentCount = await this.userListsRepository.countBy({ + userId: me.id, + }); + if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).userListLimit) { + throw new ApiError(meta.errors.tooManyUserLists); + } + const userList = await this.userListsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), 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 c3a1308286..3a079ee1ab 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -15,6 +16,11 @@ export const meta = { description: 'Add a user to an existing list.', + limit: { + duration: ms('1hour'), + max: 30, + }, + errors: { noSuchList: { message: 'No such list.', @@ -105,7 +111,7 @@ export default class extends Endpoint { } // Push the user - await this.userListService.push(user, userList); + await this.userListService.push(user, userList, me); }); } } 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 13badab727..d19d4007d6 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -7,8 +7,9 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { MetaService } from '@/core/MetaService.js'; import { EmailService } from '@/core/EmailService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['users'], @@ -61,6 +62,7 @@ export default class extends Endpoint { private metaService: MetaService, private emailService: EmailService, private getterService: GetterService, + private roleService: RoleService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { @@ -74,7 +76,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.cannotReportYourself); } - if (user.isAdmin) { + if (await this.roleService.isAdministrator(user)) { throw new ApiError(meta.errors.cannotReportAdmin); } @@ -90,13 +92,7 @@ export default class extends Endpoint { // Publish event to moderators setImmediate(async () => { - const moderators = await this.usersRepository.find({ - where: [{ - isAdmin: true, - }, { - isModerator: true, - }], - }); + const moderators = await this.roleService.getModerators(); for (const moderator of moderators) { this.globalEventService.publishAdminStream(moderator.id, 'newAbuseUserReport', { 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 f13df3ee9d..95491211bc 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 @@ -6,6 +6,7 @@ import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['users'], @@ -59,10 +60,10 @@ export default class extends Endpoint { if (ps.host) { const q = this.usersRepository.createQueryBuilder('user') .where('user.isSuspended = FALSE') - .andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' }); + .andWhere('user.host LIKE :host', { host: sqlLikeEscape(ps.host.toLowerCase()) + '%' }); if (ps.username) { - q.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }); + q.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }); } q.andWhere('user.updatedAt IS NOT NULL'); @@ -83,7 +84,7 @@ export default class extends Endpoint { .where(`user.id IN (${ followingQuery.getQuery() })`) .andWhere('user.id != :meId', { meId: me.id }) .andWhere('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }) .andWhere(new Brackets(qb => { qb .where('user.updatedAt IS NULL') .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); @@ -101,7 +102,7 @@ export default class extends Endpoint { .where(`user.id NOT IN (${ followingQuery.getQuery() })`) .andWhere('user.id != :meId', { meId: me.id }) .andWhere('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }) .andWhere('user.updatedAt IS NOT NULL'); otherQuery.setParameters(followingQuery.getParameters()); @@ -116,7 +117,7 @@ export default class extends Endpoint { } else { users = await this.usersRepository.createQueryBuilder('user') .where('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }) .andWhere('user.updatedAt IS NOT NULL') .orderBy('user.updatedAt', 'DESC') .take(ps.limit - users.length) diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index ba07714972..d7a60f0437 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -5,6 +5,7 @@ import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['users'], @@ -57,7 +58,7 @@ export default class extends Endpoint { if (isUsername) { const usernameQuery = this.usersRepository.createQueryBuilder('user') - .where('user.usernameLower LIKE :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) + .where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' }) .andWhere(new Brackets(qb => { qb .where('user.updatedAt IS NULL') .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); @@ -78,11 +79,11 @@ export default class extends Endpoint { } else { const nameQuery = this.usersRepository.createQueryBuilder('user') .where(new Brackets(qb => { - qb.where('user.name ILIKE :query', { query: '%' + ps.query + '%' }); + qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); // Also search username if it qualifies as username if (this.userEntityService.validateLocalUsername(ps.query)) { - qb.orWhere('user.usernameLower LIKE :username', { username: '%' + ps.query.toLowerCase() + '%' }); + qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' }); } })) .andWhere(new Brackets(qb => { qb @@ -106,7 +107,7 @@ export default class extends Endpoint { if (users.length < ps.limit) { const profQuery = this.userProfilesRepository.createQueryBuilder('prof') .select('prof.userId') - .where('prof.description ILIKE :query', { query: '%' + ps.query + '%' }); + .where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); if (ps.origin === 'local') { profQuery.andWhere('prof.userHost IS NULL'); diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index fcdaeae1c9..70258ef009 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -7,6 +7,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DI } from '@/di-symbols.js'; import PerUserPvChart from '@/core/chart/charts/per-user-pv.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import type { FindOptionsWhere } from 'typeorm'; @@ -91,20 +92,21 @@ export default class extends Endpoint { private userEntityService: UserEntityService, private remoteUserResolveService: RemoteUserResolveService, + private roleService: RoleService, private perUserPvChart: PerUserPvChart, private apiLoggerService: ApiLoggerService, ) { super(meta, paramDef, async (ps, me, _1, _2, _3, ip) => { let user; - const isAdminOrModerator = me && (me.isAdmin || me.isModerator); + const isModerator = await this.roleService.isModerator(me); if (ps.userIds) { if (ps.userIds.length === 0) { return []; } - const users = await this.usersRepository.findBy(isAdminOrModerator ? { + const users = await this.usersRepository.findBy(isModerator ? { id: In(ps.userIds), } : { id: In(ps.userIds), @@ -135,7 +137,7 @@ export default class extends Endpoint { user = await this.usersRepository.findOneBy(q); } - if (user == null || (!isAdminOrModerator && user.isSuspended)) { + if (user == null || (!isModerator && user.isSuspended)) { throw new ApiError(meta.errors.noSuchUser); } diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 34f782e580..185c813869 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -7,6 +7,7 @@ import type { Packed } from '@/misc/schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; import Channel from '../channel.js'; class GlobalTimelineChannel extends Channel { @@ -16,6 +17,7 @@ class GlobalTimelineChannel extends Channel { constructor( private metaService: MetaService, + private roleService: RoleService, private noteEntityService: NoteEntityService, id: string, @@ -27,10 +29,8 @@ class GlobalTimelineChannel extends Channel { @bindThis public async init(params: any) { - const meta = await this.metaService.fetch(); - if (meta.disableGlobalTimeline) { - if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return; - } + const role = await this.roleService.getUserRoleOptions(this.user ? this.user.id : null); + if (!role.gtlAvailable) return; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -95,6 +95,7 @@ export class GlobalTimelineChannelService { constructor( private metaService: MetaService, + private roleService: RoleService, private noteEntityService: NoteEntityService, ) { } @@ -103,6 +104,7 @@ export class GlobalTimelineChannelService { public create(id: string, connection: Channel['connection']): GlobalTimelineChannel { return new GlobalTimelineChannel( this.metaService, + this.roleService, this.noteEntityService, id, connection, diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 6c6afb12bf..a0f75f202c 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -8,6 +8,7 @@ import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; import Channel from '../channel.js'; class HybridTimelineChannel extends Channel { @@ -17,6 +18,7 @@ class HybridTimelineChannel extends Channel { constructor( private metaService: MetaService, + private roleService: RoleService, private noteEntityService: NoteEntityService, id: string, @@ -28,8 +30,8 @@ class HybridTimelineChannel extends Channel { @bindThis public async init(params: any): Promise { - const meta = await this.metaService.fetch(); - if (meta.disableLocalTimeline && !this.user!.isAdmin && !this.user!.isModerator) return; + const role = await this.roleService.getUserRoleOptions(this.user ? this.user.id : null); + if (!role.ltlAvailable) return; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -112,6 +114,7 @@ export class HybridTimelineChannelService { constructor( private metaService: MetaService, + private roleService: RoleService, private noteEntityService: NoteEntityService, ) { } @@ -120,6 +123,7 @@ export class HybridTimelineChannelService { public create(id: string, connection: Channel['connection']): HybridTimelineChannel { return new HybridTimelineChannel( this.metaService, + this.roleService, this.noteEntityService, id, connection, diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 54388787ef..7d76f42fe7 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -6,6 +6,7 @@ import type { Packed } from '@/misc/schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; import Channel from '../channel.js'; class LocalTimelineChannel extends Channel { @@ -15,6 +16,7 @@ class LocalTimelineChannel extends Channel { constructor( private metaService: MetaService, + private roleService: RoleService, private noteEntityService: NoteEntityService, id: string, @@ -26,10 +28,8 @@ class LocalTimelineChannel extends Channel { @bindThis public async init(params: any) { - const meta = await this.metaService.fetch(); - if (meta.disableLocalTimeline) { - if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return; - } + const role = await this.roleService.getUserRoleOptions(this.user ? this.user.id : null); + if (!role.ltlAvailable) return; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -92,6 +92,7 @@ export class LocalTimelineChannelService { constructor( private metaService: MetaService, + private roleService: RoleService, private noteEntityService: NoteEntityService, ) { } @@ -100,6 +101,7 @@ export class LocalTimelineChannelService { public create(id: string, connection: Channel['connection']): LocalTimelineChannel { return new LocalTimelineChannel( this.metaService, + this.roleService, this.noteEntityService, id, connection, diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index ec05be56ee..03837baefb 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -14,23 +14,35 @@ import type { Page } from '@/models/entities/Page.js'; import type { Packed } from '@/misc/schema.js'; import type { Webhook } from '@/models/entities/Webhook.js'; import type { Meta } from '@/models/entities/Meta.js'; +import { Following, Role, RoleAssignment } from '@/models'; import type Emitter from 'strict-event-emitter-types'; import type { EventEmitter } from 'events'; +// redis通すとDateのインスタンスはstringに変換されるので +type Serialized = { + [K in keyof T]: T[K] extends Date ? string : T[K]; +}; + //#region Stream type-body definitions export interface InternalStreamTypes { - userChangeSuspendedState: { id: User['id']; isSuspended: User['isSuspended']; }; - userChangeSilencedState: { id: User['id']; isSilenced: User['isSilenced']; }; - userChangeModeratorState: { id: User['id']; isModerator: User['isModerator']; }; - userTokenRegenerated: { id: User['id']; oldToken: User['token']; newToken: User['token']; }; - remoteUserUpdated: { id: User['id']; }; - webhookCreated: Webhook; - webhookDeleted: Webhook; - webhookUpdated: Webhook; - antennaCreated: Antenna; - antennaDeleted: Antenna; - antennaUpdated: Antenna; - metaUpdated: Meta, + userChangeSuspendedState: Serialized<{ id: User['id']; isSuspended: User['isSuspended']; }>; + userTokenRegenerated: Serialized<{ id: User['id']; oldToken: User['token']; newToken: User['token']; }>; + remoteUserUpdated: Serialized<{ id: User['id']; }>; + follow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>; + unfollow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>; + defaultRoleOverrideUpdated: Serialized; + roleCreated: Serialized; + roleDeleted: Serialized; + roleUpdated: Serialized; + userRoleAssigned: Serialized; + userRoleUnassigned: Serialized; + webhookCreated: Serialized; + webhookDeleted: Serialized; + webhookUpdated: Serialized; + antennaCreated: Serialized; + antennaDeleted: Serialized; + antennaUpdated: Serialized; + metaUpdated: Serialized; } export interface BroadcastTypes { diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index a7701e1b24..2a764a25b0 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -1,6 +1,5 @@ import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { PathOrFileDescriptor, readFileSync } from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { createBullBoard } from '@bull-board/api'; import { BullAdapter } from '@bull-board/api/bullAdapter.js'; @@ -30,6 +29,7 @@ import type { ChannelsRepository, ClipsRepository, EmojisRepository, FlashsRepos import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; +import { RoleService } from '@/core/RoleService.js'; import manifest from './manifest.json' assert { type: 'json' }; import { FeedService } from './FeedService.js'; import { UrlPreviewService } from './UrlPreviewService.js'; @@ -71,9 +71,6 @@ export class ClientServerService { @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, - @Inject(DI.emojisRepository) - private emojisRepository: EmojisRepository, - @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, @@ -87,6 +84,7 @@ export class ClientServerService { private metaService: MetaService, private urlPreviewService: UrlPreviewService, private feedService: FeedService, + private roleService: RoleService, @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, @@ -129,7 +127,12 @@ export class ClientServerService { throw new Error('login required'); } const user = await this.usersRepository.findOneBy({ token }); - if (user == null || !(user.isAdmin || user.isModerator)) { + if (user == null) { + reply.code(403); + throw new Error('no such user'); + } + const isAdministrator = await this.roleService.isAdministrator(user); + if (!isAdministrator) { reply.code(403); throw new Error('access denied'); } @@ -225,44 +228,6 @@ export class ClientServerService { return reply.sendFile('/apple-touch-icon.png', staticAssets); }); - fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { - const path = request.params.path; - - if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) { - reply.code(404); - return; - } - - reply.header('Cache-Control', 'public, max-age=86400'); - - const name = path.split('@')[0].replace('.webp', ''); - const host = path.split('@')[1]?.replace('.webp', ''); - - const emoji = await this.emojisRepository.findOneBy({ - // `@.` is the spec of ReactionService.decodeReaction - host: (host == null || host === '.') ? IsNull() : host, - name: name, - }); - - if (emoji == null) { - reply.code(404); - return; - } - - reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); - - const url = new URL('/proxy/emoji.webp', this.config.url); - // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) - url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); - url.searchParams.set('emoji', '1'); - if ('static' in request.query) url.searchParams.set('static', '1'); - - return await reply.redirect( - 301, - url.toString(), - ); - }); - fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => { const path = request.params.path; @@ -354,7 +319,7 @@ export class ClientServerService { fastify.get('/opensearch.xml', async (request, reply) => { const meta = await this.metaService.fetch(); - const name = meta.name || 'Misskey'; + const name = meta.name ?? 'Misskey'; let content = ''; content += ''; content += `${name}`; @@ -575,13 +540,12 @@ export class ClientServerService { }); // Clip - // TODO: 非publicなclipのハンドリング fastify.get<{ Params: { clip: string; } }>('/clips/:clip', async (request, reply) => { const clip = await this.clipsRepository.findOneBy({ id: request.params.clip, }); - if (clip) { + if (clip && clip.isPublic) { const _clip = await this.clipEntityService.pack(clip); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId }); const meta = await this.metaService.fetch(); diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 86df3308ec..e2fc27fecd 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -301,6 +301,10 @@ const meta = await res.json(); + if (meta.version == null) { + throw new Error('failed to fetch instance metadata'); + } + if (meta.version != v) { localStorage.setItem('v', meta.version); refresh(); diff --git a/packages/frontend/assets/cookie.png b/packages/frontend/assets/cookie.png new file mode 100644 index 0000000000..4a7f040613 Binary files /dev/null and b/packages/frontend/assets/cookie.png differ diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 0dec916fa0..730389a2e6 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -11,14 +11,14 @@ "@rollup/plugin-alias": "4.0.2", "@rollup/plugin-json": "6.0.0", "@rollup/pluginutils": "5.0.2", - "@syuilo/aiscript": "0.12.1", + "@syuilo/aiscript": "0.12.2", "@tabler/icons": "^1.118.0", "@vitejs/plugin-vue": "4.0.0", "@vue/compiler-sfc": "3.2.45", "autobind-decorator": "2.4.0", "autosize": "5.0.2", "blurhash": "2.0.4", - "broadcast-channel": "4.19.1", + "broadcast-channel": "4.20.1", "browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3", "canvas-confetti": "^1.6.0", "chart.js": "4.1.2", @@ -36,16 +36,15 @@ "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", "json5": "2.2.3", - "katex": "0.16.4", "matter-js": "0.18.0", - "mfm-js": "0.23.0", + "mfm-js": "0.23.3", "misskey-js": "0.0.14", "photoswipe": "5.3.4", "prismjs": "1.29.0", - "punycode": "2.1.1", + "punycode": "2.2.0", "querystring": "0.2.1", "rndstr": "1.0.0", - "rollup": "3.9.1", + "rollup": "3.10.0", "s-age": "1.1.2", "sanitize-html": "^2.8.1", "sass": "1.57.1", @@ -73,8 +72,8 @@ "@types/glob": "8.0.0", "@types/gulp": "4.0.10", "@types/gulp-rename": "2.0.1", - "@types/katex": "0.16.0", "@types/matter-js": "0.18.2", + "@types/node": "^18.11.18", "@types/punycode": "2.1.0", "@types/sanitize-html": "^2.8.0", "@types/seedrandom": "3.0.4", @@ -83,16 +82,16 @@ "@types/uuid": "9.0.0", "@types/websocket": "1.0.5", "@types/ws": "8.5.4", - "@typescript-eslint/eslint-plugin": "5.48.0", - "@typescript-eslint/parser": "5.48.0", + "@typescript-eslint/eslint-plugin": "5.48.1", + "@typescript-eslint/parser": "5.48.1", "@vue/runtime-core": "3.2.45", "cross-env": "7.0.3", "cypress": "12.3.0", "eslint": "8.31.0", - "eslint-plugin-import": "2.26.0", - "eslint-plugin-vue": "9.8.0", + "eslint-plugin-import": "2.27.4", + "eslint-plugin-vue": "9.9.0", "start-server-and-test": "1.15.2", "vue-eslint-parser": "^9.1.0", - "vue-tsc": "^1.0.22" + "vue-tsc": "^1.0.24" } } diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index cdfe323d50..96f58269c4 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -37,7 +37,7 @@ - diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index e2d8c010a2..5844a18633 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -33,12 +33,12 @@ {{ c.text }} - + - +