Compare commits
142 Commits
13.0.0-bet
...
13.0.0-rc.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fea7460930 | ||
![]() |
1bf2bf1773 | ||
![]() |
3d668ad10d | ||
![]() |
2801338a3c | ||
![]() |
b66f4ebba1 | ||
![]() |
9ee1b5f30a | ||
![]() |
0f31a0548c | ||
![]() |
ffc29aa6f5 | ||
![]() |
d23aa94b41 | ||
![]() |
c1b6378951 | ||
![]() |
bb5d2bda51 | ||
![]() |
d075471b2d | ||
![]() |
199d98bf79 | ||
![]() |
3ae798d526 | ||
![]() |
e1bd61c70e | ||
![]() |
0296f841c3 | ||
![]() |
bd1f4b8d98 | ||
![]() |
dc19f20153 | ||
![]() |
f5cd809f62 | ||
![]() |
09d5a7806a | ||
![]() |
4606f23ed8 | ||
![]() |
8451e08aaa | ||
![]() |
2047449294 | ||
![]() |
d61eee695f | ||
![]() |
73b62797cd | ||
![]() |
170cfc6a0e | ||
![]() |
6bf1d7e398 | ||
![]() |
e46e7f5252 | ||
![]() |
5952f1ac24 | ||
![]() |
a08369fe36 | ||
![]() |
6cb9612943 | ||
![]() |
76c049522e | ||
![]() |
c41879c542 | ||
![]() |
99bdb11d24 | ||
![]() |
c2009acb2d | ||
![]() |
46d2a8726e | ||
![]() |
7df3ca7388 | ||
![]() |
51b8d4ae3e | ||
![]() |
ab1124abba | ||
![]() |
3db84a2e8f | ||
![]() |
9a78bbf0f1 | ||
![]() |
efbec444e8 | ||
![]() |
2f06f2a6da | ||
![]() |
b8da51e08c | ||
![]() |
af6a578fa6 | ||
![]() |
73d735a1f7 | ||
![]() |
b8b1899a9f | ||
![]() |
d52f0617a1 | ||
![]() |
c730973294 | ||
![]() |
2c2e064871 | ||
![]() |
e3c39d4b52 | ||
![]() |
5da74897ae | ||
![]() |
4b1009b34e | ||
![]() |
203a7ad073 | ||
![]() |
34a7b52105 | ||
![]() |
30fc166c08 | ||
![]() |
c84d86b368 | ||
![]() |
1e5d4db0a1 | ||
![]() |
5e02f0d325 | ||
![]() |
ce5506f331 | ||
![]() |
91105845d8 | ||
![]() |
2bedc084a3 | ||
![]() |
027ef1ea4a | ||
![]() |
668aa17eef | ||
![]() |
ebf8ef22e4 | ||
![]() |
bcb5182e86 | ||
![]() |
f45059b7b1 | ||
![]() |
d0aee58599 | ||
![]() |
68e65ed5df | ||
![]() |
367ccb9971 | ||
![]() |
4151087d3c | ||
![]() |
39c058a4bb | ||
![]() |
d1807ee5dc | ||
![]() |
e6a76b31be | ||
![]() |
98469117bf | ||
![]() |
a5becfc042 | ||
![]() |
d2204fd5c8 | ||
![]() |
519a08f8b5 | ||
![]() |
303519a1bd | ||
![]() |
161da24841 | ||
![]() |
6e40024660 | ||
![]() |
73c78d4c38 | ||
![]() |
2654936c17 | ||
![]() |
23810e3e1e | ||
![]() |
d6c89bf003 | ||
![]() |
49ab2a5f93 | ||
![]() |
bc0b8afb1f | ||
![]() |
b250456814 | ||
![]() |
0a6e237d09 | ||
![]() |
54ff4e53cb | ||
![]() |
002ccbb5f0 | ||
![]() |
7b7faf1e84 | ||
![]() |
9936088200 | ||
![]() |
990f4b52bd | ||
![]() |
4c21d83639 | ||
![]() |
d43a4a2d46 | ||
![]() |
8d2c3bb18d | ||
![]() |
4e39e690b6 | ||
![]() |
6458239a7c | ||
![]() |
a5aaa032ca | ||
![]() |
71bbef69c7 | ||
![]() |
c5c40a73b7 | ||
![]() |
74910f8d70 | ||
![]() |
e00003edff | ||
![]() |
bedb98185e | ||
![]() |
da6f955d58 | ||
![]() |
6bdccea26b | ||
![]() |
b2117ba3a1 | ||
![]() |
ba349fc62f | ||
![]() |
b2c79a5f2c | ||
![]() |
3e415e733d | ||
![]() |
a5e84e5de9 | ||
![]() |
8673353029 | ||
![]() |
4579d02296 | ||
![]() |
978a9bbb3b | ||
![]() |
2470afaa2e | ||
![]() |
60e545b2fd | ||
![]() |
6555644b88 | ||
![]() |
df56bd6d57 | ||
![]() |
e51432a461 | ||
![]() |
90e2186872 | ||
![]() |
3043b2f619 | ||
![]() |
d2fc5a248b | ||
![]() |
e6d666e1ee | ||
![]() |
c5cfbd99d0 | ||
![]() |
33b22a323c | ||
![]() |
f032fb628a | ||
![]() |
7761eb8897 | ||
![]() |
58fa8c4a01 | ||
![]() |
789d61d175 | ||
![]() |
b52fd72727 | ||
![]() |
d79905e141 | ||
![]() |
cd6b1290cb | ||
![]() |
c382497167 | ||
![]() |
a8fb578854 | ||
![]() |
ff00c90a88 | ||
![]() |
d0755b5ce8 | ||
![]() |
17fa5667b8 | ||
![]() |
01d5e385ec | ||
![]() |
af80fee899 | ||
![]() |
6b37c09274 | ||
![]() |
1453a0f5cf |
@@ -122,10 +122,12 @@ id: 'aid'
|
||||
# Proxy for HTTP/HTTPS
|
||||
#proxy: http://127.0.0.1:3128
|
||||
|
||||
#proxyBypassHosts: [
|
||||
# 'example.com',
|
||||
# '192.0.2.8'
|
||||
#]
|
||||
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
|
||||
|
3
.github/workflows/pr-preview-deploy.yml
vendored
3
.github/workflows/pr-preview-deploy.yml
vendored
@@ -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,7 +10,6 @@ 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:
|
||||
|
1
.github/workflows/pr-preview-destroy.yml
vendored
1
.github/workflows/pr-preview-destroy.yml
vendored
@@ -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
|
||||
|
@@ -1 +1 @@
|
||||
v18.13.0
|
||||
v18.12.1
|
||||
|
44
CHANGELOG.md
44
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,17 +33,26 @@ 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.2にアップデートされました
|
||||
- 0.12.xの変更点についてはこちら https://github.com/syuilo/aiscript/blob/master/CHANGELOG.md#0120
|
||||
- 0.12.x未満のプラグインは読み込むことはできません
|
||||
- iOS15以下のデバイスはサポートされなくなりました
|
||||
- Firefox109以下はサポートされなくなりました
|
||||
- Firefox110以下はサポートされなくなりました
|
||||
- 109でもContainerQueriesのフラグを有効にする事で問題なく使用できます
|
||||
|
||||
#### For app developers
|
||||
- API: metaのレスポンスに`emojis`プロパティが含まれなくなりました
|
||||
@@ -51,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
|
||||
@@ -58,9 +74,22 @@ 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
|
||||
- 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
|
||||
@@ -88,6 +117,7 @@ You should also include the user name that made the change.
|
||||
- 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
|
||||
@@ -95,6 +125,7 @@ You should also include the user name that made the change.
|
||||
- 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
|
||||
@@ -107,8 +138,12 @@ You should also include the user name that made the change.
|
||||
- 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
|
||||
@@ -117,6 +152,11 @@ You should also include the user name that made the change.
|
||||
- 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
|
||||
|
30
Dockerfile
30
Dockerfile
@@ -1,4 +1,6 @@
|
||||
FROM node:18.13.0-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.13.0-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", "--"]
|
||||
|
@@ -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');
|
||||
});
|
||||
|
@@ -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
|
||||
|
@@ -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: "الخيط الزمني"
|
||||
|
@@ -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: "টাইমলাইন"
|
||||
|
@@ -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"
|
||||
|
@@ -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ář"
|
||||
|
@@ -924,6 +924,63 @@ 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: "<b>Moderatoren</b> können grundlegende Verwaltungsaufgaben erledigen.\n<b>Administratoren</b> können alle Einstellungen der Instanz verwalten."
|
||||
assignTarget: "Zuweisungsart"
|
||||
descriptionOfAssignTarget: "<b>Manuell</b> bedeutet, dass die Liste der Benutzer einer Rolle manuell verwaltet wird.\n<b>Konditionell</b> 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"
|
||||
@@ -1302,6 +1359,8 @@ _weekday:
|
||||
friday: "Freitag"
|
||||
saturday: "Samstag"
|
||||
_widgets:
|
||||
profile: "Profil"
|
||||
instanceInfo: "Instanzinformationen"
|
||||
memo: "Merkzettel"
|
||||
notifications: "Benachrichtigungen"
|
||||
timeline: "Chronik"
|
||||
|
@@ -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: "Ημερολόγιο"
|
||||
|
@@ -924,6 +924,63 @@ 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: "<b>Moderators</b> can perform basic moderation operations.\n<b>Administrators</b> can change all settings of the instance."
|
||||
assignTarget: "Assignment type"
|
||||
descriptionOfAssignTarget: "<b>Manual</b> to manually change who is part of this role and who is not.\n<b>Conditional</b> 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"
|
||||
@@ -1302,6 +1359,8 @@ _weekday:
|
||||
friday: "Friday"
|
||||
saturday: "Saturday"
|
||||
_widgets:
|
||||
profile: "Profile"
|
||||
instanceInfo: "Instance Information"
|
||||
memo: "Sticky notes"
|
||||
notifications: "Notifications"
|
||||
timeline: "Timeline"
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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: "<b>Moderatori</b> possono svolgere le attività di moderazione basilari.\n<b>Amministratori</b> possono modificare la configurazione dell'istanza."
|
||||
assignTarget: "Assegna il target"
|
||||
descriptionOfAssignTarget: "<b>Manuale</b> per assegnare manualmente questo ruolo ai profili.\n<b>Condizionale</b> 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"
|
||||
|
@@ -193,7 +193,7 @@ clearQueueConfirmText: "未配達の投稿は配送されなくなります。
|
||||
clearCachedFiles: "キャッシュをクリア"
|
||||
clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?"
|
||||
blockedInstances: "ブロックしたインスタンス"
|
||||
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。"
|
||||
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。サブドメインもブロックされます。"
|
||||
muteAndBlock: "ミュートとブロック"
|
||||
mutedUsers: "ミュートしたユーザー"
|
||||
blockedUsers: "ブロックしたユーザー"
|
||||
@@ -924,6 +924,64 @@ neverShow: "今後表示しない"
|
||||
remindMeLater: "また後で"
|
||||
didYouLikeMisskey: "Misskeyを気に入っていただけましたか?"
|
||||
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!"
|
||||
roles: "ロール"
|
||||
role: "ロール"
|
||||
normalUser: "一般ユーザー"
|
||||
undefined: "未定義"
|
||||
assign: "アサイン"
|
||||
unassign: "アサインを解除"
|
||||
color: "色"
|
||||
manageCustomEmojis: "カスタム絵文字の管理"
|
||||
youCannotCreateAnymore: "これ以上作成することはできません。"
|
||||
|
||||
_role:
|
||||
new: "ロールの作成"
|
||||
edit: "ロールの編集"
|
||||
name: "ロール名"
|
||||
description: "ロールの説明"
|
||||
permission: "ロールの権限"
|
||||
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関する操作を行えます。\n<b>管理者</b>はインスタンスの全ての設定を変更できます。"
|
||||
assignTarget: "アサインターゲット"
|
||||
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
|
||||
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: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
||||
|
@@ -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: "スクリプト"
|
||||
|
@@ -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"
|
||||
|
@@ -69,6 +69,7 @@ _mfm:
|
||||
_sfx:
|
||||
notification: "ಅಧಿಸೂಚನೆಗಳು"
|
||||
_widgets:
|
||||
profile: "ಪ್ರೊಫೈಲು"
|
||||
notifications: "ಅಧಿಸೂಚನೆಗಳು"
|
||||
timeline: "ಸಮಯಸಾಲು"
|
||||
_cw:
|
||||
|
@@ -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: "복구"
|
||||
@@ -924,6 +924,63 @@ neverShow: "다시 보지 않기"
|
||||
remindMeLater: "나중에 알림"
|
||||
didYouLikeMisskey: "Misskey가 마음에 드시나요?"
|
||||
pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!"
|
||||
roles: "역할"
|
||||
role: "역할"
|
||||
normalUser: "일반 사용자"
|
||||
undefined: "정의되지 않음"
|
||||
assign: "할당"
|
||||
unassign: "할당 취소"
|
||||
color: "색"
|
||||
manageCustomEmojis: "커스텀 이모지 관리"
|
||||
youCannotCreateAnymore: "더 이상 생성할 수 없습니다."
|
||||
_role:
|
||||
new: "새 역할 생성"
|
||||
edit: "역할 수정"
|
||||
name: "역할 이름"
|
||||
description: "역할 설명"
|
||||
permission: "역할의 권한"
|
||||
descriptionOfPermission: "<b>모더레이터</b>는 기본적인 중재와 관련된 작업을 수행할 수 있습니다.\n<b>관리자</b>는 인스턴스의 모든 설정을 변경할 수 있습니다."
|
||||
assignTarget: "할당 대상"
|
||||
descriptionOfAssignTarget: "<b>수동</b>을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n<b>조건부</b>를 선택하면 조건을 설정해 일치하는 사용자를 자동으로 포함되게 할 수 있습니다."
|
||||
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: "탐지 민감도"
|
||||
@@ -1302,6 +1359,8 @@ _weekday:
|
||||
friday: "금요일"
|
||||
saturday: "토요일"
|
||||
_widgets:
|
||||
profile: "프로필"
|
||||
instanceInfo: "인스턴스 정보"
|
||||
memo: "스티커 메모"
|
||||
notifications: "알림"
|
||||
timeline: "타임라인"
|
||||
@@ -1325,7 +1384,7 @@ _widgets:
|
||||
aiscript: "AiScript 콘솔"
|
||||
aiscriptApp: "AiScript 앱"
|
||||
aichan: "아이"
|
||||
userList: "사용자 목록"
|
||||
userList: "유저 리스트"
|
||||
_userList:
|
||||
chooseList: "리스트 선택"
|
||||
clicker: "클리커"
|
||||
|
@@ -440,6 +440,8 @@ _sfx:
|
||||
notification: "Meldingen"
|
||||
chat: "Chat"
|
||||
_widgets:
|
||||
profile: "Profiel"
|
||||
instanceInfo: "Serverinformatie"
|
||||
notifications: "Meldingen"
|
||||
timeline: "Tijdlijn"
|
||||
activity: "Activiteit"
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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: "Лента"
|
||||
|
@@ -917,6 +917,7 @@ 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"
|
||||
@@ -1295,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"
|
||||
|
@@ -327,7 +327,16 @@ 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"
|
||||
@@ -377,7 +386,16 @@ _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"
|
||||
@@ -395,6 +413,7 @@ _profile:
|
||||
changeAvatar: "Ändra profilbild"
|
||||
changeBanner: "Ändra banner"
|
||||
_exportOrImport:
|
||||
allNotes: "Alla noter"
|
||||
followingList: "Följer"
|
||||
muteList: "Tysta"
|
||||
blockingList: "Blockera"
|
||||
@@ -423,5 +442,6 @@ _deck:
|
||||
_columns:
|
||||
notifications: "Notifikationer"
|
||||
tl: "Tidslinje"
|
||||
antenna: "Antenner"
|
||||
list: "Listor"
|
||||
mentions: "Omnämningar"
|
||||
|
@@ -924,6 +924,55 @@ neverShow: "ไม่ต้องแสดงข้อความนี้อ
|
||||
remindMeLater: "ไว้ครั้งหน้าแล้วกัน"
|
||||
didYouLikeMisskey: "คุณเคยชอบ Misskey ไหม?"
|
||||
pleaseDonate: "{host} ใช้ซอฟต์แวร์ฟรี Misskey เราขอขอบคุณการบริจาคของคุณอย่างสูงเพื่อให้การพัฒนา Misskey สามารถดำเนินต่อไปได้นะ!"
|
||||
roles: "บทบาท"
|
||||
role: "บทบาท"
|
||||
normalUser: "ผู้ใช้มาตรฐาน"
|
||||
undefined: "ไม่ได้กำหนด"
|
||||
assign: "กำหนด"
|
||||
unassign: "ยังไม่มอบหมาย"
|
||||
color: "สี"
|
||||
manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
|
||||
_role:
|
||||
new: "บทบาทใหม่"
|
||||
edit: "แก้ไขบทบาท"
|
||||
name: "ชื่อบทบาท"
|
||||
description: "คำอธิบายบทบาท"
|
||||
permission: "สิทธิ์ตามบทบาท"
|
||||
descriptionOfPermission: "<b>ผู้ดูแลกลั่นกรองเนื้อหา</b> สามารถดำเนินการดูแลขั้นพื้นฐานได้นะ\n<b>ผู้ดูแลระบบ</b> สามารถเปลี่ยนการตั้งค่าทั้งหมดของอินสแตนซ์ได้นะ"
|
||||
assignTarget: "กำหนดเป้าหมาย"
|
||||
descriptionOfAssignTarget: "<b>แมนนวล</b> เพื่อเปลี่ยนผู้ที่เป็นส่วนหนึ่งของบทบาทนี้และใครที่ไม่ใช่ด้วยตนเอง\n<b>เงื่อนไข</b> เพื่อให้ผู้ใช้ได้รับการกำหนดและนำออกจากบทบาทนี้โดยอัตโนมัติตามเงื่อนไขชุดหนึ่ง"
|
||||
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: "การตรวจจับความไว"
|
||||
@@ -1302,6 +1351,8 @@ _weekday:
|
||||
friday: "วันศุกร์"
|
||||
saturday: "วันเสาร์"
|
||||
_widgets:
|
||||
profile: "โปรไฟล์"
|
||||
instanceInfo: "ข้อมูล อินสแตนซ์"
|
||||
memo: "โน้ตแปะ"
|
||||
notifications: "การเเจ้งเตือน"
|
||||
timeline: "ไทม์ไลน์"
|
||||
|
@@ -53,6 +53,7 @@ _mfm:
|
||||
_sfx:
|
||||
notification: "Bildirim"
|
||||
_widgets:
|
||||
profile: "Profil"
|
||||
notifications: "Bildirim"
|
||||
timeline: "Zaman çizelgesi"
|
||||
_profile:
|
||||
|
@@ -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: "Стрічка"
|
||||
|
@@ -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"
|
||||
|
@@ -13,7 +13,7 @@ fetchingAsApObject: "在联邦宇宙查询中..."
|
||||
ok: "OK"
|
||||
gotIt: "我明白了"
|
||||
cancel: "取消"
|
||||
noThankYou: "不用"
|
||||
noThankYou: "不用,谢谢"
|
||||
enterUsername: "输入用户名"
|
||||
renotedBy: "由 {user} 转贴"
|
||||
noNotes: "没有帖子"
|
||||
@@ -922,7 +922,65 @@ numberOfLikes: "点赞数"
|
||||
show: "显示"
|
||||
neverShow: "不再显示"
|
||||
remindMeLater: "稍后提醒我"
|
||||
didYouLikeMisskey: "你在Misskey玩得还开心吗?"
|
||||
didYouLikeMisskey: "您喜欢Misskey吗?"
|
||||
pleaseDonate: "Misskey是{host}所使用的免费软件。为了今后也能够维持Misskey的开发,请在有余力的情况下进行捐助!"
|
||||
roles: "角色"
|
||||
role: "角色"
|
||||
normalUser: "普通用户"
|
||||
undefined: "未定义"
|
||||
assign: "分配"
|
||||
unassign: "取消分配"
|
||||
color: "颜色"
|
||||
manageCustomEmojis: "管理自定义表情符号"
|
||||
youCannotCreateAnymore: "抱歉,您无法再创建更多了。"
|
||||
_role:
|
||||
new: "创建角色"
|
||||
edit: "编辑角色"
|
||||
name: "角色名称"
|
||||
description: "角色描述"
|
||||
permission: "角色权限"
|
||||
descriptionOfPermission: "<b>监察员</b>可以执行基本的审核操作。\n<b>管理员</b>可以更改实例的所有设置。"
|
||||
assignTarget: "授权对象"
|
||||
descriptionOfAssignTarget: "<b>手动</b>指手动选择谁被包括在这个角色中。\n<b>符合条件</b>指设置条件以自动包括符合条件的用户。"
|
||||
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: "检测敏感度"
|
||||
@@ -1301,6 +1359,8 @@ _weekday:
|
||||
friday: "星期五"
|
||||
saturday: "星期六"
|
||||
_widgets:
|
||||
profile: "个人资料"
|
||||
instanceInfo: "实例信息"
|
||||
memo: "便签"
|
||||
notifications: "通知"
|
||||
timeline: "时间线"
|
||||
@@ -1327,6 +1387,7 @@ _widgets:
|
||||
userList: "用户列表"
|
||||
_userList:
|
||||
chooseList: "选择列表"
|
||||
clicker: "点击器"
|
||||
_cw:
|
||||
hide: "隐藏"
|
||||
show: "查看更多"
|
||||
|
@@ -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: "刪除帳號"
|
||||
@@ -924,6 +924,58 @@ neverShow: "不再顯示"
|
||||
remindMeLater: "以後再說"
|
||||
didYouLikeMisskey: "您是否喜愛Misskey呢?"
|
||||
pleaseDonate: "Misskey是由{host}使用的免費軟體。請贊助我們,讓開發能夠持續!"
|
||||
roles: "角色"
|
||||
role: "角色"
|
||||
normalUser: "一般使用者"
|
||||
undefined: "未定義"
|
||||
assign: "指派"
|
||||
unassign: "取消指派"
|
||||
color: "顏色"
|
||||
manageCustomEmojis: "管理自訂表情符號"
|
||||
_role:
|
||||
new: "建立角色"
|
||||
edit: "編輯角色"
|
||||
name: "角色名稱"
|
||||
description: "角色描述 "
|
||||
permission: "角色的權限"
|
||||
descriptionOfPermission: "<b>審核員</b>執行與審核相關的基本操作。\n<b>管理員</b>能變更實例的全部設定。"
|
||||
assignTarget: "指派目標"
|
||||
descriptionOfAssignTarget: "<b>手動</b>是以手動管理這個角色包含的人員。\n<b>符合條件</b>是設定條件以自動包含符合條件的使用者。"
|
||||
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: "檢測敏感度"
|
||||
@@ -1159,7 +1211,7 @@ _theme:
|
||||
navActive: "側邊欄文本 (活動)"
|
||||
navIndicator: "側邊欄指示符"
|
||||
link: "鏈接"
|
||||
hashtag: "#tag"
|
||||
hashtag: "標籤"
|
||||
mention: "提到"
|
||||
mentionMe: "提到了我"
|
||||
renote: "轉發貼文"
|
||||
@@ -1302,6 +1354,8 @@ _weekday:
|
||||
friday: "週五"
|
||||
saturday: "週六"
|
||||
_widgets:
|
||||
profile: "個人檔案"
|
||||
instanceInfo: "實例資訊"
|
||||
memo: "備忘錄"
|
||||
notifications: "通知"
|
||||
timeline: "時間軸"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "13.0.0-beta.37",
|
||||
"version": "13.0.0-rc.6",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -60,5 +60,8 @@
|
||||
"eslint": "^8.31.0",
|
||||
"start-server-and-test": "1.15.2",
|
||||
"typescript": "4.9.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tensorflow/tfjs-core": "^4.1.0"
|
||||
}
|
||||
}
|
||||
|
11
packages/backend/migration/1673336077243-PollChoiceLength.js
Normal file
11
packages/backend/migration/1673336077243-PollChoiceLength.js
Normal file
@@ -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`);
|
||||
}
|
||||
}
|
37
packages/backend/migration/1673500412259-Role.js
Normal file
37
packages/backend/migration/1673500412259-Role.js
Normal file
@@ -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"`);
|
||||
}
|
||||
}
|
11
packages/backend/migration/1673515526953-RoleColor.js
Normal file
11
packages/backend/migration/1673515526953-RoleColor.js
Normal file
@@ -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"`);
|
||||
}
|
||||
}
|
13
packages/backend/migration/1673522856499-RoleIroiro.js
Normal file
13
packages/backend/migration/1673522856499-RoleIroiro.js
Normal file
@@ -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`);
|
||||
}
|
||||
}
|
13
packages/backend/migration/1673524604156-RoleLastUsedAt.js
Normal file
13
packages/backend/migration/1673524604156-RoleLastUsedAt.js
Normal file
@@ -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"`);
|
||||
}
|
||||
}
|
15
packages/backend/migration/1673570377815-RoleConditional.js
Normal file
15
packages/backend/migration/1673570377815-RoleConditional.js
Normal file
@@ -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"`);
|
||||
}
|
||||
}
|
11
packages/backend/migration/1673575973645-MetaClean.js
Normal file
11
packages/backend/migration/1673575973645-MetaClean.js
Normal file
@@ -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'`);
|
||||
}
|
||||
}
|
@@ -72,12 +72,11 @@
|
||||
"json5-loader": "4.0.1",
|
||||
"jsonld": "8.1.0",
|
||||
"jsrsasign": "10.6.1",
|
||||
"mfm-js": "0.23.1",
|
||||
"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",
|
||||
"node-fetch": "3.3.0",
|
||||
"nodemailer": "6.8.0",
|
||||
"nsfwjs": "2.4.2",
|
||||
"oauth": "^0.10.0",
|
||||
@@ -118,6 +117,7 @@
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.11",
|
||||
"ulid": "2.3.0",
|
||||
"undici": "^5.14.0",
|
||||
"unzipper": "0.10.11",
|
||||
"uuid": "9.0.0",
|
||||
"vary": "1.1.2",
|
||||
@@ -180,6 +180,7 @@
|
||||
"execa": "6.1.0",
|
||||
"jest": "29.3.1",
|
||||
"jest-mock": "^29.3.1",
|
||||
"node-fetch": "3.3.0",
|
||||
"typescript": "4.9.4"
|
||||
}
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ 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 { bindThis } from '@/decorators.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
@@ -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);
|
||||
|
@@ -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,
|
||||
) {
|
||||
}
|
||||
@@ -27,16 +21,16 @@ export class CaptchaService {
|
||||
response,
|
||||
});
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: params,
|
||||
headers: {
|
||||
'User-Agent': this.config.userAgent,
|
||||
const res = await this.httpRequestService.fetch(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
body: params,
|
||||
},
|
||||
// TODO
|
||||
//timeout: 10 * 1000,
|
||||
agent: (url, bypassProxy) => this.httpRequestService.getAgentByUrl(url, bypassProxy),
|
||||
}).catch(err => {
|
||||
{
|
||||
noOkError: true,
|
||||
}
|
||||
).catch(err => {
|
||||
throw `${err.message ?? err}`;
|
||||
});
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -53,7 +53,7 @@ export class CreateSystemUserService {
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: null,
|
||||
token: secret,
|
||||
isAdmin: false,
|
||||
isRoot: false,
|
||||
isLocked: true,
|
||||
isExplorable: false,
|
||||
isBot: true,
|
||||
|
@@ -23,6 +23,9 @@ export class DeleteAccountService {
|
||||
id: string;
|
||||
host: string | null;
|
||||
}): Promise<void> {
|
||||
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 => {});
|
||||
|
||||
|
@@ -8,11 +8,12 @@ import got, * as Got from 'got';
|
||||
import chalk from 'chalk';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { buildConnector } from 'undici';
|
||||
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@@ -20,6 +21,7 @@ import { bindThis } from '@/decorators.js';
|
||||
@Injectable()
|
||||
export class DownloadService {
|
||||
private logger: Logger;
|
||||
private undiciFetcher: UndiciFetcher;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
@@ -29,70 +31,42 @@ export class DownloadService {
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('download');
|
||||
|
||||
this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption(
|
||||
{
|
||||
connect: process.env.NODE_ENV === 'development' ?
|
||||
this.httpRequestService.clientDefaults.connect
|
||||
:
|
||||
this.httpRequestService.getConnectorWithIpCheck(
|
||||
buildConnector({
|
||||
...this.httpRequestService.clientDefaults.connect,
|
||||
}),
|
||||
(ip) => !this.isPrivateIp(ip)
|
||||
),
|
||||
bodyTimeout: 30 * 1000,
|
||||
},
|
||||
{
|
||||
connect: this.httpRequestService.clientDefaults.connect,
|
||||
}
|
||||
), this.logger);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async downloadUrl(url: string, path: string): Promise<void> {
|
||||
this.logger.info(`Downloading ${chalk.cyan(url)} to ${chalk.cyanBright(path)} ...`);
|
||||
|
||||
|
||||
const timeout = 30 * 1000;
|
||||
const operationTimeout = 60 * 1000;
|
||||
const maxSize = this.config.maxFileSize ?? 262144000;
|
||||
|
||||
const req = got.stream(url, {
|
||||
headers: {
|
||||
'User-Agent': this.config.userAgent,
|
||||
},
|
||||
timeout: {
|
||||
lookup: timeout,
|
||||
connect: timeout,
|
||||
secureConnect: timeout,
|
||||
socket: timeout, // read timeout
|
||||
response: timeout,
|
||||
send: timeout,
|
||||
request: operationTimeout, // whole operation timeout
|
||||
},
|
||||
agent: {
|
||||
http: this.httpRequestService.httpAgent,
|
||||
https: this.httpRequestService.httpsAgent,
|
||||
},
|
||||
http2: false, // default
|
||||
retry: {
|
||||
limit: 0,
|
||||
},
|
||||
}).on('response', (res: Got.Response) => {
|
||||
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) {
|
||||
if (this.isPrivateIp(res.ip)) {
|
||||
this.logger.warn(`Blocked address: ${res.ip}`);
|
||||
req.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const contentLength = res.headers['content-length'];
|
||||
if (contentLength != null) {
|
||||
const size = Number(contentLength);
|
||||
if (size > maxSize) {
|
||||
this.logger.warn(`maxSize exceeded (${size} > ${maxSize}) on response`);
|
||||
req.destroy();
|
||||
}
|
||||
}
|
||||
}).on('downloadProgress', (progress: Got.Progress) => {
|
||||
if (progress.transferred > maxSize) {
|
||||
this.logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`);
|
||||
req.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await pipeline(req, fs.createWriteStream(path));
|
||||
} catch (e) {
|
||||
if (e instanceof Got.HTTPError) {
|
||||
throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
||||
const response = await this.undiciFetcher.fetch(url);
|
||||
|
||||
if (response.body === null) {
|
||||
throw new StatusError('No body', 400, 'No body');
|
||||
}
|
||||
|
||||
|
||||
await pipeline(stream.Readable.fromWeb(response.body), fs.createWriteStream(path));
|
||||
|
||||
this.logger.succ(`Download finished: ${chalk.cyan(url)}`);
|
||||
}
|
||||
|
||||
@@ -114,7 +88,7 @@ export class DownloadService {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@bindThis
|
||||
private isPrivateIp(ip: string): boolean {
|
||||
for (const net of this.config.allowedPrivateNetworks ?? []) {
|
||||
@@ -124,6 +98,6 @@ export class DownloadService {
|
||||
}
|
||||
}
|
||||
|
||||
return PrivateIp(ip);
|
||||
return PrivateIp(ip) ?? false;
|
||||
}
|
||||
}
|
||||
|
@@ -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})`);
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { URL } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import fetch from 'node-fetch';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import type { Instance } from '@/models/entities/Instance.js';
|
||||
import type { InstancesRepository } from '@/models/index.js';
|
||||
@@ -191,11 +190,7 @@ export class FetchInstanceMetadataService {
|
||||
|
||||
const faviconUrl = url + '/favicon.ico';
|
||||
|
||||
const favicon = await fetch(faviconUrl, {
|
||||
// TODO
|
||||
//timeout: 10000,
|
||||
agent: url => this.httpRequestService.getAgentByUrl(url),
|
||||
});
|
||||
const favicon = await this.httpRequestService.fetch(faviconUrl, {}, { noOkError: true });
|
||||
|
||||
if (favicon.ok) {
|
||||
return faviconUrl;
|
||||
|
@@ -1,67 +1,257 @@
|
||||
import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import CacheableLookup from 'cacheable-lookup';
|
||||
import fetch from 'node-fetch';
|
||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { Response } from 'node-fetch';
|
||||
import type { URL } from 'node:url';
|
||||
import * as undici from 'undici';
|
||||
import { LookupFunction } from 'node:net';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
|
||||
// true to allow, false to deny
|
||||
export type IpChecker = (ip: string) => boolean;
|
||||
|
||||
/*
|
||||
* Child class to create and save Agent for fetch.
|
||||
* You should construct this when you want
|
||||
* to change timeout, size limit, socket connect function, etc.
|
||||
*/
|
||||
export class UndiciFetcher {
|
||||
/**
|
||||
* Get http non-proxy agent (undici)
|
||||
*/
|
||||
public nonProxiedAgent: undici.Agent;
|
||||
|
||||
/**
|
||||
* Get http proxy or non-proxy agent (undici)
|
||||
*/
|
||||
public agent: undici.ProxyAgent | undici.Agent;
|
||||
|
||||
private proxyBypassHosts: string[];
|
||||
private userAgent: string | undefined;
|
||||
|
||||
private logger: Logger | undefined;
|
||||
|
||||
constructor(
|
||||
args: {
|
||||
agentOptions: undici.Agent.Options;
|
||||
proxy?: {
|
||||
uri: string;
|
||||
options?: undici.Agent.Options; // Override of agentOptions
|
||||
},
|
||||
proxyBypassHosts?: string[];
|
||||
userAgent?: string;
|
||||
},
|
||||
logger?: Logger,
|
||||
) {
|
||||
this.logger = logger;
|
||||
this.logger?.debug('UndiciFetcher constructor', args);
|
||||
|
||||
this.proxyBypassHosts = args.proxyBypassHosts ?? [];
|
||||
this.userAgent = args.userAgent;
|
||||
|
||||
this.nonProxiedAgent = new undici.Agent({
|
||||
...args.agentOptions,
|
||||
connect: (process.env.NODE_ENV !== 'production' && typeof args.agentOptions.connect !== 'function')
|
||||
? (options, cb) => {
|
||||
// Custom connector for debug
|
||||
undici.buildConnector(args.agentOptions.connect as undici.buildConnector.BuildOptions)(options, (err, socket) => {
|
||||
this.logger?.debug('Socket connector called', socket);
|
||||
if (err) {
|
||||
this.logger?.debug(`Socket error`, err);
|
||||
cb(new Error(`Error while socket connecting\n${err}`), null);
|
||||
return;
|
||||
}
|
||||
this.logger?.debug(`Socket connected: port ${socket.localPort} => remote ${socket.remoteAddress}`);
|
||||
cb(null, socket);
|
||||
});
|
||||
} : args.agentOptions.connect,
|
||||
});
|
||||
|
||||
this.agent = args.proxy
|
||||
? new undici.ProxyAgent({
|
||||
...args.agentOptions,
|
||||
...args.proxy.options,
|
||||
|
||||
uri: args.proxy.uri,
|
||||
|
||||
connect: (process.env.NODE_ENV !== 'production' && typeof (args.proxy?.options?.connect ?? args.agentOptions.connect) !== 'function')
|
||||
? (options, cb) => {
|
||||
// Custom connector for debug
|
||||
undici.buildConnector((args.proxy?.options?.connect ?? args.agentOptions.connect) as undici.buildConnector.BuildOptions)(options, (err, socket) => {
|
||||
this.logger?.debug('Socket connector called (secure)', socket);
|
||||
if (err) {
|
||||
this.logger?.debug(`Socket error`, err);
|
||||
cb(new Error(`Error while socket connecting\n${err}`), null);
|
||||
return;
|
||||
}
|
||||
this.logger?.debug(`Socket connected (secure): port ${socket.localPort} => remote ${socket.remoteAddress}`);
|
||||
cb(null, socket);
|
||||
});
|
||||
} : (args.proxy?.options?.connect ?? args.agentOptions.connect),
|
||||
})
|
||||
: this.nonProxiedAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent by URL
|
||||
* @param url URL
|
||||
* @param bypassProxy Allways bypass proxy
|
||||
*/
|
||||
@bindThis
|
||||
public getAgentByUrl(url: URL, bypassProxy = false): undici.Agent | undici.ProxyAgent {
|
||||
if (bypassProxy || this.proxyBypassHosts.includes(url.hostname)) {
|
||||
return this.nonProxiedAgent;
|
||||
} else {
|
||||
return this.agent;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetch(
|
||||
url: string | URL,
|
||||
options: undici.RequestInit = {},
|
||||
privateOptions: { noOkError?: boolean; bypassProxy?: boolean; } = { noOkError: false, bypassProxy: false }
|
||||
): Promise<undici.Response> {
|
||||
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');
|
||||
});
|
||||
if (!res.ok && !privateOptions.noOkError) {
|
||||
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getJson<T extends unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<T> {
|
||||
const res = await this.fetch(
|
||||
url,
|
||||
{
|
||||
headers: Object.assign({
|
||||
Accept: accept,
|
||||
}, headers ?? {}),
|
||||
}
|
||||
);
|
||||
|
||||
return await res.json() as T;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getHtml(url: string, accept = 'text/html, */*', headers?: Record<string, string>): Promise<string> {
|
||||
const res = await this.fetch(
|
||||
url,
|
||||
{
|
||||
headers: Object.assign({
|
||||
Accept: accept,
|
||||
}, headers ?? {}),
|
||||
}
|
||||
);
|
||||
|
||||
return await res.text();
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class HttpRequestService {
|
||||
/**
|
||||
* Get http non-proxy agent
|
||||
*/
|
||||
public defaultFetcher: UndiciFetcher;
|
||||
public fetch: UndiciFetcher['fetch'];
|
||||
public getHtml: UndiciFetcher['getHtml'];
|
||||
public defaultJsonFetcher: UndiciFetcher;
|
||||
public getJson: UndiciFetcher['getJson'];
|
||||
|
||||
//#region for old http/https, only used in S3Service
|
||||
// http non-proxy agent
|
||||
private http: http.Agent;
|
||||
|
||||
/**
|
||||
* Get https non-proxy agent
|
||||
*/
|
||||
// https non-proxy agent
|
||||
private https: https.Agent;
|
||||
|
||||
/**
|
||||
* Get http proxy or non-proxy agent
|
||||
*/
|
||||
// http proxy or non-proxy agent
|
||||
public httpAgent: http.Agent;
|
||||
|
||||
/**
|
||||
* Get https proxy or non-proxy agent
|
||||
*/
|
||||
// https proxy or non-proxy agent
|
||||
public httpsAgent: https.Agent;
|
||||
//#endregion
|
||||
|
||||
public readonly dnsCache: CacheableLookup;
|
||||
public readonly clientDefaults: undici.Agent.Options;
|
||||
private maxSockets: number;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
const cache = new CacheableLookup({
|
||||
this.logger = this.loggerService.getLogger('http-request');
|
||||
|
||||
this.dnsCache = new CacheableLookup({
|
||||
maxTtl: 3600, // 1hours
|
||||
errorTtl: 30, // 30secs
|
||||
lookup: false, // nativeのdns.lookupにfallbackしない
|
||||
});
|
||||
|
||||
|
||||
this.clientDefaults = {
|
||||
keepAliveTimeout: 30 * 1000,
|
||||
keepAliveMaxTimeout: 10 * 60 * 1000,
|
||||
keepAliveTimeoutThreshold: 1 * 1000,
|
||||
strictContentLength: true,
|
||||
headersTimeout: 10 * 1000,
|
||||
bodyTimeout: 10 * 1000,
|
||||
maxHeaderSize: 16364, // default
|
||||
maxResponseSize: 10 * 1024 * 1024,
|
||||
maxRedirections: 3,
|
||||
connect: {
|
||||
timeout: 10 * 1000, // コネクションが確立するまでのタイムアウト
|
||||
maxCachedSessions: 300, // TLSセッションのキャッシュ数 https://github.com/nodejs/undici/blob/v5.14.0/lib/core/connect.js#L80
|
||||
lookup: this.dnsCache.lookup as LookupFunction, // https://github.com/nodejs/undici/blob/v5.14.0/lib/core/connect.js#L98
|
||||
},
|
||||
}
|
||||
|
||||
this.maxSockets = Math.max(64, this.config.deliverJobConcurrency ?? 128);
|
||||
|
||||
this.defaultFetcher = new UndiciFetcher(this.getStandardUndiciFetcherOption(), this.logger);
|
||||
|
||||
this.fetch = this.defaultFetcher.fetch;
|
||||
this.getHtml = this.defaultFetcher.getHtml;
|
||||
|
||||
this.defaultJsonFetcher = new UndiciFetcher(this.getStandardUndiciFetcherOption({
|
||||
maxResponseSize: 1024 * 256,
|
||||
}), this.logger);
|
||||
|
||||
this.getJson = this.defaultJsonFetcher.getJson;
|
||||
|
||||
//#region for old http/https, only used in S3Service
|
||||
this.http = new http.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
lookup: cache.lookup,
|
||||
lookup: this.dnsCache.lookup,
|
||||
} as http.AgentOptions);
|
||||
|
||||
this.https = new https.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
lookup: cache.lookup,
|
||||
lookup: this.dnsCache.lookup,
|
||||
} as https.AgentOptions);
|
||||
|
||||
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
||||
|
||||
|
||||
this.httpAgent = config.proxy
|
||||
? new HttpProxyAgent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
maxSockets,
|
||||
maxSockets: this.maxSockets,
|
||||
maxFreeSockets: 256,
|
||||
scheduling: 'lifo',
|
||||
proxy: config.proxy,
|
||||
@@ -72,21 +262,42 @@ export class HttpRequestService {
|
||||
? new HttpsProxyAgent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
maxSockets,
|
||||
maxSockets: this.maxSockets,
|
||||
maxFreeSockets: 256,
|
||||
scheduling: 'lifo',
|
||||
proxy: config.proxy,
|
||||
})
|
||||
: this.https;
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getStandardUndiciFetcherOption(opts: undici.Agent.Options = {}, proxyOpts: undici.Agent.Options = {}) {
|
||||
return {
|
||||
agentOptions: {
|
||||
...this.clientDefaults,
|
||||
...opts,
|
||||
},
|
||||
...(this.config.proxy ? {
|
||||
proxy: {
|
||||
uri: this.config.proxy,
|
||||
options: {
|
||||
connections: this.maxSockets,
|
||||
...proxyOpts,
|
||||
}
|
||||
}
|
||||
} : {}),
|
||||
userAgent: this.config.userAgent,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent by URL
|
||||
* Get http agent by URL
|
||||
* @param url URL
|
||||
* @param bypassProxy Allways bypass proxy
|
||||
*/
|
||||
@bindThis
|
||||
public getAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent {
|
||||
public getHttpAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent {
|
||||
if (bypassProxy || (this.config.proxyBypassHosts || []).includes(url.hostname)) {
|
||||
return url.protocol === 'http:' ? this.http : this.https;
|
||||
} else {
|
||||
@@ -94,67 +305,37 @@ export class HttpRequestService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check ip
|
||||
*/
|
||||
@bindThis
|
||||
public async getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record<string, string>): Promise<unknown> {
|
||||
const res = await this.getResponse({
|
||||
url,
|
||||
method: 'GET',
|
||||
headers: Object.assign({
|
||||
'User-Agent': this.config.userAgent,
|
||||
Accept: accept,
|
||||
}, headers ?? {}),
|
||||
timeout,
|
||||
size: 1024 * 256,
|
||||
});
|
||||
public getConnectorWithIpCheck(connector: undici.buildConnector.connector, checkIp: IpChecker): undici.buildConnector.connectorAsync {
|
||||
return (options, cb) => {
|
||||
connector(options, (err, socket) => {
|
||||
this.logger.debug('Socket connector (with ip checker) called', socket);
|
||||
if (err) {
|
||||
this.logger.error(`Socket error`, err)
|
||||
cb(new Error(`Error while socket connecting\n${err}`), null);
|
||||
return;
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
if (socket.remoteAddress == undefined) {
|
||||
this.logger.error(`Socket error: remoteAddress is undefined`);
|
||||
cb(new Error('remoteAddress is undefined (maybe socket destroyed)'), null);
|
||||
return;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: Record<string, string>): Promise<string> {
|
||||
const res = await this.getResponse({
|
||||
url,
|
||||
method: 'GET',
|
||||
headers: Object.assign({
|
||||
'User-Agent': this.config.userAgent,
|
||||
Accept: accept,
|
||||
}, headers ?? {}),
|
||||
timeout,
|
||||
});
|
||||
// allow
|
||||
if (checkIp(socket.remoteAddress)) {
|
||||
this.logger.debug(`Socket connected (ip ok): ${socket.localPort} => ${socket.remoteAddress}`);
|
||||
cb(null, socket);
|
||||
return;
|
||||
}
|
||||
|
||||
return await res.text();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getResponse(args: {
|
||||
url: string,
|
||||
method: string,
|
||||
body?: string,
|
||||
headers: Record<string, string>,
|
||||
timeout?: number,
|
||||
size?: number,
|
||||
}): Promise<Response> {
|
||||
const timeout = args.timeout ?? 10 * 1000;
|
||||
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => {
|
||||
controller.abort();
|
||||
}, timeout * 6);
|
||||
|
||||
const res = await fetch(args.url, {
|
||||
method: args.method,
|
||||
headers: args.headers,
|
||||
body: args.body,
|
||||
timeout,
|
||||
size: args.size ?? 10 * 1024 * 1024,
|
||||
agent: (url) => this.getAgentByUrl(url),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
|
||||
}
|
||||
|
||||
return res;
|
||||
this.logger.error('IP is not allowed', socket);
|
||||
cb(new StatusError('IP is not allowed', 403, 'IP is not allowed'), null);
|
||||
socket.destroy();
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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<Note> {
|
||||
@@ -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[]) {
|
||||
|
@@ -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.');
|
||||
}
|
||||
|
||||
|
283
packages/backend/src/core/RoleService.ts
Normal file
283
packages/backend/src/core/RoleService.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
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;
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class RoleService implements OnApplicationShutdown {
|
||||
private rolesCache: Cache<Role[]>;
|
||||
private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>;
|
||||
|
||||
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<Role[]>(Infinity);
|
||||
this.roleAssignmentByUserIdCache = new Cache<RoleAssignment[]>(Infinity);
|
||||
|
||||
this.redisSubscriber.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
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<RoleOptions> {
|
||||
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')),
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async isModerator(user: { id: User['id']; isRoot: User['isRoot'] } | null): Promise<boolean> {
|
||||
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<boolean> {
|
||||
if (user == null) return false;
|
||||
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
|
||||
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<User[]> {
|
||||
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<User['id'][]> {
|
||||
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<User[]> {
|
||||
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);
|
||||
}
|
||||
}
|
@@ -33,7 +33,7 @@ export class S3Service {
|
||||
? false
|
||||
: meta.objectStorageS3ForcePathStyle,
|
||||
httpOptions: {
|
||||
agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
|
||||
agent: this.httpRequestService.getHttpAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -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,
|
||||
}));
|
||||
|
@@ -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);
|
||||
|
@@ -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<void> {
|
||||
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),
|
||||
|
@@ -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(),
|
||||
|
@@ -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);
|
||||
|
@@ -30,7 +30,7 @@ export class WebfingerService {
|
||||
public async webfinger(query: string): Promise<IWebFinger> {
|
||||
const url = this.genUrl(query);
|
||||
|
||||
return await this.httpRequestService.getJson(url, 'application/jrd+json, application/json') as IWebFinger;
|
||||
return await this.httpRequestService.getJson<IWebFinger>(url, 'application/jrd+json, application/json');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@@ -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) {
|
||||
|
@@ -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,
|
||||
};
|
||||
}
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -5,8 +5,10 @@ import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.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;
|
||||
@@ -28,13 +30,21 @@ type PrivateKey = {
|
||||
|
||||
@Injectable()
|
||||
export class ApRequestService {
|
||||
private undiciFetcher: UndiciFetcher;
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
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
|
||||
@@ -148,16 +158,17 @@ export class ApRequestService {
|
||||
url,
|
||||
body,
|
||||
additionalHeaders: {
|
||||
'User-Agent': this.config.userAgent,
|
||||
},
|
||||
});
|
||||
|
||||
await this.httpRequestService.getResponse({
|
||||
await this.undiciFetcher.fetch(
|
||||
url,
|
||||
method: req.request.method,
|
||||
headers: req.request.headers,
|
||||
body,
|
||||
});
|
||||
{
|
||||
method: req.request.method,
|
||||
headers: req.request.headers,
|
||||
body,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,15 +187,16 @@ export class ApRequestService {
|
||||
},
|
||||
url,
|
||||
additionalHeaders: {
|
||||
'User-Agent': this.config.userAgent,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await this.httpRequestService.getResponse({
|
||||
const res = await this.httpRequestService.fetch(
|
||||
url,
|
||||
method: req.request.method,
|
||||
headers: req.request.headers,
|
||||
});
|
||||
{
|
||||
method: req.request.method,
|
||||
headers: req.request.headers,
|
||||
}
|
||||
);
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@@ -12,11 +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<string>;
|
||||
private user?: ILocalUser;
|
||||
private undiciFetcher: UndiciFetcher;
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
private config: Config,
|
||||
@@ -31,9 +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
|
||||
@@ -87,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');
|
||||
}
|
||||
|
||||
@@ -96,8 +105,8 @@ export class Resolver {
|
||||
}
|
||||
|
||||
const object = (this.user
|
||||
? await this.apRequestService.signedGet(value, this.user)
|
||||
: await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject;
|
||||
? await this.apRequestService.signedGet(value, this.user) as IObject
|
||||
: await this.undiciFetcher.getJson<IObject>(value, 'application/activity+json, application/ld+json'));
|
||||
|
||||
if (object == null || (
|
||||
Array.isArray(object['@context']) ?
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import * as crypto from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import fetch from 'node-fetch';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CONTEXTS } from './misc/contexts.js';
|
||||
@@ -116,14 +115,19 @@ class LdSignature {
|
||||
|
||||
@bindThis
|
||||
private async fetchDocument(url: string) {
|
||||
const json = await fetch(url, {
|
||||
headers: {
|
||||
Accept: 'application/ld+json, application/json',
|
||||
const json = await this.httpRequestService.fetch(
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/ld+json, application/json',
|
||||
},
|
||||
// TODO
|
||||
//timeout: this.loderTimeout,
|
||||
},
|
||||
// TODO
|
||||
//timeout: this.loderTimeout,
|
||||
agent: u => u.protocol === 'http:' ? this.httpRequestService.httpAgent : this.httpRequestService.httpsAgent,
|
||||
}).then(res => {
|
||||
{
|
||||
noOkError: true,
|
||||
}
|
||||
).then(res => {
|
||||
if (!res.ok) {
|
||||
throw `${res.status} ${res.statusText}`;
|
||||
} else {
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -61,21 +61,21 @@ export default class FederationChart extends Chart<typeof schema> {
|
||||
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<typeof schema> {
|
||||
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<typeof schema> {
|
||||
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()
|
||||
|
@@ -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,
|
||||
|
83
packages/backend/src/core/entities/RoleEntityService.ts
Normal file
83
packages/backend/src/core/entities/RoleEntityService.ts
Normal file
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
@@ -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<T extends { host: User['host'] }>(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<Instance | null>;
|
||||
|
||||
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<Instance | null>(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
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
],
|
||||
|
@@ -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<string, any>;
|
||||
}
|
||||
|
@@ -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[];
|
||||
|
||||
|
143
packages/backend/src/models/entities/Role.ts
Normal file
143
packages/backend/src/models/entities/Role.ts
Normal file
@@ -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<string, {
|
||||
useDefault: boolean;
|
||||
value: any;
|
||||
}>;
|
||||
}
|
42
packages/backend/src/models/entities/RoleAssignment.ts
Normal file
42
packages/backend/src/models/entities/RoleAssignment.ts
Normal file
@@ -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;
|
||||
}
|
@@ -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<User>) {
|
||||
if (data == null) return;
|
||||
|
||||
|
@@ -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<UserSecurityKey>;
|
||||
export type WebhooksRepository = Repository<Webhook>;
|
||||
export type ChannelsRepository = Repository<Channel>;
|
||||
export type RetentionAggregationsRepository = Repository<RetentionAggregation>;
|
||||
export type RolesRepository = Repository<Role>;
|
||||
export type RoleAssignmentsRepository = Repository<RoleAssignment>;
|
||||
export type FlashsRepository = Repository<Flash>;
|
||||
export type FlashLikesRepository = Repository<FlashLike>;
|
||||
|
@@ -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,
|
||||
|
@@ -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)';
|
||||
}
|
||||
|
||||
|
@@ -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}`);
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -33,24 +33,26 @@ export class WebhookDeliverProcessorService {
|
||||
try {
|
||||
this.logger.debug(`delivering ${job.data.webhookId}`);
|
||||
|
||||
const res = await this.httpRequestService.getResponse({
|
||||
url: job.data.to,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': 'Misskey-Hooks',
|
||||
'X-Misskey-Host': this.config.host,
|
||||
'X-Misskey-Hook-Id': job.data.webhookId,
|
||||
'X-Misskey-Hook-Secret': job.data.secret,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
hookId: job.data.webhookId,
|
||||
userId: job.data.userId,
|
||||
eventId: job.data.eventId,
|
||||
createdAt: job.data.createdAt,
|
||||
type: job.data.type,
|
||||
body: job.data.content,
|
||||
}),
|
||||
});
|
||||
const res = await this.httpRequestService.fetch(
|
||||
job.data.to,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': 'Misskey-Hooks',
|
||||
'X-Misskey-Host': this.config.host,
|
||||
'X-Misskey-Hook-Id': job.data.webhookId,
|
||||
'X-Misskey-Hook-Secret': job.data.secret,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
hookId: job.data.webhookId,
|
||||
userId: job.data.userId,
|
||||
eventId: job.data.eventId,
|
||||
createdAt: job.data.createdAt,
|
||||
type: job.data.type,
|
||||
body: job.data.content,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
this.webhooksRepository.update({ id: job.data.webhookId }, {
|
||||
latestSentAt: new Date(),
|
||||
|
@@ -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,
|
||||
|
@@ -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<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
||||
) {
|
||||
const isSecure = user != null && token == null;
|
||||
const isModerator = user != null && (user.isModerator || user.isAdmin);
|
||||
|
||||
if (ep.meta.secure && !isSecure) {
|
||||
throw new ApiError(accessDenied);
|
||||
@@ -234,30 +235,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)) {
|
||||
|
@@ -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);
|
||||
|
@@ -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';
|
||||
@@ -326,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';
|
||||
@@ -368,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 };
|
||||
@@ -386,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 };
|
||||
@@ -656,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 };
|
||||
|
||||
@@ -703,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,
|
||||
@@ -721,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,
|
||||
@@ -991,7 +1001,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$users_search,
|
||||
$users_show,
|
||||
$users_stats,
|
||||
$admin_driveCapOverride,
|
||||
$fetchRss,
|
||||
$retention,
|
||||
],
|
||||
@@ -1032,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,
|
||||
@@ -1050,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,
|
||||
@@ -1318,7 +1331,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$users_search,
|
||||
$users_show,
|
||||
$users_stats,
|
||||
$admin_driveCapOverride,
|
||||
$fetchRss,
|
||||
$retention,
|
||||
],
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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';
|
||||
@@ -325,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 = [
|
||||
@@ -365,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],
|
||||
@@ -383,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],
|
||||
@@ -653,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],
|
||||
];
|
||||
@@ -680,14 +686,16 @@ export interface IEndpointMeta {
|
||||
readonly requireCredential?: boolean;
|
||||
|
||||
/**
|
||||
* 管理者のみ使えるエンドポイントか否か
|
||||
* isModeratorなロールを必要とするか
|
||||
*/
|
||||
readonly requireModerator?: boolean;
|
||||
|
||||
/**
|
||||
* isAdministratorなロールを必要とするか
|
||||
*/
|
||||
readonly requireAdmin?: boolean;
|
||||
|
||||
/**
|
||||
* 管理者またはモデレーターのみ使えるエンドポイントか否か
|
||||
*/
|
||||
readonly requireModerator?: boolean;
|
||||
readonly requireRoleOption?: string;
|
||||
|
||||
/**
|
||||
* エンドポイントのリミテーションに関するやつ
|
||||
|
@@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
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,
|
||||
|
@@ -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<typeof meta, typeof paramDef> {
|
||||
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)) {
|
||||
|
@@ -38,7 +38,7 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
@Inject(DI.adsRepository)
|
||||
private adsRepository: AdsRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
|
@@ -8,7 +8,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user