Compare commits
77 Commits
13.0.0-bet
...
13.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 | ||
![]() |
1688083e9a | ||
![]() |
616594d3cd | ||
![]() |
6783178dc3 | ||
![]() |
3f033d6ab7 | ||
![]() |
d10e000883 | ||
![]() |
ce528ff22e | ||
![]() |
5e4e02235a | ||
![]() |
e4179336e4 | ||
![]() |
7823ba494f | ||
![]() |
7bdff90415 | ||
![]() |
f3c0af7e23 | ||
![]() |
72dfbfcf35 | ||
![]() |
9cbe878d0b | ||
![]() |
618405c4d3 | ||
![]() |
0b08fcac4a | ||
![]() |
eac6ebb239 | ||
![]() |
194fb14e07 | ||
![]() |
c2d05b507a | ||
![]() |
4df43a9107 | ||
![]() |
0da7fcdbed | ||
![]() |
1e50b2688a | ||
![]() |
c1cd018626 | ||
![]() |
b588e8b60b | ||
![]() |
06f55ffb37 | ||
![]() |
02df6a28cd | ||
![]() |
d64abedf9f | ||
![]() |
4d39d1caf6 | ||
![]() |
d06f61f23f | ||
![]() |
c179d6f735 | ||
![]() |
3bc0cdbfb7 | ||
![]() |
b04155e7ba |
@@ -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
|
||||
|
29
CHANGELOG.md
29
CHANGELOG.md
@@ -12,7 +12,7 @@ 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
|
||||
@@ -27,10 +27,17 @@ 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にアップデートされました
|
||||
@@ -51,6 +58,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
|
||||
@@ -60,13 +68,16 @@ You should also include the user name that made the change.
|
||||
- Add Cloudflare Turnstile CAPTCHA support @CyberRex0
|
||||
- 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: improve note scoring for featured notes @CyberRex0
|
||||
- Server: アンケート選択肢の文字数制限を緩和 @syuilo
|
||||
- Server: improve stats api performance @syuilo
|
||||
- Server: improve nodeinfo performance @syuilo
|
||||
- Server: delete outdated notifications regularly to improve db performance @syuilo
|
||||
- Server: delete outdated hard-mutes regularly to improve db performance @syuilo
|
||||
- Server: delete outdated notes of antenna regularly to improve db performance @syuilo
|
||||
- Server: improve activitypub deliver performance @syuilo
|
||||
- Client: use tabler-icons instead of fontawesome to better design @syuilo
|
||||
- Client: Add AiScript App widget
|
||||
- Client: Add new gabber kick sounds (thanks for noizenecio)
|
||||
- Client: Add link to user RSS feed in profile menu @ssmucny
|
||||
- Client: Compress non-animated PNG files @saschanaz
|
||||
@@ -74,15 +85,18 @@ You should also include the user name that made the change.
|
||||
- Client: enhance dashboard of control panel @syuilo
|
||||
- Client: Vite is upgraded to v4 @syuilo, @tamaina
|
||||
- Client: HMR is available while yarn dev @tamaina
|
||||
- Client: Make widgets of universal/classic sync between devices @tamaina
|
||||
- Client: Implement the button to subscribe push notification @tamaina
|
||||
- Client: Implement the toggle to or not to close push notifications when notifications or messages are read @tamaina
|
||||
- Client: Improve RSS widget @tamaina
|
||||
- Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz
|
||||
- Client: OpenSearch support @SoniEx2 @chaoticryptidz
|
||||
- Client: Support remote objects in search @SoniEx2
|
||||
- Client: user activity page @syuilo
|
||||
- Client: Make widgets of universal/classic sync between devices @tamaina
|
||||
- Client: add user list widget @syuilo
|
||||
- Client: Add AiScript App widget
|
||||
- Client: add profile widget @syuilo
|
||||
- Client: add instance info widget @syuilo
|
||||
- Client: Improve RSS widget @tamaina
|
||||
- Client: add heatmap of daily active users to about page @syuilo
|
||||
- Client: introduce fluent emoji @syuilo
|
||||
- Client: add new theme @syuilo
|
||||
@@ -105,8 +119,10 @@ You should also include the user name that made the change.
|
||||
- Server: 特定のPNG画像のアップロードに失敗する問題を修正 @usbharu
|
||||
- Server: 非公開のクリップのURLでOGPレンダリングされる問題を修正 @syuilo
|
||||
- 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
|
||||
@@ -115,6 +131,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
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM node:18.12.1-bullseye AS builder
|
||||
FROM node:18.13.0-bullseye AS builder
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
@@ -22,7 +22,7 @@ COPY . ./
|
||||
RUN git submodule update --init
|
||||
RUN yarn build
|
||||
|
||||
FROM node:18.12.1-bullseye-slim AS runner
|
||||
FROM node:18.13.0-bullseye-slim AS runner
|
||||
|
||||
WORKDIR /misskey
|
||||
|
||||
|
@@ -1117,6 +1117,8 @@ _weekday:
|
||||
friday: "الجمعة"
|
||||
saturday: "السبت"
|
||||
_widgets:
|
||||
profile: "الملف التعريفي"
|
||||
instanceInfo: "معلومات مثيل الخادم"
|
||||
memo: "ملاحظة لاصقة"
|
||||
notifications: "الإشعارات"
|
||||
timeline: "الخيط الزمني"
|
||||
|
@@ -1200,6 +1200,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"
|
||||
|
@@ -694,6 +694,8 @@ _weekday:
|
||||
friday: "Pátek"
|
||||
saturday: "Sobota"
|
||||
_widgets:
|
||||
profile: "Váš profil"
|
||||
instanceInfo: "Informace o instanci"
|
||||
notifications: "Oznámení"
|
||||
timeline: "Časová osa"
|
||||
calendar: "Kalendář"
|
||||
|
@@ -1302,6 +1302,8 @@ _weekday:
|
||||
friday: "Freitag"
|
||||
saturday: "Samstag"
|
||||
_widgets:
|
||||
profile: "Profil"
|
||||
instanceInfo: "Instanzinformationen"
|
||||
memo: "Merkzettel"
|
||||
notifications: "Benachrichtigungen"
|
||||
timeline: "Chronik"
|
||||
|
@@ -343,6 +343,8 @@ _antennaSources:
|
||||
userList: "Σημειώματα από καθορισμένη λίστα μελών"
|
||||
userGroup: "Σημειώματα από μέλη καθορισμένης ομάδας"
|
||||
_widgets:
|
||||
profile: "Προφίλ"
|
||||
instanceInfo: "Πληροφορίες του instance"
|
||||
notifications: "Ειδοποιήσεις"
|
||||
timeline: "Χρονολόγιο"
|
||||
calendar: "Ημερολόγιο"
|
||||
|
@@ -1302,6 +1302,8 @@ _weekday:
|
||||
friday: "Friday"
|
||||
saturday: "Saturday"
|
||||
_widgets:
|
||||
profile: "Profile"
|
||||
instanceInfo: "Instance Information"
|
||||
memo: "Sticky notes"
|
||||
notifications: "Notifications"
|
||||
timeline: "Timeline"
|
||||
|
@@ -1296,6 +1296,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"
|
||||
|
@@ -1289,6 +1289,8 @@ _weekday:
|
||||
friday: "Vendredi"
|
||||
saturday: "Samedi"
|
||||
_widgets:
|
||||
profile: "Profil"
|
||||
instanceInfo: "Informations sur l’instance"
|
||||
memo: "Note collante"
|
||||
notifications: "Notifications"
|
||||
timeline: "Fil"
|
||||
|
@@ -1206,6 +1206,8 @@ _weekday:
|
||||
friday: "Jumat"
|
||||
saturday: "Sabtu"
|
||||
_widgets:
|
||||
profile: "Profil"
|
||||
instanceInfo: "Informasi Instansi"
|
||||
memo: "Catatan memo"
|
||||
notifications: "Pemberitahuan"
|
||||
timeline: "Linimasa"
|
||||
|
@@ -1296,6 +1296,8 @@ _weekday:
|
||||
friday: "Venerdì"
|
||||
saturday: "Sabato"
|
||||
_widgets:
|
||||
profile: "Profilo"
|
||||
instanceInfo: "Informazioni sull'istanza"
|
||||
memo: "Promemoria"
|
||||
notifications: "Notifiche"
|
||||
timeline: "Timeline"
|
||||
|
@@ -924,6 +924,49 @@ neverShow: "今後表示しない"
|
||||
remindMeLater: "また後で"
|
||||
didYouLikeMisskey: "Misskeyを気に入っていただけましたか?"
|
||||
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!"
|
||||
roles: "ロール"
|
||||
role: "ロール"
|
||||
normalUser: "一般ユーザー"
|
||||
undefined: "未定義"
|
||||
assign: "アサイン"
|
||||
unassign: "アサインを解除"
|
||||
color: "色"
|
||||
|
||||
_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: "パブリック投稿の許可"
|
||||
driveCapacity: "ドライブ容量"
|
||||
antennaMax: "アンテナの作成可能数"
|
||||
_condition:
|
||||
isLocal: "ローカルユーザー"
|
||||
isRemote: "リモートユーザー"
|
||||
createdLessThan: "アカウント作成から~以内"
|
||||
createdMoreThan: "アカウント作成から~経過"
|
||||
and: "~かつ~"
|
||||
or: "~または~"
|
||||
not: "~ではない"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
||||
@@ -1335,6 +1378,8 @@ _weekday:
|
||||
saturday: "土曜日"
|
||||
|
||||
_widgets:
|
||||
profile: "プロフィール"
|
||||
instanceInfo: "インスタンス情報"
|
||||
memo: "付箋"
|
||||
notifications: "通知"
|
||||
timeline: "タイムライン"
|
||||
|
@@ -1295,6 +1295,8 @@ _weekday:
|
||||
friday: "金曜日"
|
||||
saturday: "土曜日"
|
||||
_widgets:
|
||||
profile: "プロフィール"
|
||||
instanceInfo: "インスタンス情報"
|
||||
memo: "付箋"
|
||||
notifications: "通知"
|
||||
timeline: "タイムライン"
|
||||
|
@@ -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:
|
||||
|
@@ -1302,6 +1302,8 @@ _weekday:
|
||||
friday: "금요일"
|
||||
saturday: "토요일"
|
||||
_widgets:
|
||||
profile: "프로필"
|
||||
instanceInfo: "인스턴스 정보"
|
||||
memo: "스티커 메모"
|
||||
notifications: "알림"
|
||||
timeline: "타임라인"
|
||||
|
@@ -440,6 +440,8 @@ _sfx:
|
||||
notification: "Meldingen"
|
||||
chat: "Chat"
|
||||
_widgets:
|
||||
profile: "Profiel"
|
||||
instanceInfo: "Serverinformatie"
|
||||
notifications: "Meldingen"
|
||||
timeline: "Tijdlijn"
|
||||
activity: "Activiteit"
|
||||
|
@@ -1213,6 +1213,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"
|
||||
|
@@ -1213,6 +1213,8 @@ _weekday:
|
||||
friday: "Пятница"
|
||||
saturday: "Суббота"
|
||||
_widgets:
|
||||
profile: "Профиль"
|
||||
instanceInfo: "Информация об инстансе"
|
||||
memo: "Напоминания"
|
||||
notifications: "Уведомления"
|
||||
timeline: "Лента"
|
||||
|
@@ -1295,6 +1295,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"
|
||||
|
@@ -1302,6 +1302,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:
|
||||
|
@@ -1229,6 +1229,8 @@ _weekday:
|
||||
friday: "П'ятниця"
|
||||
saturday: "Субота"
|
||||
_widgets:
|
||||
profile: "Профіль"
|
||||
instanceInfo: "Про цей інстанс"
|
||||
memo: "Нагадування"
|
||||
notifications: "Сповіщення"
|
||||
timeline: "Стрічка"
|
||||
|
@@ -1271,6 +1271,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"
|
||||
|
@@ -922,7 +922,8 @@ numberOfLikes: "点赞数"
|
||||
show: "显示"
|
||||
neverShow: "不再显示"
|
||||
remindMeLater: "稍后提醒我"
|
||||
didYouLikeMisskey: "你在Misskey玩得还开心吗?"
|
||||
didYouLikeMisskey: "您喜欢Misskey吗?"
|
||||
pleaseDonate: "Misskey是{host}所使用的免费软件。为了今后也能够维持Misskey的开发,请在有余力的情况下进行捐助!"
|
||||
_sensitiveMediaDetection:
|
||||
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
|
||||
sensitivity: "检测敏感度"
|
||||
@@ -1301,6 +1302,8 @@ _weekday:
|
||||
friday: "星期五"
|
||||
saturday: "星期六"
|
||||
_widgets:
|
||||
profile: "个人资料"
|
||||
instanceInfo: "实例信息"
|
||||
memo: "便签"
|
||||
notifications: "通知"
|
||||
timeline: "时间线"
|
||||
@@ -1327,6 +1330,7 @@ _widgets:
|
||||
userList: "用户列表"
|
||||
_userList:
|
||||
chooseList: "选择列表"
|
||||
clicker: "点击器"
|
||||
_cw:
|
||||
hide: "隐藏"
|
||||
show: "查看更多"
|
||||
|
@@ -797,7 +797,7 @@ squareAvatars: "頭像以方形顯示"
|
||||
sent: "發送"
|
||||
received: "收取"
|
||||
searchResult: "搜尋結果"
|
||||
hashtags: "#tag"
|
||||
hashtags: "標籤"
|
||||
troubleshooting: "故障排除"
|
||||
useBlurEffect: "在 UI 上使用模糊效果"
|
||||
learnMore: "更多資訊"
|
||||
@@ -1159,7 +1159,7 @@ _theme:
|
||||
navActive: "側邊欄文本 (活動)"
|
||||
navIndicator: "側邊欄指示符"
|
||||
link: "鏈接"
|
||||
hashtag: "#tag"
|
||||
hashtag: "標籤"
|
||||
mention: "提到"
|
||||
mentionMe: "提到了我"
|
||||
renote: "轉發貼文"
|
||||
@@ -1302,6 +1302,8 @@ _weekday:
|
||||
friday: "週五"
|
||||
saturday: "週六"
|
||||
_widgets:
|
||||
profile: "個人檔案"
|
||||
instanceInfo: "實例資訊"
|
||||
memo: "備忘錄"
|
||||
notifications: "通知"
|
||||
timeline: "時間軸"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "13.0.0-beta.36",
|
||||
"version": "13.0.0-beta.41",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
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'`);
|
||||
}
|
||||
}
|
@@ -77,7 +77,6 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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[]) {
|
||||
|
243
packages/backend/src/core/RoleService.ts
Normal file
243
packages/backend/src/core/RoleService.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
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 { RoleCondFormulaValue } from '@/models/entities/Role.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
export type RoleOptions = {
|
||||
gtlAvailable: boolean;
|
||||
ltlAvailable: boolean;
|
||||
canPublicNote: boolean;
|
||||
driveCapacityMb: number;
|
||||
antennaLimit: number;
|
||||
};
|
||||
|
||||
export const DEFAULT_ROLE: RoleOptions = {
|
||||
gtlAvailable: true,
|
||||
ltlAvailable: true,
|
||||
canPublicNote: true,
|
||||
driveCapacityMb: 100,
|
||||
antennaLimit: 5,
|
||||
};
|
||||
|
||||
@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;
|
||||
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));
|
||||
}
|
||||
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),
|
||||
driveCapacityMb: Math.max(...getOptionValues('driveCapacityMb')),
|
||||
antennaLimit: Math.max(...getOptionValues('antennaLimit')),
|
||||
};
|
||||
}
|
||||
|
||||
@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,11 @@ 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 type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class UserCacheService implements OnApplicationShutdown {
|
||||
@@ -42,8 +42,6 @@ export class UserCacheService implements OnApplicationShutdown {
|
||||
const { type, body } = obj.message;
|
||||
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);
|
||||
@@ -70,6 +68,11 @@ export class UserCacheService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
};
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -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 {
|
||||
|
82
packages/backend/src/core/entities/RoleEntityService.ts
Normal file
82
packages/backend/src/core/entities/RoleEntityService.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
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,
|
||||
...(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,13 @@ export class UserEntityService implements OnModuleInit {
|
||||
userId: user.id,
|
||||
}).then(result => result >= 1)
|
||||
: false,
|
||||
...(isMe || opts.includeSecrets ? {
|
||||
driveCapacityOverrideMb: user.driveCapacityOverrideMb,
|
||||
} : {}),
|
||||
} : {}),
|
||||
|
||||
...(opts.detail && isMe ? {
|
||||
avatarId: user.avatarId,
|
||||
bannerId: user.bannerId,
|
||||
isModerator: isModerator,
|
||||
isAdmin: isAdmin,
|
||||
injectFeaturedNote: profile!.injectFeaturedNote,
|
||||
receiveAnnouncementEmail: profile!.receiveAnnouncementEmail,
|
||||
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
|
||||
@@ -484,6 +488,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[];
|
||||
|
||||
|
119
packages/backend/src/models/entities/Role.ts
Normal file
119
packages/backend/src/models/entities/Role.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
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;
|
||||
};
|
||||
|
||||
export type RoleCondFormulaValue =
|
||||
CondFormulaValueAnd |
|
||||
CondFormulaValueOr |
|
||||
CondFormulaValueNot |
|
||||
CondFormulaValueIsLocal |
|
||||
CondFormulaValueIsRemote |
|
||||
CondFormulaValueCreatedLessThan |
|
||||
CondFormulaValueCreatedMoreThan;
|
||||
|
||||
@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,
|
||||
|
@@ -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,40 @@ 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.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.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 (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) {
|
||||
|
@@ -38,8 +38,6 @@ 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___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';
|
||||
@@ -369,8 +372,6 @@ const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', us
|
||||
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 $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 };
|
||||
|
||||
@@ -704,8 +710,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$admin_getTableStats,
|
||||
$admin_getUserIps,
|
||||
$admin_invite,
|
||||
$admin_moderators_add,
|
||||
$admin_moderators_remove,
|
||||
$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,
|
||||
],
|
||||
@@ -1033,8 +1042,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$admin_getTableStats,
|
||||
$admin_getUserIps,
|
||||
$admin_invite,
|
||||
$admin_moderators_add,
|
||||
$admin_moderators_remove,
|
||||
$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,
|
||||
],
|
||||
|
@@ -37,8 +37,6 @@ 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___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 = [
|
||||
@@ -366,8 +369,6 @@ const eps = [
|
||||
['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],
|
||||
['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,15 +686,15 @@ export interface IEndpointMeta {
|
||||
readonly requireCredential?: boolean;
|
||||
|
||||
/**
|
||||
* 管理者のみ使えるエンドポイントか否か
|
||||
*/
|
||||
readonly requireAdmin?: boolean;
|
||||
|
||||
/**
|
||||
* 管理者またはモデレーターのみ使えるエンドポイントか否か
|
||||
* isModeratorなロールを必要とするか
|
||||
*/
|
||||
readonly requireModerator?: boolean;
|
||||
|
||||
/**
|
||||
* isAdministratorなロールを必要とするか
|
||||
*/
|
||||
readonly requireAdmin?: boolean;
|
||||
|
||||
/**
|
||||
* エンドポイントのリミテーションに関するやつ
|
||||
* 省略した場合はリミテーションは無いものとして解釈されます。
|
||||
|
@@ -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)) {
|
||||
|
@@ -8,7 +8,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@@ -1,61 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
overrideMb: { type: 'number', nullable: true },
|
||||
},
|
||||
required: ['userId', 'overrideMb'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
|
||||
if (user == null) {
|
||||
throw new Error('user not found');
|
||||
}
|
||||
|
||||
if (!this.userEntityService.isLocalUser(user)) {
|
||||
throw new Error('user is not local user');
|
||||
}
|
||||
|
||||
/*if (user.isAdmin) {
|
||||
throw new Error('cannot suspend admin');
|
||||
}
|
||||
if (user.isModerator) {
|
||||
throw new Error('cannot suspend moderator');
|
||||
}*/
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
driveCapacityOverrideMb: ps.overrideMb,
|
||||
});
|
||||
|
||||
this.moderationLogService.insertModerationLog(me, 'change-drive-capacity-override', {
|
||||
targetId: user.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { DriveFilesRepository } from '@/models/index.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -159,6 +160,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const file = ps.fileId ? await this.driveFilesRepository.findOneBy({ id: ps.fileId }) : await this.driveFilesRepository.findOne({
|
||||
@@ -175,6 +178,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
|
||||
const isModerator = await this.roleService.isModerator(me);
|
||||
|
||||
return {
|
||||
id: file.id,
|
||||
userId: file.userId,
|
||||
@@ -202,8 +207,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
name: file.name,
|
||||
md5: file.md5,
|
||||
createdAt: file.createdAt.toISOString(),
|
||||
requestIp: me.isAdmin ? file.requestIp : null,
|
||||
requestHeaders: me.isAdmin ? file.requestHeaders : null,
|
||||
requestIp: isModerator ? file.requestIp : null,
|
||||
requestHeaders: isModerator ? file.requestHeaders : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
|
||||
tags: ['admin'],
|
||||
} as const;
|
||||
|
@@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
|
||||
tags: ['admin'],
|
||||
|
||||
|
@@ -7,7 +7,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
requireModerator: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@@ -4,6 +4,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { DEFAULT_ROLE } from '@/core/RoleService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
@@ -15,14 +16,6 @@ export const meta = {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
driveCapacityPerLocalUserMb: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
driveCapacityPerRemoteUserMb: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
cacheRemoteFiles: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
@@ -377,10 +370,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
repositoryUrl: instance.repositoryUrl,
|
||||
feedbackUrl: instance.feedbackUrl,
|
||||
disableRegistration: instance.disableRegistration,
|
||||
disableLocalTimeline: instance.disableLocalTimeline,
|
||||
disableGlobalTimeline: instance.disableGlobalTimeline,
|
||||
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
||||
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
enableHcaptcha: instance.enableHcaptcha,
|
||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||
@@ -451,6 +440,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
deeplIsPro: instance.deeplIsPro,
|
||||
enableIpLogging: instance.enableIpLogging,
|
||||
enableActiveEmailValidation: instance.enableActiveEmailValidation,
|
||||
baseRole: { ...DEFAULT_ROLE, ...instance.defaultRoleOverride },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@@ -1,49 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
|
||||
if (user == null) {
|
||||
throw new Error('user not found');
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
throw new Error('cannot mark as moderator if admin user');
|
||||
}
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
isModerator: true,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: true });
|
||||
});
|
||||
}
|
||||
}
|
@@ -50,8 +50,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new Error('user not found');
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
throw new Error('cannot reset password of admin');
|
||||
if (user.isRoot) {
|
||||
throw new Error('cannot reset password of root');
|
||||
}
|
||||
|
||||
const passwd = rndstr('a-zA-Z0-9', 8);
|
||||
|
@@ -0,0 +1,96 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin', 'role'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
errors: {
|
||||
noSuchRole: {
|
||||
message: 'No such role.',
|
||||
code: 'NO_SUCH_ROLE',
|
||||
id: '6503c040-6af4-4ed9-bf07-f2dd16678eab',
|
||||
},
|
||||
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '558ea170-f653-4700-94d0-5a818371d0df',
|
||||
},
|
||||
|
||||
accessDenied: {
|
||||
message: 'Only administrators can edit members of the role.',
|
||||
code: 'ACCESS_DENIED',
|
||||
id: '25b5bc31-dc79-4ebd-9bd2-c84978fd052c',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roleId: { type: 'string', format: 'misskey:id' },
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [
|
||||
'roleId',
|
||||
'userId',
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.rolesRepository)
|
||||
private rolesRepository: RolesRepository,
|
||||
|
||||
@Inject(DI.roleAssignmentsRepository)
|
||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
private roleService: RoleService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
||||
if (role == null) {
|
||||
throw new ApiError(meta.errors.noSuchRole);
|
||||
}
|
||||
|
||||
if (!role.canEditMembersByModerator && !(await this.roleService.isAdministrator(me))) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
if (user == null) {
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
|
||||
const date = new Date();
|
||||
const created = await this.roleAssignmentsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: date,
|
||||
roleId: role.id,
|
||||
userId: user.id,
|
||||
}).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
this.rolesRepository.update(ps.roleId, {
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RolesRepository } from '@/models/index.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin', 'role'],
|
||||
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
color: { type: 'string', nullable: true },
|
||||
target: { type: 'string' },
|
||||
condFormula: { type: 'object' },
|
||||
isPublic: { type: 'boolean' },
|
||||
isModerator: { type: 'boolean' },
|
||||
isAdministrator: { type: 'boolean' },
|
||||
canEditMembersByModerator: { type: 'boolean' },
|
||||
options: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'name',
|
||||
'description',
|
||||
'color',
|
||||
'target',
|
||||
'condFormula',
|
||||
'isPublic',
|
||||
'isModerator',
|
||||
'isAdministrator',
|
||||
'canEditMembersByModerator',
|
||||
'options',
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.rolesRepository)
|
||||
private rolesRepository: RolesRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
private idService: IdService,
|
||||
private roleEntityService: RoleEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const date = new Date();
|
||||
const created = await this.rolesRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: date,
|
||||
updatedAt: date,
|
||||
lastUsedAt: date,
|
||||
name: ps.name,
|
||||
description: ps.description,
|
||||
color: ps.color,
|
||||
target: ps.target,
|
||||
condFormula: ps.condFormula,
|
||||
isPublic: ps.isPublic,
|
||||
isAdministrator: ps.isAdministrator,
|
||||
isModerator: ps.isModerator,
|
||||
canEditMembersByModerator: ps.canEditMembersByModerator,
|
||||
options: ps.options,
|
||||
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
this.globalEventService.publishInternalEvent('roleCreated', created);
|
||||
|
||||
return await this.roleEntityService.pack(created, me);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RolesRepository } from '@/models/index.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin', 'role'],
|
||||
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
|
||||
errors: {
|
||||
noSuchRole: {
|
||||
message: 'No such role.',
|
||||
code: 'NO_SUCH_ROLE',
|
||||
id: 'de0d6ecd-8e0a-4253-88ff-74bc89ae3d45',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roleId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [
|
||||
'roleId',
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.rolesRepository)
|
||||
private rolesRepository: RolesRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
||||
if (role == null) {
|
||||
throw new ApiError(meta.errors.noSuchRole);
|
||||
}
|
||||
await this.rolesRepository.delete({
|
||||
id: ps.roleId,
|
||||
});
|
||||
this.globalEventService.publishInternalEvent('roleDeleted', role);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RolesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin', 'role'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
},
|
||||
required: [
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.rolesRepository)
|
||||
private rolesRepository: RolesRepository,
|
||||
|
||||
private roleEntityService: RoleEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const roles = await this.rolesRepository.find({
|
||||
order: { lastUsedAt: 'DESC' },
|
||||
});
|
||||
return await this.roleEntityService.packMany(roles, me, { detail: false });
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RolesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin', 'role'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
errors: {
|
||||
noSuchRole: {
|
||||
message: 'No such role.',
|
||||
code: 'NO_SUCH_ROLE',
|
||||
id: '07dc7d34-c0d8-49b7-96c6-db3ce64ee0b3',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roleId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [
|
||||
'roleId',
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.rolesRepository)
|
||||
private rolesRepository: RolesRepository,
|
||||
|
||||
private roleEntityService: RoleEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
||||
if (role == null) {
|
||||
throw new ApiError(meta.errors.noSuchRole);
|
||||
}
|
||||
return await this.roleEntityService.pack(role);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,101 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin', 'role'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
errors: {
|
||||
noSuchRole: {
|
||||
message: 'No such role.',
|
||||
code: 'NO_SUCH_ROLE',
|
||||
id: '6e519036-a70d-4c76-b679-bc8fb18194e2',
|
||||
},
|
||||
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '2b730f78-1179-461b-88ad-d24c9af1a5ce',
|
||||
},
|
||||
|
||||
notAssigned: {
|
||||
message: 'Not assigned.',
|
||||
code: 'NOT_ASSIGNED',
|
||||
id: 'b9060ac7-5c94-4da4-9f55-2047c953df44',
|
||||
},
|
||||
|
||||
accessDenied: {
|
||||
message: 'Only administrators can edit members of the role.',
|
||||
code: 'ACCESS_DENIED',
|
||||
id: '24636eee-e8c1-493e-94b2-e16ad401e262',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roleId: { type: 'string', format: 'misskey:id' },
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [
|
||||
'roleId',
|
||||
'userId',
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.rolesRepository)
|
||||
private rolesRepository: RolesRepository,
|
||||
|
||||
@Inject(DI.roleAssignmentsRepository)
|
||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
private roleService: RoleService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
||||
if (role == null) {
|
||||
throw new ApiError(meta.errors.noSuchRole);
|
||||
}
|
||||
|
||||
if (!role.canEditMembersByModerator && !(await this.roleService.isAdministrator(me))) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
if (user == null) {
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
|
||||
const roleAssignment = await this.roleAssignmentsRepository.findOneBy({ userId: user.id, roleId: role.id });
|
||||
if (roleAssignment == null) {
|
||||
throw new ApiError(meta.errors.notAssigned);
|
||||
}
|
||||
|
||||
await this.roleAssignmentsRepository.delete(roleAssignment.id);
|
||||
|
||||
this.rolesRepository.update(ps.roleId, {
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('userRoleUnassigned', roleAssignment);
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,11 +1,13 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import type { RolesRepository } from '@/models/index.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
tags: ['admin', 'role'],
|
||||
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
@@ -14,32 +16,27 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
options: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
required: ['userId'],
|
||||
required: [
|
||||
'options',
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
|
||||
if (user == null) {
|
||||
throw new Error('user not found');
|
||||
}
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
isModerator: false,
|
||||
await this.metaService.update({
|
||||
defaultRoleOverride: ps.options,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: false });
|
||||
this.globalEventService.publishInternalEvent('defaultRoleOverrideUpdated', ps.options);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RolesRepository } from '@/models/index.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin', 'role'],
|
||||
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
|
||||
errors: {
|
||||
noSuchRole: {
|
||||
message: 'No such role.',
|
||||
code: 'NO_SUCH_ROLE',
|
||||
id: 'cd23ef55-09ad-428a-ac61-95a45e124b32',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roleId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
color: { type: 'string', nullable: true },
|
||||
target: { type: 'string' },
|
||||
condFormula: { type: 'object' },
|
||||
isPublic: { type: 'boolean' },
|
||||
isModerator: { type: 'boolean' },
|
||||
isAdministrator: { type: 'boolean' },
|
||||
canEditMembersByModerator: { type: 'boolean' },
|
||||
options: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'roleId',
|
||||
'name',
|
||||
'description',
|
||||
'color',
|
||||
'target',
|
||||
'condFormula',
|
||||
'isPublic',
|
||||
'isModerator',
|
||||
'isAdministrator',
|
||||
'canEditMembersByModerator',
|
||||
'options',
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.rolesRepository)
|
||||
private rolesRepository: RolesRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
||||
if (role == null) {
|
||||
throw new ApiError(meta.errors.noSuchRole);
|
||||
}
|
||||
|
||||
const date = new Date();
|
||||
await this.rolesRepository.update(ps.roleId, {
|
||||
updatedAt: date,
|
||||
name: ps.name,
|
||||
description: ps.description,
|
||||
color: ps.color,
|
||||
target: ps.target,
|
||||
condFormula: ps.condFormula,
|
||||
isPublic: ps.isPublic,
|
||||
isModerator: ps.isModerator,
|
||||
isAdministrator: ps.isAdministrator,
|
||||
canEditMembersByModerator: ps.canEditMembersByModerator,
|
||||
options: ps.options,
|
||||
});
|
||||
const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId });
|
||||
this.globalEventService.publishInternalEvent('roleUpdated', updated);
|
||||
});
|
||||
}
|
||||
}
|
@@ -2,6 +2,8 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository, SigninsRepository, UserProfilesRepository } from '@/models/index.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -35,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
@Inject(DI.signinsRepository)
|
||||
private signinsRepository: SigninsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private roleEntityService: RoleEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const [user, profile] = await Promise.all([
|
||||
@@ -46,15 +51,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new Error('user not found');
|
||||
}
|
||||
|
||||
const isModerator = await this.roleService.isModerator(user);
|
||||
const isSilenced = !(await this.roleService.getUserRoleOptions(user.id)).canPublicNote;
|
||||
|
||||
const _me = await this.usersRepository.findOneByOrFail({ id: me.id });
|
||||
if ((_me.isModerator && !_me.isAdmin) && user.isAdmin) {
|
||||
if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) {
|
||||
throw new Error('cannot show info of admin');
|
||||
}
|
||||
|
||||
if (!_me.isAdmin) {
|
||||
if (!await this.roleService.isAdministrator(_me)) {
|
||||
return {
|
||||
isModerator: user.isModerator,
|
||||
isSilenced: user.isSilenced,
|
||||
isSuspended: user.isSuspended,
|
||||
};
|
||||
}
|
||||
@@ -66,6 +72,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
const signins = await this.signinsRepository.findBy({ userId: user.id });
|
||||
|
||||
const roles = await this.roleService.getUserRoles(user.id);
|
||||
|
||||
return {
|
||||
email: profile.email,
|
||||
emailVerified: profile.emailVerified,
|
||||
@@ -80,12 +88,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
mutedWords: profile.mutedWords,
|
||||
mutedInstances: profile.mutedInstances,
|
||||
mutingNotificationTypes: profile.mutingNotificationTypes,
|
||||
isModerator: user.isModerator,
|
||||
isSilenced: user.isSilenced,
|
||||
isModerator: isModerator,
|
||||
isSilenced: isSilenced,
|
||||
isSuspended: user.isSuspended,
|
||||
lastActiveDate: user.lastActiveDate,
|
||||
moderationNote: profile.moderationNote,
|
||||
signins,
|
||||
roles: await this.roleEntityService.packMany(roles, me, { detail: false }),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -28,7 +29,7 @@ export const paramDef = {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
offset: { type: 'integer', default: 0 },
|
||||
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt', '+lastActiveDate', '-lastActiveDate'] },
|
||||
state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' },
|
||||
state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'suspended'], default: 'all' },
|
||||
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
|
||||
username: { type: 'string', nullable: true, default: null },
|
||||
hostname: {
|
||||
@@ -49,18 +50,33 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.usersRepository.createQueryBuilder('user');
|
||||
|
||||
switch (ps.state) {
|
||||
case 'available': query.where('user.isSuspended = FALSE'); break;
|
||||
case 'admin': query.where('user.isAdmin = TRUE'); break;
|
||||
case 'moderator': query.where('user.isModerator = TRUE'); break;
|
||||
case 'adminOrModerator': query.where('user.isAdmin = TRUE OR user.isModerator = TRUE'); break;
|
||||
case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;
|
||||
case 'silenced': query.where('user.isSilenced = TRUE'); break;
|
||||
case 'suspended': query.where('user.isSuspended = TRUE'); break;
|
||||
case 'admin': {
|
||||
const adminIds = await this.roleService.getAdministratorIds();
|
||||
if (adminIds.length === 0) return [];
|
||||
query.where('user.id IN (:...adminIds)', { adminIds: adminIds });
|
||||
break;
|
||||
}
|
||||
case 'moderator': {
|
||||
const moderatorIds = await this.roleService.getModeratorIds(false);
|
||||
if (moderatorIds.length === 0) return [];
|
||||
query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds });
|
||||
break;
|
||||
}
|
||||
case 'adminOrModerator': {
|
||||
const adminOrModeratorIds = await this.roleService.getModeratorIds();
|
||||
if (adminOrModeratorIds.length === 0) return [];
|
||||
query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (ps.origin) {
|
||||
|
@@ -1,55 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
|
||||
if (user == null) {
|
||||
throw new Error('user not found');
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
throw new Error('cannot silence admin');
|
||||
}
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
isSilenced: true,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('userChangeSilencedState', { id: user.id, isSilenced: true });
|
||||
|
||||
this.moderationLogService.insertModerationLog(me, 'silence', {
|
||||
targetId: user.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -9,6 +9,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -41,6 +42,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private userEntityService: UserEntityService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private userSuspendService: UserSuspendService,
|
||||
private roleService: RoleService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
@@ -51,12 +53,8 @@ export default class extends Endpoint<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 (await this.roleService.isModerator(user)) {
|
||||
throw new Error('cannot suspend moderator account');
|
||||
}
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
|
@@ -1,51 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
|
||||
if (user == null) {
|
||||
throw new Error('user not found');
|
||||
}
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
isSilenced: false,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('userChangeSilencedState', { id: user.id, isSilenced: false });
|
||||
|
||||
this.moderationLogService.insertModerationLog(me, 'unsilence', {
|
||||
targetId: user.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -19,8 +19,6 @@ export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
disableRegistration: { type: 'boolean', nullable: true },
|
||||
disableLocalTimeline: { type: 'boolean', nullable: true },
|
||||
disableGlobalTimeline: { type: 'boolean', nullable: true },
|
||||
useStarForReactionFallback: { type: 'boolean', nullable: true },
|
||||
pinnedUsers: { type: 'array', nullable: true, items: {
|
||||
type: 'string',
|
||||
@@ -42,8 +40,6 @@ export const paramDef = {
|
||||
description: { type: 'string', nullable: true },
|
||||
defaultLightTheme: { type: 'string', nullable: true },
|
||||
defaultDarkTheme: { type: 'string', nullable: true },
|
||||
localDriveCapacityMb: { type: 'integer' },
|
||||
remoteDriveCapacityMb: { type: 'integer' },
|
||||
cacheRemoteFiles: { type: 'boolean' },
|
||||
emailRequiredForSignup: { type: 'boolean' },
|
||||
enableHcaptcha: { type: 'boolean' },
|
||||
@@ -130,14 +126,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
set.disableRegistration = ps.disableRegistration;
|
||||
}
|
||||
|
||||
if (typeof ps.disableLocalTimeline === 'boolean') {
|
||||
set.disableLocalTimeline = ps.disableLocalTimeline;
|
||||
}
|
||||
|
||||
if (typeof ps.disableGlobalTimeline === 'boolean') {
|
||||
set.disableGlobalTimeline = ps.disableGlobalTimeline;
|
||||
}
|
||||
|
||||
if (typeof ps.useStarForReactionFallback === 'boolean') {
|
||||
set.useStarForReactionFallback = ps.useStarForReactionFallback;
|
||||
}
|
||||
@@ -194,14 +182,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
set.defaultDarkTheme = ps.defaultDarkTheme;
|
||||
}
|
||||
|
||||
if (ps.localDriveCapacityMb !== undefined) {
|
||||
set.localDriveCapacityMb = ps.localDriveCapacityMb;
|
||||
}
|
||||
|
||||
if (ps.remoteDriveCapacityMb !== undefined) {
|
||||
set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb;
|
||||
}
|
||||
|
||||
if (ps.cacheRemoteFiles !== undefined) {
|
||||
set.cacheRemoteFiles = ps.cacheRemoteFiles;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user