Bump base to v13.10.3+58f3a2e

This commit is contained in:
ltlapy 2023-04-03 14:09:16 +09:00
commit 7a7ac9f645
271 changed files with 11300 additions and 2613 deletions

36
.github/workflows/api-misskey-js.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: API report (misskey.js)
on: [push, pull_request]
jobs:
report:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3.3.0
- run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v3.6.0
with:
node-version: 18.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm i --frozen-lockfile
- name: Build
run: pnpm --filter misskey-js build
- name: Check files
run: ls packages/misskey-js/built
- name: API report
run: pnpm --filter misskey-js api-prod
- name: Show report
if: always()
run: cat packages/misskey-js/temp/misskey-js.api.md

View file

@ -36,6 +36,7 @@ jobs:
- backend
- frontend
- sw
- misskey-js
steps:
- uses: actions/checkout@v3.3.0
with:
@ -61,6 +62,7 @@ jobs:
matrix:
workspace:
- backend
- misskey-js
steps:
- uses: actions/checkout@v3.3.0
with:

52
.github/workflows/test-misskey-js.yml vendored Normal file
View file

@ -0,0 +1,52 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Test (misskey.js)
on:
push:
branches: [ develop ]
pull_request:
branches: [ develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- name: Checkout
uses: actions/checkout@v3.3.0
- run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Build
run: pnpm --filter misskey-js build
- name: Test
run: pnpm --filter misskey-js test
env:
CI: true
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/misskey-js/coverage/coverage-final.json

1
.gitignore vendored
View file

@ -55,6 +55,7 @@ api-docs.json
.DS_Store
/files
ormconfig.json
temp
# blender backups
*.blend1

View file

@ -5,13 +5,57 @@
-
### Client
-
- 「にゃああああああああああああああ!!!!!!!!!!!!」 (`isCat`) 有効時にアバターに表示される猫耳について挙動を変更
- 「UIにぼかし効果を使用」 (`useBlurEffect`) で次の挙動が有効になります
- 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
- 猫耳の色がアバター上部のピクセルから決定されます(無効化時はアバター全体の平均色)
- 左耳は上からおよそ 10%, 左からおよそ 20% の位置で決定します
- 右耳は上からおよそ 10%, 左からおよそ 80% の位置で決定します
- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
### Server
-
-->
## 13.x.x (unreleased)
### General
- チャンネルをお気に入りに登録できるように
- チャンネルにノートをピン留めできるように
- アンテナのタイムライン取得時のパフォーマンスを向上
- チャンネルのタイムライン取得時のパフォーマンスを向上
### Client
- 検索ページでURLを入力した際に照会したときと同等の挙動をするように
- ノートのリアクションを大きく表示するオプションを追加
- オブジェクトストレージの設定画面を分かりやすく
### Server
-
## 13.10.3
### Changes
- オブジェクトストレージのリージョン指定が必須になりました
- リージョンの指定の無いサービスは us-east-1 を設定してください
- 値が空の場合は設定ファイルまたは環境変数の使用を試みます
- e.g. ~/aws/config, AWS_REGION
### General
- コンディショナルロールの条件に「投稿数が~以下」「投稿数が~以上」を追加
- リアクション非対応AP実装からのLikeアクティビティの解釈を👍から♥に
### Client
- クリップボタンをノートアクションに追加できるように
- センシティブワードの一覧にピン留めユーザーのIDが表示される問題を修正
### Server
- リモートユーザーのチャート生成を無効にするオプションを追加
- リモートサーバーのチャート生成を無効にするオプションを追加
- ドライブのチャートはローカルユーザーのみ生成するように
- 空のアンテナが作成できるのを修正
## 13.10.2
### Server

View file

@ -23,6 +23,7 @@ COPY --link ["scripts", "./scripts"]
COPY --link ["packages/backend/package.json", "./packages/backend/"]
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
COPY --link ["packages/sw/package.json", "./packages/sw/"]
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm i --frozen-lockfile --aggregate-output

View file

@ -545,7 +545,6 @@ tokenRequested: "منح حق الوصول إلى الحساب"
pluginTokenRequestedDescription: "ستتمكن الإضافة من استخدام هذه الأذونات."
notificationType: "أنواع الإشعارات"
edit: "التعديل"
useStarForReactionFallback: "استخدم ★ كبديل إذا كان التفاعل مجهولًا"
emailServer: "خادم البريد الإلكتروني"
emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها."
email: "البريد الإلكتروني "
@ -1275,3 +1274,7 @@ _deck:
channel: "القنوات"
mentions: "الإشارات"
direct: "مباشرة"
_webhookSettings:
name: "الإسم"
active: "مفعّل"

View file

@ -562,7 +562,6 @@ tokenRequested: "অ্যাকাউন্টে অ্যাক্সেস
pluginTokenRequestedDescription: "এই প্লাগইনটি এখানে দেওয়া অনুমুতিসমূহ ব্যাবহার করবে"
notificationType: "বিজ্ঞপ্তির ধরন"
edit: "সম্পাদনা"
useStarForReactionFallback: "রিঅ্যাকশনের ইমোজি না জানলে ★ ব্যবহার করুন"
emailServer: "ইমেইল সার্ভার"
enableEmail: "ইমেইল বিতরণ চালু করুন"
emailConfigInfo: "আপনার ইমেল ঠিকানা নিশ্চিত করতে এবং আপনার পাসওয়ার্ড পুনরায় সেট করতে ব্যবহৃত হয়"
@ -1354,3 +1353,7 @@ _deck:
channel: "চ্যানেলগুলি"
mentions: "উল্লেখসমূহ"
direct: "ডাইরেক্ট নোটগুলি"
_webhookSettings:
name: "নাম"
active: "চালু"

View file

@ -460,3 +460,4 @@ _deck:
list: "Llistes"
mentions: "Mencions"
direct: "Publicacions directes"

View file

@ -776,3 +776,7 @@ _deck:
list: "Seznamy"
channel: "Kanály"
mentions: "Zmínění"
_webhookSettings:
name: "Jméno"
active: "Zapnuto"

View file

@ -1,2 +1,3 @@
---
_lang_: "Dansk"

View file

@ -460,7 +460,7 @@ aboutX: "Über {x}"
emojiStyle: "Emoji-Stil"
native: "Nativ"
disableDrawer: "Keine ausfahrbaren Menüs verwenden"
showNoteActionsOnlyHover: "Aktionen für Notizen nur bei Mouseover anzeigen"
showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen"
noHistory: "Kein Verlauf gefunden"
signinHistory: "Anmeldungsverlauf"
enableAdvancedMfm: "Erweitertes MFM aktivieren"
@ -594,7 +594,6 @@ tokenRequested: "Zugriff zum Benutzerkonto gewähren"
pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können."
notificationType: "Art der Benachrichtigung"
edit: "Bearbeiten"
useStarForReactionFallback: "Verwende ★ falls das Reaktions-Emoji unbekannt ist"
emailServer: "Email-Server"
enableEmail: "Email-Versand aktivieren"
emailConfigInfo: "Zur Email-Bestätigung bei Registrierung oder zum Zurücksetzen des Passworts verwendet"
@ -981,6 +980,9 @@ drivecleaner: "Drive-Reiniger"
retryAllQueuesNow: "Sofort Warteschlangen erneut ausführen"
retryAllQueuesConfirmTitle: "Wirklich erneut versuchen?"
retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverlast führen."
enableChartsForRemoteUser: "Diagramme für Nutzer fremder Instanzen erstellen"
enableChartsForFederatedInstances: "Diagramme für fremde Instanzen erstellen"
showClipButtonInNoteFooter: "\"Clip\" zum Notizmenu hinzufügen"
_achievements:
earnedAt: "Freigeschaltet am"
_types:
@ -1277,6 +1279,8 @@ _role:
followersMoreThanOrEq: "Hat X oder mehr Follower"
followingLessThanOrEq: "Folgt X oder weniger Benutzern"
followingMoreThanOrEq: "Folgt X oder mehr Benutzern"
notesLessThanOrEq: "Beitragszahl ist kleiner-gleich"
notesMoreThanOrEq: "Beitragszahl ist größer-gleich"
and: "UND-Bedingung"
or: "ODER-Bedingung"
not: "NICHT-Bedingung"
@ -1875,3 +1879,18 @@ _disabledTimeline:
_drivecleaner:
orderBySizeDesc: "Absteigende Dateigrößen"
orderByCreatedAtAsc: "Aufsteigendes Erstelldatum"
_webhookSettings:
createWebhook: "Webhook erstellen"
name: "Name"
secret: "Secret"
events: "Webhook-Ereignisse"
active: "Aktiviert"
_events:
follow: "Wenn du jemandem folgst"
followed: "Wenn dir jemand folgt"
note: "Wenn du eine Notiz schickst"
reply: "Wenn du eine Antwort erhältst"
renote: "Wenn du ein Renote erhältst"
reaction: "Wenn du eine Reaktion erhältst"
mention: "Wenn du erwähnt wirst"

View file

@ -392,3 +392,6 @@ _deck:
antenna: "Αντένες"
list: "Λίστα"
mentions: "Επισημάνσεις"
_webhookSettings:
name: "Όνομα"

View file

@ -594,7 +594,6 @@ tokenRequested: "Grant access to account"
pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here."
notificationType: "Notification type"
edit: "Edit"
useStarForReactionFallback: "Use ★ as fallback if the reaction emoji is unknown"
emailServer: "Email server"
enableEmail: "Enable email distribution"
emailConfigInfo: "Used to confirm your email during sign-up or if you forget your password"
@ -986,6 +985,9 @@ drivecleaner: "Drive Cleaner"
retryAllQueuesNow: "Retry running all queues"
retryAllQueuesConfirmTitle: "Really retry all?"
retryAllQueuesConfirmText: "This will temporarily increase the server load."
enableChartsForRemoteUser: "Generate remote user data charts"
enableChartsForFederatedInstances: "Generate remote instance data charts"
showClipButtonInNoteFooter: "Add \"Clip\" to note action menu"
_achievements:
earnedAt: "Unlocked at"
_types:
@ -1282,6 +1284,8 @@ _role:
followersMoreThanOrEq: "Has X or more followers"
followingLessThanOrEq: "Follows X or fewer accounts"
followingMoreThanOrEq: "Follows X or more accounts"
notesLessThanOrEq: "Post count is less than/equal to"
notesMoreThanOrEq: "Post count is greater than/equal to"
and: "AND-Condition"
or: "OR-Condition"
not: "NOT-Condition"
@ -1880,3 +1884,18 @@ _disabledTimeline:
_drivecleaner:
orderBySizeDesc: "Descending Filesizes"
orderByCreatedAtAsc: "Ascending Dates"
_webhookSettings:
createWebhook: "Create Webhook"
name: "Name"
secret: "Secret"
events: "Webhook Events"
active: "Enabled"
_events:
follow: "When following a user"
followed: "When being followed"
note: "When posting a note"
reply: "When receiving a reply"
renote: "When renoted"
reaction: "When receiving a reaction"
mention: "When being mentioned"

View file

@ -594,7 +594,6 @@ tokenRequested: "Permiso de acceso a la cuenta"
pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí"
notificationType: "Tipo de notificación"
edit: "Editar"
useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella"
emailServer: "Servidor de correo"
enableEmail: "Activar el envío de correos electrónicos"
emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña"
@ -1875,3 +1874,7 @@ _disabledTimeline:
_drivecleaner:
orderBySizeDesc: "Más grandes"
orderByCreatedAtAsc: "Más antiguos"
_webhookSettings:
name: "Nombre"
active: "Activado"

View file

@ -575,7 +575,6 @@ tokenRequested: "Autoriser l'accès au compte"
pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici."
notificationType: "Type de notifications"
edit: "Editer"
useStarForReactionFallback: "Utiliser ★ comme alternative si lémoji de réaction est inconnu"
emailServer: "Serveur mail"
enableEmail: "Activer la distribution de courriel"
emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas doubli."
@ -1468,3 +1467,7 @@ _deck:
channel: "Canaux"
mentions: "Mentions"
direct: "Direct"
_webhookSettings:
name: "Nom"
active: "Activé"

View file

@ -1 +1,2 @@
---

View file

@ -1 +1,2 @@
---

View file

@ -579,7 +579,6 @@ tokenRequested: "Berikan ijin akses ke akun"
pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini."
notificationType: "Jenis pemberitahuan"
edit: "Sunting"
useStarForReactionFallback: "Gunakan ★ sebagai fallback jika reaksi emoji tidak diketahui"
emailServer: "Peladen surel"
enableEmail: "Nyalakan distribusi surel"
emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi"
@ -1804,3 +1803,7 @@ _deck:
channel: "Kanal"
mentions: "Sebutan"
direct: "Langsung"
_webhookSettings:
name: "Nama"
active: "Aktif"

View file

@ -565,8 +565,8 @@ enableInfiniteScroll: "Abilita scorrimento infinito"
visibility: "Visibilità"
poll: "Sondaggio"
useCw: "Nascondere media"
enablePlayer: "Apri in lettore video"
disablePlayer: "Chiudi il lettore"
enablePlayer: "Visualizza"
disablePlayer: "Chiudi"
expandTweet: "Espandi tweet"
themeEditor: "Editor di temi"
description: "Descrizione"
@ -594,7 +594,6 @@ tokenRequested: "Autorizza accesso al profilo"
pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui."
notificationType: "Tipo di notifiche"
edit: "Modifica"
useStarForReactionFallback: "Se è sconosciuto l'emoji di reazione, usare la ★ come alternativa."
emailServer: "Server email"
enableEmail: "Abilita consegna email"
emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per reimpostare la tua password"
@ -978,6 +977,9 @@ license: "Licenza"
unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?"
myClips: "Le mie Clip"
drivecleaner: "Drive cleaner"
retryAllQueuesNow: "Ritenta di consumare tutte le code"
retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?"
retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente."
_achievements:
earnedAt: "Data di conseguimento"
_types:
@ -1872,3 +1874,7 @@ _disabledTimeline:
_drivecleaner:
orderBySizeDesc: "Dal più grande al più piccolo"
orderByCreatedAtAsc: "Dal più vecchio al più recente"
_webhookSettings:
name: "Nome"
active: "Attivo"

View file

@ -472,7 +472,7 @@ native: "ネイティブ"
disableDrawer: "メニューをドロワーで表示しない"
youHaveNoGroups: "グループがありません"
joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。"
showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示する"
showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
noHistory: "履歴はありません"
signinHistory: "ログイン履歴"
enableAdvancedMfm: "高度なMFMを有効にする"
@ -512,12 +512,13 @@ objectStoragePrefixDesc: "このprefixのディレクトリ下に格納されま
objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。'<host>'または'<host>:<port>'のように指定します。"
objectStorageRegion: "Region"
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は、空または'us-east-1'にしてください。"
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は'us-east-1'にしてください。AWS設定ファイルまたは環境変数を参照する場合は空にしてください。"
objectStorageUseSSL: "SSLを使用する"
objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください"
objectStorageUseProxy: "Proxyを利用する"
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
s3ForcePathStyleDesc: "s3ForcePathStyleを有効にすると、バケット名をURLのホスト名ではなくパスの一部として指定することを強制します。セルフホストされたMinioなどの使用時に有効にする必要がある場合があります。"
serverLogs: "サーバーログ"
deleteAll: "全て削除"
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
@ -606,7 +607,6 @@ tokenRequested: "アカウントへのアクセス許可"
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
notificationType: "通知の種類"
edit: "編集"
useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
emailServer: "メールサーバー"
enableEmail: "メール配信機能を有効化する"
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
@ -998,6 +998,11 @@ drivecleaner: "ドライブクリーナー"
retryAllQueuesNow: "すべてのキューを今すぐ再試行"
retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
largeNoteReactions: "ノートのリアクションを大きく表示"
noteIdOrUrl: "ートIDまたはURL"
_achievements:
earnedAt: "獲得日時"
@ -1296,6 +1301,8 @@ _role:
followersMoreThanOrEq: "フォロワー数が~以上"
followingLessThanOrEq: "フォロー数が~以下"
followingMoreThanOrEq: "フォロー数が~以上"
notesLessThanOrEq: "投稿数が~以下"
notesMoreThanOrEq: "投稿数が~以上"
and: "~かつ~"
or: "~または~"
not: "~ではない"
@ -1951,4 +1958,20 @@ _disabledTimeline:
_drivecleaner:
orderBySizeDesc: "サイズが大きい順"
orderByCreatedAtAsc: "追加日が古い順"
orderByCreatedAtAsc: "追加日が古い順"
_webhookSettings:
createWebhook: "Webhookを作成"
name: "名前"
secret: "シークレット"
events: "Webhookを実行するタイミング"
active: "有効"
_events:
follow: "フォローしたとき"
followed: "フォローされたとき"
note: "ノートを投稿したとき"
reply: "返信されたとき"
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"

View file

@ -594,7 +594,6 @@ tokenRequested: "アカウントへのアクセス許してやったらどうや
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。"
notificationType: "通知の種類"
edit: "編集"
useStarForReactionFallback: "リアクションがようわからん場合、★を使う"
emailServer: "メールサーバー"
enableEmail: "メール配信を受け取る"
emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで"
@ -981,6 +980,9 @@ drivecleaner: "ドライブキレイキレイ"
retryAllQueuesNow: "キューを全部もっかいやり直す"
retryAllQueuesConfirmTitle: "もっかいやってみるか?"
retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへんで。"
enableChartsForRemoteUser: "リモートユーザーのチャートを作る"
enableChartsForFederatedInstances: "リモートサーバーのチャートを作る"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
_achievements:
earnedAt: "貰った日ぃ"
_types:
@ -1277,6 +1279,8 @@ _role:
followersMoreThanOrEq: "フォロワー数が~以上"
followingLessThanOrEq: "フォロー数が~以下"
followingMoreThanOrEq: "フォロー数が~以上"
notesLessThanOrEq: "投稿数が~以下しかない"
notesMoreThanOrEq: "投稿を~以上しとる"
and: "~かつ~"
or: "~または~"
not: "~ではない"
@ -1875,3 +1879,18 @@ _disabledTimeline:
_drivecleaner:
orderBySizeDesc: "サイズのでかい順"
orderByCreatedAtAsc: "追加日の古い順"
_webhookSettings:
createWebhook: "Webhookをつくる"
name: "名前"
secret: "シークレット"
events: "Webhookを投げるタイミング"
active: "有効"
_events:
follow: "フォローしたとき~!"
followed: "フォローもらったとき~!"
note: "ノートを投稿したとき~!"
reply: "返信があるとき~!"
renote: "Renoteされるとき"
reaction: "リアクションがあるとき~!"
mention: "メンションがあるとき~!"

View file

@ -1 +1,2 @@
---

View file

@ -103,3 +103,4 @@ _deck:
_columns:
notifications: "Ilɣuyen"
list: "Tibdarin"

View file

@ -83,3 +83,4 @@ _deck:
notifications: "ಅಧಿಸೂಚನೆಗಳು"
tl: "ಸಮಯಸಾಲು"
mentions: "ಹೆಸರಿಸಿದ"

View file

@ -592,7 +592,6 @@ tokenRequested: "계정 접근 허용"
pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다."
notificationType: "알림 유형"
edit: "편집"
useStarForReactionFallback: "알 수 없는 리액션 이모지 대신 ★ 사용"
emailServer: "메일 서버"
enableEmail: "이메일 송신 기능 활성화"
emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다."
@ -1854,3 +1853,7 @@ _deck:
_dialog:
charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {min}"
charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}"
_webhookSettings:
name: "이름"
active: "활성화"

View file

@ -368,3 +368,4 @@ _deck:
list: "ລາຍການ"
channel: "ຊ່ອງ"
mentions: "ກ່າວເຖິງ"

View file

@ -483,3 +483,6 @@ _deck:
antenna: "Antennes"
list: "Lijsten"
mentions: "Vermeldingen"
_webhookSettings:
name: "Naam"

View file

@ -1,2 +1,3 @@
---
_lang_: "Norsk Bokmål"

View file

@ -129,6 +129,7 @@ unblockConfirm: "Czy na pewno chcesz odblokować to konto?"
suspendConfirm: "Czy na pewno chcesz zawiesić to konto?"
unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?"
selectList: "Wybierz listę"
selectChannel: "Wybierz kanał"
selectAntenna: "Wybierz Antennę"
selectWidget: "Wybierz widżet"
editWidgets: "Edytuj widżety"
@ -149,6 +150,7 @@ flagAsCatDescription: "Przełącz tę opcję, aby konto było oznaczone jako kot
flagShowTimelineReplies: "Pokazuj odpowiedzi na osi czasu"
autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz"
addAccount: "Dodaj konto"
reloadAccountsList: "Odśwież listę kont"
loginFailed: "Nie udało się zalogować"
showOnRemote: "Zobacz na zdalnej instancji"
general: "Ogólne"
@ -159,6 +161,7 @@ searchWith: "Szukaj: {q}"
youHaveNoLists: "Nie masz żadnej listy"
followConfirm: "Czy na pewno chcesz zaobserwować {name}?"
proxyAccount: "Konto proxy"
proxyAccountDescription: "Opis konta pełnomocniczego"
host: "Host"
selectUser: "Wybierz użytkownika"
recipient: "Odbiorca"
@ -253,6 +256,7 @@ noMoreHistory: "Nie ma dalszej historii"
startMessaging: "Rozpocznij czat"
nUsersRead: "przeczytano przez {n}"
agreeTo: "Wyrażam zgodę na {0}"
agreeBelow: "Zaakceptuj poniżej"
tos: "Regulamin"
start: "Rozpocznij"
home: "Strona główna"
@ -385,13 +389,19 @@ about: "Informacje"
aboutMisskey: "O Misskey"
administrator: "Admin"
token: "Token"
2fa: "Klucz 2FA "
totp: "Klucz aplikacji uwierzytelniającej (totp)"
totpDescription: "Opis klucza czasowego"
moderator: "Moderator"
moderation: "Moderacja"
nUsersMentioned: "{n} wspomnianych użytkowników"
securityKeyAndPasskey: "Klucz bezpieczeństwa i klucze Passkey"
securityKey: "Klucz bezpieczeństwa"
lastUsed: "Ostatnio używane"
lastUsedAt: "Ostatnio używane w"
unregister: "Cofnij rejestrację"
passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła"
passwordLessLoginDescription: "Opis logowania bez użycia hasła"
resetPassword: "Zresetuj hasło"
newPasswordIs: "Nowe hasło to „{password}”"
reduceUiAnimation: "Ogranicz animacje w UI"
@ -518,11 +528,16 @@ disablePagesScript: "Wyłącz AiScript na Stronach"
updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku"
deleteAllFiles: "Usuń wszystkie pliki"
deleteAllFilesConfirm: "Czy na pewno chcesz usunąć wszystkie pliki?"
removeAllFollowing: "Przestań obserwować"
removeAllFollowingDescription: "Przestań obserwować wszystkie konta z {host}. Wykonaj to, jeżeli instancja już nie istnieje."
userSuspended: "To konto zostało zawieszone."
userSilenced: "Ten użytkownik został wyciszony."
yourAccountSuspendedTitle: "To konto jest zawieszone"
yourAccountSuspendedDescription: "To konto zostało zawieszone z powodu złamania regulaminu serwera lub innych podobnych. Skontaktuj się z administratorem, jeśli chciałbyś poznać bardziej szczegółowy powód. Proszę nie zakładać nowego konta."
tokenRevoked: "Token odrzucony"
tokenRevokedDescription: "Opis odrzuconego tokena"
accountDeleted: "Konto usunięte"
accountDeletedDescription: "Opis konta usuniętego"
menu: "Menu"
divider: "Rozdzielacz"
addItem: "Dodaj element"
@ -548,7 +563,9 @@ author: "Autor"
leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?"
manage: "Zarządzanie"
plugins: "Wtyczki"
preferencesBackups: "Kopia zapasowa ustawień"
deck: "Tablica"
undeck: "oddkouj"
useBlurEffectForModal: "Używaj efektu rozmycia w modalach"
useFullReactionPicker: "Używaj pełnowymiarowego wybornika reakcji"
width: "Szerokość"
@ -564,7 +581,6 @@ tokenRequested: "Przydziel dostęp do konta"
pluginTokenRequestedDescription: "Ta wtyczka będzie mogła korzystać z ustawionych tu uprawnień."
notificationType: "Rodzaj powiadomień"
edit: "Edytuj"
useStarForReactionFallback: "Użyj ★ jako zapasowego emoji, gdy emoji reakcji jest nieznane"
emailServer: "Serwer poczty e-mail"
enableEmail: "Włącz dostarczanie wiadomości e-mail"
emailConfigInfo: "Wykorzystywany do potwierdzenia adresu e-mail w trakcie rejestracji, lub gdy zapomnisz hasła"
@ -816,6 +832,8 @@ tenMinutes: "10 minut"
oneHour: "1 godzina"
oneDay: "1 dzień"
oneWeek: "1 tydzień"
oneMonth: "jeden miesiąc"
failedToFetchAccountInformation: "Nie udało się uzyskać informacji o koncie"
file: "Pliki"
recommended: "Zalecane"
check: "Zweryfikuj"
@ -1358,3 +1376,7 @@ _deck:
channel: "Kanały"
mentions: "Wspomnienia"
direct: "Bezpośredni"
_webhookSettings:
name: "Nazwa"
active: "Właczono"

View file

@ -555,3 +555,6 @@ _deck:
list: "Listas"
mentions: "Menções"
direct: "Notas diretas"
_webhookSettings:
name: "Nome"

View file

@ -561,7 +561,6 @@ tokenRequested: "Acordă acces la cont"
pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici."
notificationType: "Tipul notificării"
edit: "Editează"
useStarForReactionFallback: "Folosește ★ ca fallback dacă emoji-ul este necunoscut"
emailServer: "Server email"
enableEmail: "Activează distribuția de emailuri"
emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola"
@ -702,3 +701,6 @@ _deck:
list: "Liste"
channel: "Canale"
mentions: "Mențiuni"
_webhookSettings:
name: "Nume"

View file

@ -585,7 +585,6 @@ tokenRequested: "Открыть доступ к учётной записи"
pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь."
notificationType: "Тип уведомления"
edit: "Изменить"
useStarForReactionFallback: "Ставить ★ в качестве реакции вместо неизвестного эмодзи"
emailServer: "Сервер электронной почты"
enableEmail: "Включить обмен электронной почтой"
emailConfigInfo: "Используется для подтверждения адреса электронной почты и сброса пароля."
@ -1837,3 +1836,7 @@ _deck:
_dialog:
charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}"
charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}"
_webhookSettings:
name: "Название"
active: "Вкл."

View file

@ -1 +1,2 @@
---

View file

@ -586,7 +586,6 @@ tokenRequested: "Povoliť prístup k účtu"
pluginTokenRequestedDescription: "Tento plugin bude môcť používať oprávnenia nastavené tu."
notificationType: "Typ oznámenia"
edit: "Upraviť"
useStarForReactionFallback: "Použiť ★ keď emoji reakcie nie je známe"
emailServer: "Email server"
enableEmail: "Zapnúť email"
emailConfigInfo: "Používa sa na overenie emaily pri registrácii alebo pri zabudnutí hesla"
@ -1475,3 +1474,7 @@ _deck:
channel: "Kanály"
mentions: "Zmienky"
direct: "Priame poznámky"
_webhookSettings:
name: "Názov"
active: "Zapnuté"

View file

@ -442,3 +442,6 @@ _deck:
antenna: "Antenner"
list: "Listor"
mentions: "Omnämningar"
_webhookSettings:
active: "Aktiverad"

View file

@ -544,6 +544,8 @@ userSuspended: "ผู้ใช้รายนี้ถูกระงับก
userSilenced: "ผู้ใช้รายนี้กำลังถูกปิดกั้น"
yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ"
yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่"
tokenRevoked: "โทเค็นไม่ถูกต้อง"
accountDeleted: "ลบบัญชีแล้ว"
menu: "เมนู"
divider: "ตัวแบ่ง"
addItem: "เพิ่มรายการ"
@ -587,7 +589,6 @@ tokenRequested: "ให้สิทธิ์การเข้าถึงบั
pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ"
notificationType: "ประเภทการแจ้งเตือน"
edit: "แก้ไข"
useStarForReactionFallback: "ใช้ ★ เป็นทางเลือกแทนถ้าหากไม่ทราบอิโมจิ"
emailServer: "อีเมล์เซิร์ฟเวอร์"
enableEmail: "เปิดใช้งานการกระจายอีเมล"
emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน"
@ -959,6 +960,18 @@ invitationRequiredToRegister: "อินสแตนซ์นี้เป็น
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ"
postToTheChannel: "โพสต์ลงช่อง"
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
likeOnly: "ที่ชอบเท่านั้น"
resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?"
sensitiveWords: "คำที่ละเอียดอ่อน"
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งานนะค่ะ"
license: "ใบอนุญาต"
unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?"
myClips: "คลิปของฉัน"
drivecleaner: "ทำความสะอาดไดรฟ์"
retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหมดอีกครั้ง"
retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?"
retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
_achievements:
earnedAt: "ได้รับเมื่อ"
_types:
@ -1218,6 +1231,8 @@ _role:
iconUrl: "ไอคอน URL"
asBadge: "แสดงเป็นตรา"
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
displayOrder: "ตำแหน่ง"
descriptionOfDisplayOrder: "ยิ่งตัวเลขสูง ตำแหน่ง UI ก็ยิ่งสูงขึ้นนะ"
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
priority: "ลำดับความสำคัญ"
@ -1243,6 +1258,7 @@ _role:
rateLimitFactor: "ขีดจำกัดอัตรา"
descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า"
canHideAds: "ซ่อนโฆษณา"
canSearchNotes: "การใช้การค้นหาโน้ต"
_condition:
isLocal: "ผู้ใช้ภายใน"
isRemote: "ผู้ใช้ระยะไกล"
@ -1844,3 +1860,13 @@ _deck:
_dialog:
charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}"
charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}"
_disabledTimeline:
title: "ปิดใช้งานไทม์ไลน์"
description: "คุณไม่สามารถใช้ไทม์ไลน์นี้ภายใต้บทบาทปัจจุบันของคุณได้"
_drivecleaner:
orderBySizeDesc: "ขนาดไฟล์จากมากไปหาน้อย"
orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก"
_webhookSettings:
name: "ชื่อ"
active: "เปิดใช้งาน"

View file

@ -60,3 +60,4 @@ _deck:
_columns:
notifications: "Bildirim"
tl: "Zaman çizelgesi"

View file

@ -2,3 +2,4 @@
_lang_: "ياپونچە"
search: "ئىزدەش"
searchByGoogle: "ئىزدەش"

View file

@ -576,7 +576,6 @@ tokenRequested: "Надати доступ до акаунту"
pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані."
notificationType: "Тип сповіщення"
edit: "Редагувати"
useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий"
emailServer: "Email сервер"
enableEmail: "Увімкнути функцію доставки пошти"
emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю."
@ -1639,3 +1638,7 @@ _deck:
channel: "Канали"
mentions: "Згадки"
direct: "Особисте"
_webhookSettings:
name: "Ім'я"
active: "Увімкнено"

View file

@ -585,7 +585,6 @@ tokenRequested: "Cấp quyền truy cập vào tài khoản"
pluginTokenRequestedDescription: "Plugin này sẽ có thể sử dụng các quyền được đặt ở đây."
notificationType: "Loại thông báo"
edit: "Sửa"
useStarForReactionFallback: "Dùng ★ nếu emoji biểu cảm không có"
emailServer: "Email máy chủ"
enableEmail: "Bật phân phối email"
emailConfigInfo: "Được dùng để xác minh email của bạn lúc đăng ký hoặc nếu bạn quên mật khẩu của mình"
@ -1705,3 +1704,7 @@ _deck:
_dialog:
charactersExceeded: "Bạn nhắn quá giới hạn ký tự!! Hiện nay {current} / giới hạn {max}"
charactersBelow: "Bạn nhắn quá ít tối thiểu ký tự!! Hiện nay {current} / Tối thiểu {min}"
_webhookSettings:
name: "Tên"
active: "Đã bật"

View file

@ -594,7 +594,6 @@ tokenRequested: "允许访问账户"
pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
notificationType: "通知类型"
edit: "编辑"
useStarForReactionFallback: "如果回应的是未知表情符号,则使用★作为代替"
emailServer: "邮件服务器"
enableEmail: "启用发送邮件功能"
emailConfigInfo: "用于确认电子邮件和密码重置"
@ -986,6 +985,9 @@ drivecleaner: "网盘整理"
retryAllQueuesNow: "立刻重试所有队列"
retryAllQueuesConfirmTitle: "要再尝试一次吗?"
retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
enableChartsForRemoteUser: "生成远程用户的图表"
enableChartsForFederatedInstances: "生成远程服务器的图表"
showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
_achievements:
earnedAt: "达成时间"
_types:
@ -1282,6 +1284,8 @@ _role:
followersMoreThanOrEq: "关注者不少于"
followingLessThanOrEq: "关注中不多于"
followingMoreThanOrEq: "关注中不少于"
notesLessThanOrEq: "帖子数在~以下"
notesMoreThanOrEq: "帖子数在~以上"
and: "符合以下全部条件"
or: "符合以下任一条件"
not: "不符合以下任何条件"
@ -1880,3 +1884,18 @@ _disabledTimeline:
_drivecleaner:
orderBySizeDesc: "按大小降序排列"
orderByCreatedAtAsc: "按添加日期降序排列"
_webhookSettings:
createWebhook: "创建 Webhook"
name: "名称"
secret: "密钥"
events: "何时运行Webhook"
active: "已启用"
_events:
follow: "关注时"
followed: "被关注时"
note: "发布贴文时"
reply: "收到回复时"
renote: "被转发时"
reaction: "被回应时"
mention: "被提及时"

View file

@ -15,7 +15,7 @@ gotIt: "知道了"
cancel: "取消"
noThankYou: "現在不要"
enterUsername: "輸入使用者名稱"
renotedBy: "{user} 轉了"
renotedBy: "{user} 轉了"
noNotes: "無貼文。"
noNotifications: "沒有通知"
instance: "實例"
@ -99,9 +99,9 @@ followRequestPending: "追隨許可批准中"
enterEmoji: "輸入表情符號"
renote: "轉發"
unrenote: "取消轉發"
renoted: "轉成功"
renoted: "轉成功"
cantRenote: "無法轉發此貼文。"
cantReRenote: "無法轉傳之前已經轉傳過的內容。"
cantReRenote: "無法轉發之前已經轉發過的內容。"
quote: "引用"
inChannelRenote: "在頻道內轉發"
inChannelQuote: "在頻道內引用"
@ -594,7 +594,6 @@ tokenRequested: "允許存取帳戶"
pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。"
notificationType: "通知形式"
edit: "編輯"
useStarForReactionFallback: "以★代替未知的表情符號"
emailServer: "電郵伺服器"
enableEmail: "啟用發送電郵功能"
emailConfigInfo: "用於確認電郵地址及密碼重置"
@ -678,8 +677,8 @@ sentReactionsCount: "反應發送次數"
receivedReactionsCount: "收到反應次數"
pollVotesCount: "已統計的投票數"
pollVotedCount: "已投票數"
yes: "確定"
no: "取消"
yes: ""
no: ""
driveFilesCount: "雲端硬碟檔案數量"
driveUsage: "雲端硬碟使用量"
noCrawle: "拒絕搜尋引擎索引"
@ -981,6 +980,9 @@ drivecleaner: "雲端硬碟清掃器"
retryAllQueuesNow: "立刻重試所有佇列"
retryAllQueuesConfirmTitle: "要現在重試嗎?"
retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
enableChartsForRemoteUser: "生成遠端用戶的圖表"
enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
showClipButtonInNoteFooter: "將摘錄添加至貼文"
_achievements:
earnedAt: "獲得日期"
_types:
@ -1097,7 +1099,7 @@ _achievements:
title: "有備而來"
description: "設定了個人檔案"
_markedAsCat:
title: "我是貓"
title: "吾輩乃貓是也"
description: "已將帳戶設定為貓"
flavor: "還沒有名字。"
_following1:
@ -1277,6 +1279,8 @@ _role:
followersMoreThanOrEq: "追隨者人數在~以上"
followingLessThanOrEq: "追隨人數在~以下"
followingMoreThanOrEq: "追隨人數在~以上"
notesLessThanOrEq: "發布數在~以下"
notesMoreThanOrEq: "發布數在~以上"
and: "~和~"
or: "~或~"
not: "~否"
@ -1875,3 +1879,18 @@ _disabledTimeline:
_drivecleaner:
orderBySizeDesc: "檔案由大到小"
orderByCreatedAtAsc: "依照加入的日期順序"
_webhookSettings:
createWebhook: "建立 Webhook"
name: "名稱"
secret: "秘密"
events: "什麼時候運行Webhook"
active: "已啟用"
_events:
follow: "當你追隨時"
followed: "當被追隨時"
note: "當發布貼文時"
reply: "當收到回覆時"
renote: "當被轉發時"
reaction: "當獲得反應時"
mention: "當被提到時"

View file

@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "13.10.2+klapy",
"version": "13.10.3+58f3a2e+klapy",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@7.29.3",
"packageManager": "pnpm@8.1.0",
"workspaces": [
"packages/frontend",
"packages/backend",
@ -51,16 +51,16 @@
"gulp-replace": "1.1.4",
"gulp-terser": "2.1.0",
"js-yaml": "4.1.0",
"typescript": "4.9.5"
"typescript": "5.0.2"
},
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.54.1",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"cross-env": "7.0.3",
"cypress": "12.7.0",
"eslint": "8.35.0",
"cypress": "12.9.0",
"eslint": "8.37.0",
"start-server-and-test": "2.0.0"
},
"optionalDependencies": {

View file

@ -0,0 +1,11 @@
export class enableChartsForRemoteUser1679639483253 {
name = 'enableChartsForRemoteUser1679639483253'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableChartsForRemoteUser" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableChartsForRemoteUser"`);
}
}

View file

@ -0,0 +1,11 @@
export class cleanup1679651580149 {
name = 'cleanup1679651580149'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`);
}
}

View file

@ -0,0 +1,11 @@
export class enableChartsForFederatedInstances1679652081809 {
name = 'enableChartsForFederatedInstances1679652081809'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableChartsForFederatedInstances" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableChartsForFederatedInstances"`);
}
}

View file

@ -0,0 +1,21 @@
export class channelFavorite1680228513388 {
name = 'channelFavorite1680228513388'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "channel_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "channelId" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_59bddfd54d48689a298d41af00c" PRIMARY KEY ("id")); COMMENT ON COLUMN "channel_favorite"."createdAt" IS 'The created date of the ChannelFavorite.'`);
await queryRunner.query(`CREATE INDEX "IDX_735a5544f9249d412255f47f95" ON "channel_favorite" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_d3ca0db011b75ac2a940a2337d" ON "channel_favorite" ("channelId") `);
await queryRunner.query(`CREATE INDEX "IDX_8302bd27226605ece14842fb25" ON "channel_favorite" ("userId") `);
await queryRunner.query(`ALTER TABLE "channel_favorite" ADD CONSTRAINT "FK_d3ca0db011b75ac2a940a2337d2" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "channel_favorite" ADD CONSTRAINT "FK_8302bd27226605ece14842fb25a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel_favorite" DROP CONSTRAINT "FK_8302bd27226605ece14842fb25a"`);
await queryRunner.query(`ALTER TABLE "channel_favorite" DROP CONSTRAINT "FK_d3ca0db011b75ac2a940a2337d2"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8302bd27226605ece14842fb25"`);
await queryRunner.query(`DROP INDEX "public"."IDX_d3ca0db011b75ac2a940a2337d"`);
await queryRunner.query(`DROP INDEX "public"."IDX_735a5544f9249d412255f47f95"`);
await queryRunner.query(`DROP TABLE "channel_favorite"`);
}
}

View file

@ -0,0 +1,11 @@
export class channelNotePining1680238118084 {
name = 'channelNotePining1680238118084'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel" ADD "pinnedNoteIds" character varying(128) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "pinnedNoteIds"`);
}
}

View file

@ -0,0 +1,10 @@
export class cleanup1680491187535 {
name = 'cleanup1680491187535'
async up(queryRunner) {
await queryRunner.query(`DROP TABLE "antenna_note" `);
}
async down(queryRunner) {
}
}

View file

@ -23,43 +23,45 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "^1.3.11",
"@swc/core-darwin-arm64": "^1.3.38",
"@swc/core-darwin-x64": "^1.3.38",
"@swc/core-linux-arm-gnueabihf": "^1.3.38",
"@swc/core-linux-arm64-gnu": "^1.3.38",
"@swc/core-linux-arm64-musl": "^1.3.38",
"@swc/core-linux-x64-gnu": "^1.3.38",
"@swc/core-linux-x64-musl": "^1.3.38",
"@swc/core-win32-arm64-msvc": "^1.3.38",
"@swc/core-win32-ia32-msvc": "^1.3.38",
"@swc/core-win32-x64-msvc": "^1.3.38",
"@swc/core-darwin-arm64": "^1.3.42",
"@swc/core-darwin-x64": "^1.3.42",
"@swc/core-linux-arm-gnueabihf": "^1.3.42",
"@swc/core-linux-arm64-gnu": "^1.3.42",
"@swc/core-linux-arm64-musl": "^1.3.42",
"@swc/core-linux-x64-gnu": "^1.3.42",
"@swc/core-linux-x64-musl": "^1.3.42",
"@swc/core-win32-arm64-msvc": "^1.3.42",
"@swc/core-win32-ia32-msvc": "^1.3.42",
"@swc/core-win32-x64-msvc": "^1.3.42",
"@tensorflow/tfjs": "4.2.0",
"@tensorflow/tfjs-node": "4.2.0"
},
"dependencies": {
"@aws-sdk/client-s3": "3.301.0",
"@aws-sdk/lib-storage": "3.301.0",
"@aws-sdk/node-http-handler": "3.296.0",
"@bull-board/api": "5.0.0",
"@bull-board/fastify": "5.0.0",
"@bull-board/ui": "5.0.0",
"@discordapp/twemoji": "14.0.2",
"@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.0",
"@fastify/http-proxy": "8.4.0",
"@fastify/multipart": "7.4.2",
"@fastify/cors": "8.2.1",
"@fastify/http-proxy": "9.0.0",
"@fastify/multipart": "7.5.0",
"@fastify/static": "6.9.0",
"@fastify/view": "7.4.1",
"@nestjs/common": "9.3.9",
"@nestjs/core": "9.3.9",
"@nestjs/testing": "9.3.9",
"@nestjs/common": "9.3.12",
"@nestjs/core": "9.3.12",
"@nestjs/testing": "9.3.12",
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.38",
"@swc/core": "1.3.42",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "5.3.1",
"autwh": "0.1.0",
"aws-sdk": "2.1318.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"bull": "4.10.4",
@ -74,7 +76,7 @@
"date-fns": "2.29.3",
"deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1",
"fastify": "4.14.1",
"fastify": "4.15.0",
"feed": "4.2.2",
"file-type": "18.2.1",
"fluent-ffmpeg": "2.1.2",
@ -86,21 +88,21 @@
"ip-cidr": "3.1.0",
"is-svg": "4.3.2",
"js-yaml": "4.1.0",
"jsdom": "21.1.0",
"jsdom": "21.1.1",
"json5": "2.2.3",
"jsonld": "8.1.1",
"jsrsasign": "10.6.1",
"jsrsasign": "10.7.0",
"mfm-js": "0.23.3",
"mime-types": "2.1.35",
"misskey-js": "0.0.15",
"misskey-js": "workspace:*",
"ms": "3.0.0-canary.1",
"nested-property": "4.0.0",
"node-fetch": "3.3.0",
"node-fetch": "3.3.1",
"nodemailer": "6.9.1",
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"os-utils": "0.0.14",
"otpauth": "^9.0.2",
"otpauth": "9.1.1",
"parse5": "7.1.2",
"pg": "8.10.0",
"private-ip": "3.0.0",
@ -123,7 +125,7 @@
"sanitize-html": "2.10.0",
"seedrandom": "3.0.5",
"semver": "7.3.8",
"sharp": "0.31.3",
"sharp": "0.32.0",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
@ -131,25 +133,25 @@
"systeminformation": "5.17.12",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.3",
"tsconfig-paths": "4.1.2",
"tsc-alias": "1.8.5",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.11",
"typescript": "4.9.5",
"typescript": "5.0.2",
"ulid": "2.3.0",
"unzipper": "0.10.11",
"uuid": "9.0.0",
"vary": "1.1.2",
"web-push": "3.5.0",
"websocket": "1.0.34",
"ws": "8.12.1",
"ws": "8.13.0",
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.5.0",
"@swc/jest": "0.2.24",
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.1",
"@types/archiver": "5.3.2",
"@types/bcryptjs": "2.4.2",
"@types/bull": "4.10.0",
"@types/cbor": "6.0.0",
@ -158,13 +160,13 @@
"@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21",
"@types/ioredis": "4.28.10",
"@types/jest": "29.4.0",
"@types/jest": "29.5.0",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.0",
"@types/jsdom": "21.1.1",
"@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.5",
"@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1",
"@types/node": "18.15.0",
"@types/node": "18.15.11",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.7",
"@types/oauth": "0.9.1",
@ -176,7 +178,7 @@
"@types/ratelimiter": "3.4.4",
"@types/redis": "4.0.11",
"@types/rename": "1.0.4",
"@types/sanitize-html": "2.8.1",
"@types/sanitize-html": "2.9.0",
"@types/semver": "7.3.13",
"@types/sharp": "0.31.1",
"@types/sinonjs__fake-timers": "8.1.2",
@ -188,10 +190,11 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.54.1",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"aws-sdk-client-mock": "^2.1.1",
"cross-env": "7.0.3",
"eslint": "8.35.0",
"eslint": "8.37.0",
"eslint-plugin-import": "2.27.5",
"execa": "6.1.0",
"jest": "29.5.0",

View file

@ -12,7 +12,7 @@ import { PushNotificationService } from '@/core/PushNotificationService.js';
import * as Acct from '@/misc/acct.js';
import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js';
import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
import type { MutingsRepository, NotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
@ -24,6 +24,9 @@ export class AntennaService implements OnApplicationShutdown {
private antennas: Antenna[];
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@ -33,9 +36,6 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@ -95,54 +95,13 @@ export class AntennaService implements OnApplicationShutdown {
@bindThis
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
// 通知しない設定になっているか、自分自身の投稿なら既読にする
const read = !antenna.notify || (antenna.userId === noteUser.id);
this.antennaNotesRepository.insert({
id: this.idService.genId(),
antennaId: antenna.id,
noteId: note.id,
read: read,
});
this.redisClient.xadd(
`antennaTimeline:${antenna.id}`,
'MAXLEN', '~', '200',
`${this.idService.parse(note.id).date.getTime()}-*`,
'note', note.id);
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
if (!read) {
const mutings = await this.mutingsRepository.find({
where: {
muterId: antenna.userId,
},
select: ['muteeId'],
});
// Copy
const _note: Note = {
...note,
};
if (note.replyId != null) {
_note.reply = await this.notesRepository.findOneByOrFail({ id: note.replyId });
}
if (note.renoteId != null) {
_note.renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
}
if (isUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) {
return;
}
// 2秒経っても既読にならなかったら通知
setTimeout(async () => {
const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false });
if (unread) {
this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
antenna: { id: antenna.id, name: antenna.name },
note: await this.noteEntityService.pack(note),
});
}
}, 2000);
}
}
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている

View file

@ -8,7 +8,7 @@ import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Emoji } from '@/models/entities/Emoji.js';
import type { EmojisRepository, Note } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { Cache } from '@/misc/cache.js';
import { KVCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js';
import type { Config } from '@/config.js';
import { ReactionService } from '@/core/ReactionService.js';
@ -16,7 +16,7 @@ import { query } from '@/misc/prelude/url.js';
@Injectable()
export class CustomEmojiService {
private cache: Cache<Emoji | null>;
private cache: KVCache<Emoji | null>;
constructor(
@Inject(DI.config)
@ -34,7 +34,7 @@ export class CustomEmojiService {
private globalEventService: GlobalEventService,
private reactionService: ReactionService,
) {
this.cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
this.cache = new KVCache<Emoji | null>(1000 * 60 * 60 * 12);
}
@bindThis

View file

@ -4,6 +4,7 @@ import { v4 as uuid } from 'uuid';
import sharp from 'sharp';
import { sharpBmp } from 'sharp-read-bmp';
import { IsNull } from 'typeorm';
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
@ -36,7 +37,6 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { correctFilename } from '@/misc/correct-filename.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import type S3 from 'aws-sdk/clients/s3.js';
type AddFileArgs = {
/** User who wish to add file */
@ -81,6 +81,7 @@ type UploadFromUrlArgs = {
export class DriveService {
private registerLogger: Logger;
private downloaderLogger: Logger;
private deleteLogger: Logger;
constructor(
@Inject(DI.config)
@ -118,6 +119,7 @@ export class DriveService {
const logger = new Logger('drive', 'blue');
this.registerLogger = logger.createSubLogger('register', 'yellow');
this.downloaderLogger = logger.createSubLogger('downloader');
this.deleteLogger = logger.createSubLogger('delete');
}
/***
@ -368,7 +370,7 @@ export class DriveService {
Body: stream,
ContentType: type,
CacheControl: 'max-age=31536000, immutable',
} as S3.PutObjectRequest;
} as PutObjectCommandInput;
if (filename) params.ContentDisposition = contentDisposition(
'inline',
@ -378,21 +380,16 @@ export class DriveService {
);
if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
const s3 = this.s3Service.getS3(meta);
const upload = s3.upload(params, {
partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024,
});
await upload.promise()
await this.s3Service.upload(meta, params)
.then(
result => {
if (result) {
if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
} else {
this.registerLogger.error(`Upload Result Empty: key = ${key}, filename = ${filename}`);
} else { // AbortMultipartUploadCommandOutput
this.registerLogger.error(`Upload Result Aborted: key = ${key}, filename = ${filename}`);
}
},
})
.catch(
err => {
this.registerLogger.error(`Upload Failed: key = ${key}, filename = ${filename}`, err);
},
@ -528,10 +525,10 @@ export class DriveService {
};
const properties: {
width?: number;
height?: number;
orientation?: number;
} = {};
width?: number;
height?: number;
orientation?: number;
} = {};
if (info.width) {
properties['width'] = info.width;
@ -616,17 +613,20 @@ export class DriveService {
if (user) {
this.driveFileEntityService.pack(file, { self: true }).then(packedFile => {
// Publish driveFileCreated event
// Publish driveFileCreated event
this.globalEventService.publishMainStream(user.id, 'driveFileCreated', packedFile);
this.globalEventService.publishDriveStream(user.id, 'fileCreated', packedFile);
});
}
// 統計を更新
this.driveChart.update(file, true);
this.perUserDriveChart.update(file, true);
if (file.userHost !== null) {
this.instanceChart.updateDrive(file, true);
if (file.userHost == null) {
// ローカルユーザーのみ
this.perUserDriveChart.update(file, true);
} else {
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateDrive(file, true);
}
}
return file;
@ -692,7 +692,7 @@ export class DriveService {
@bindThis
private async deletePostProcess(file: DriveFile, isExpired = false) {
// リモートファイル期限切れ削除後は直リンクにする
// リモートファイル期限切れ削除後は直リンクにする
if (isExpired && file.userHost !== null && file.uri != null) {
this.driveFilesRepository.update(file.id, {
isLink: true,
@ -709,33 +709,36 @@ export class DriveService {
this.driveFilesRepository.delete(file.id);
}
// 統計を更新
this.driveChart.update(file, false);
this.perUserDriveChart.update(file, false);
if (file.userHost !== null) {
this.instanceChart.updateDrive(file, false);
if (file.userHost == null) {
// ローカルユーザーのみ
this.perUserDriveChart.update(file, false);
} else {
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateDrive(file, false);
}
}
}
@bindThis
public async deleteObjectStorageFile(key: string) {
const meta = await this.metaService.fetch();
const s3 = this.s3Service.getS3(meta);
try {
await s3.deleteObject({
Bucket: meta.objectStorageBucket!,
const param = {
Bucket: meta.objectStorageBucket,
Key: key,
}).promise();
} as DeleteObjectCommandInput;
await this.s3Service.delete(meta, param);
} catch (err: any) {
if (err.code === 'NoSuchKey') {
console.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err);
if (err.name === 'NoSuchKey') {
this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);
return;
} else {
throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, {
cause: err,
});
}
throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, {
cause: err,
});
}
}

View file

@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import type { InstancesRepository } from '@/models/index.js';
import type { Instance } from '@/models/entities/Instance.js';
import { Cache } from '@/misc/cache.js';
import { KVCache } from '@/misc/cache.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js';
@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js';
@Injectable()
export class FederatedInstanceService {
private cache: Cache<Instance>;
private cache: KVCache<Instance>;
constructor(
@Inject(DI.instancesRepository)
@ -18,7 +18,7 @@ export class FederatedInstanceService {
private utilityService: UtilityService,
private idService: IdService,
) {
this.cache = new Cache<Instance>(1000 * 60 * 60);
this.cache = new KVCache<Instance>(1000 * 60 * 60);
}
@bindThis

View file

@ -10,7 +10,6 @@ import type {
AdminStreamTypes,
AntennaStreamTypes,
BroadcastTypes,
ChannelStreamTypes,
DriveStreamTypes,
GroupMessagingStreamTypes,
InternalStreamTypes,
@ -82,11 +81,6 @@ export class GlobalEventService {
});
}
@bindThis
public publishChannelStream<K extends keyof ChannelStreamTypes>(channelId: Channel['id'], type: K, value?: ChannelStreamTypes[K]): void {
this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishUserListStream<K extends keyof UserListStreamTypes>(listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void {
this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);

View file

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { ulid } from 'ulid';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { genAid } from '@/misc/id/aid.js';
import { genAid, parseAid } from '@/misc/id/aid.js';
import { genMeid } from '@/misc/id/meid.js';
import { genMeidg } from '@/misc/id/meidg.js';
import { genObjectId } from '@/misc/id/object-id.js';
@ -32,4 +32,17 @@ export class IdService {
default: throw new Error('unrecognized id generation method');
}
}
@bindThis
public parse(id: string): { date: Date; } {
switch (this.method) {
case 'aid': return parseAid(id);
// TODO
//case 'meid':
//case 'meidg':
//case 'ulid':
//case 'objectid':
default: throw new Error('unrecognized id generation method');
}
}
}

View file

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import type { LocalUser } from '@/models/entities/User.js';
import type { UsersRepository } from '@/models/index.js';
import { Cache } from '@/misc/cache.js';
import { KVCache } from '@/misc/cache.js';
import { DI } from '@/di-symbols.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
import { bindThis } from '@/decorators.js';
@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
@Injectable()
export class InstanceActorService {
private cache: Cache<LocalUser>;
private cache: KVCache<LocalUser>;
constructor(
@Inject(DI.usersRepository)
@ -19,7 +19,7 @@ export class InstanceActorService {
private createSystemUserService: CreateSystemUserService,
) {
this.cache = new Cache<LocalUser>(Infinity);
this.cache = new KVCache<LocalUser>(Infinity);
}
@bindThis

View file

@ -137,7 +137,7 @@ export class MessagingService {
const activity = this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false, true), note));
this.queueService.deliver(user, activity, recipientUser.inbox);
this.queueService.deliver(user, activity, recipientUser.inbox, false);
}
return messageObj;
}
@ -159,7 +159,7 @@ export class MessagingService {
if (this.userEntityService.isLocalUser(user) && this.userEntityService.isRemoteUser(recipient)) {
const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), user));
this.queueService.deliver(user, activity, recipient.inbox);
this.queueService.deliver(user, activity, recipient.inbox, false);
}
} else if (message.groupId) {
this.globalEventService.publishGroupMessagingStream(message.groupId, 'deleted', message.id);
@ -297,10 +297,10 @@ export class MessagingService {
if (contents.length > 1) {
const collection = this.apRendererService.renderOrderedCollection(null, contents.length, undefined, undefined, contents);
this.queueService.deliver(user, this.apRendererService.addContext(collection), recipient.inbox);
this.queueService.deliver(user, this.apRendererService.addContext(collection), recipient.inbox, false);
} else {
for (const content of contents) {
this.queueService.deliver(user, this.apRendererService.addContext(content), recipient.inbox);
this.queueService.deliver(user, this.apRendererService.addContext(content), recipient.inbox, false);
}
}
}

View file

@ -1,6 +1,7 @@
import { setImmediate } from 'node:timers/promises';
import * as mfm from 'mfm-js';
import { In, DataSource } from 'typeorm';
import Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
@ -19,7 +20,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import { checkWordMute } from '@/misc/check-word-mute.js';
import type { Channel } from '@/models/entities/Channel.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { Cache } from '@/misc/cache.js';
import { KVCache } from '@/misc/cache.js';
import type { UserProfile } from '@/models/entities/UserProfile.js';
import { RelayService } from '@/core/RelayService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@ -46,7 +47,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
const mutedWordsCache = new KVCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -150,6 +151,9 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.db)
private db: DataSource,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -321,6 +325,14 @@ export class NoteCreateService implements OnApplicationShutdown {
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
if (data.channel) {
this.redisClient.xadd(
`channelTimeline:${data.channel.id}`,
'MAXLEN', '~', '1000',
`${this.idService.parse(note.id).date.getTime()}-*`,
'note', note.id);
}
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
() => { /* aborted, ignore this */ },
@ -435,15 +447,20 @@ export class NoteCreateService implements OnApplicationShutdown {
createdAt: User['createdAt'];
isBot: User['isBot'];
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
// 統計を更新
const meta = await this.metaService.fetch();
this.notesChart.update(note, true);
this.perUserNotesChart.update(user, note, true);
if (meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserNotesChart.update(user, note, true);
}
// Register host
if (this.userEntityService.isRemoteUser(user)) {
this.federatedInstanceService.fetch(user.host).then(i => {
this.federatedInstanceService.fetch(user.host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
this.instanceChart.updateNote(i.host, note, true);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, true);
}
});
}

View file

@ -16,6 +16,7 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
@Injectable()
export class NoteDeleteService {
@ -39,6 +40,7 @@ export class NoteDeleteService {
private federatedInstanceService: FederatedInstanceService,
private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService,
private metaService: MetaService,
private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart,
private instanceChart: InstanceChart,
@ -95,14 +97,19 @@ export class NoteDeleteService {
}
//#endregion
// 統計を更新
const meta = await this.metaService.fetch();
this.notesChart.update(note, false);
this.perUserNotesChart.update(user, note, false);
if (meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserNotesChart.update(user, note, false);
}
if (this.userEntityService.isRemoteUser(user)) {
this.federatedInstanceService.fetch(user.host).then(i => {
this.federatedInstanceService.fetch(user.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
this.instanceChart.updateNote(i.host, note, false);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, false);
}
});
}
}

View file

@ -8,7 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
import type { Note } from '@/models/entities/Note.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js';
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { NotificationService } from './NotificationService.js';
@ -38,9 +38,6 @@ export class NoteReadService implements OnApplicationShutdown {
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
private userEntityService: UserEntityService,
private idService: IdService,
private globalEventService: GlobalEventService,
@ -121,7 +118,6 @@ export class NoteReadService implements OnApplicationShutdown {
const readMentions: (Note | Packed<'Note'>)[] = [];
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
const readChannelNotes: (Note | Packed<'Note'>)[] = [];
const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) {
@ -133,14 +129,6 @@ export class NoteReadService implements OnApplicationShutdown {
if (note.channelId && followingChannels.has(note.channelId)) {
readChannelNotes.push(note);
}
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
for (const antenna of myAntennas) {
if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) {
readAntennaNotes.push(note);
}
}
}
}
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
@ -186,35 +174,6 @@ export class NoteReadService implements OnApplicationShutdown {
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
});
}
if (readAntennaNotes.length > 0) {
await this.antennaNotesRepository.update({
antennaId: In(myAntennas.map(a => a.id)),
noteId: In(readAntennaNotes.map(n => n.id)),
}, {
read: true,
});
// TODO: まとめてクエリしたい
for (const antenna of myAntennas) {
const count = await this.antennaNotesRepository.countBy({
antennaId: antenna.id,
read: false,
});
if (count === 0) {
this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
}
}
this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
if (!unread) {
this.globalEventService.publishMainStream(userId, 'readAllAntennas');
this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
}
});
}
}
onApplicationShutdown(signal?: string | undefined): void {

View file

@ -21,6 +21,8 @@ import { bindThis } from '@/decorators.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
const FALLBACK = '❤';
const legacies: Record<string, string> = {
'like': '👍',
'love': '❤', // ここに記述する場合は異体字セレクタを入れない
@ -147,7 +149,11 @@ export class ReactionService {
.where('id = :id', { id: note.id })
.execute();
this.perUserReactionsChart.update(user, note);
const meta = await this.metaService.fetch();
if (meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserReactionsChart.update(user, note);
}
// カスタム絵文字リアクションだったら絵文字情報も送る
const decodedReaction = this.decodeReaction(reaction);
@ -251,12 +257,6 @@ export class ReactionService {
//#endregion
}
@bindThis
public async getFallbackReaction(): Promise<string> {
const meta = await this.metaService.fetch();
return meta.useStarForReactionFallback ? '⭐' : '👍';
}
@bindThis
public convertLegacyReactions(reactions: Record<string, number>) {
const _reactions = {} as Record<string, number>;
@ -290,7 +290,7 @@ export class ReactionService {
@bindThis
public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> {
if (reaction == null) return await this.getFallbackReaction();
if (reaction == null) return FALLBACK;
reacterHost = this.utilityService.toPunyNullable(reacterHost);
@ -318,7 +318,7 @@ export class ReactionService {
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
}
return await this.getFallbackReaction();
return FALLBACK;
}
@bindThis

View file

@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
import type { LocalUser, User } from '@/models/entities/User.js';
import type { RelaysRepository, UsersRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import { Cache } from '@/misc/cache.js';
import { KVCache } from '@/misc/cache.js';
import type { Relay } from '@/models/entities/Relay.js';
import { QueueService } from '@/core/QueueService.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const;
@Injectable()
export class RelayService {
private relaysCache: Cache<Relay[]>;
private relaysCache: KVCache<Relay[]>;
constructor(
@Inject(DI.usersRepository)
@ -30,7 +30,7 @@ export class RelayService {
private createSystemUserService: CreateSystemUserService,
private apRendererService: ApRendererService,
) {
this.relaysCache = new Cache<Relay[]>(1000 * 60 * 10);
this.relaysCache = new KVCache<Relay[]>(1000 * 60 * 10);
}
@bindThis

View file

@ -2,7 +2,7 @@ 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 { KVCache } from '@/misc/cache.js';
import type { User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@ -57,8 +57,8 @@ export const DEFAULT_POLICIES: RolePolicies = {
@Injectable()
export class RoleService implements OnApplicationShutdown {
private rolesCache: Cache<Role[]>;
private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>;
private rolesCache: KVCache<Role[]>;
private roleAssignmentByUserIdCache: KVCache<RoleAssignment[]>;
public static AlreadyAssignedError = class extends Error {};
public static NotAssignedError = class extends Error {};
@ -84,8 +84,8 @@ export class RoleService implements OnApplicationShutdown {
) {
//this.onMessage = this.onMessage.bind(this);
this.rolesCache = new Cache<Role[]>(Infinity);
this.roleAssignmentByUserIdCache = new Cache<RoleAssignment[]>(Infinity);
this.rolesCache = new KVCache<Role[]>(Infinity);
this.roleAssignmentByUserIdCache = new KVCache<RoleAssignment[]>(Infinity);
this.redisSubscriber.on('message', this.onMessage);
}
@ -192,6 +192,12 @@ export class RoleService implements OnApplicationShutdown {
case 'followingMoreThanOrEq': {
return user.followingCount >= value.value;
}
case 'notesLessThanOrEq': {
return user.notesCount <= value.value;
}
case 'notesMoreThanOrEq': {
return user.notesCount >= value.value;
}
default:
return false;
}

View file

@ -1,11 +1,16 @@
import { URL } from 'node:url';
import * as http from 'node:http';
import * as https from 'node:https';
import { Inject, Injectable } from '@nestjs/common';
import S3 from 'aws-sdk/clients/s3.js';
import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
import { NodeHttpHandler, NodeHttpHandlerOptions } from '@aws-sdk/node-http-handler';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { Meta } from '@/models/entities/Meta.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/client-s3';
@Injectable()
export class S3Service {
@ -18,25 +23,47 @@ export class S3Service {
}
@bindThis
public getS3(meta: Meta) {
public getS3Client(meta: Meta): S3Client {
const u = meta.objectStorageEndpoint
? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}`
: `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`;
? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}`
: `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent
return new S3({
endpoint: meta.objectStorageEndpoint && meta.objectStorageEndpoint.length > 0
? meta.objectStorageEndpoint
: undefined,
accessKeyId: meta.objectStorageAccessKey!,
secretAccessKey: meta.objectStorageSecretKey!,
region: meta.objectStorageRegion ?? undefined,
sslEnabled: meta.objectStorageUseSSL,
s3ForcePathStyle: !meta.objectStorageEndpoint // AWS with endPoint omitted
? false
: meta.objectStorageS3ForcePathStyle,
httpOptions: {
agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
},
const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy);
const handlerOption: NodeHttpHandlerOptions = {};
if (meta.objectStorageUseSSL) {
handlerOption.httpsAgent = agent as https.Agent;
} else {
handlerOption.httpAgent = agent as http.Agent;
}
return new S3Client({
endpoint: meta.objectStorageEndpoint ? u : undefined,
credentials: (meta.objectStorageAccessKey !== null && meta.objectStorageSecretKey !== null) ? {
accessKeyId: meta.objectStorageAccessKey,
secretAccessKey: meta.objectStorageSecretKey,
} : undefined,
region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined, // 空文字列もundefinedにするため ?? は使わない
tls: meta.objectStorageUseSSL,
forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
requestHandler: new NodeHttpHandler(handlerOption),
});
}
@bindThis
public async upload(meta: Meta, input: PutObjectCommandInput) {
const client = this.getS3Client(meta);
return new Upload({
client,
params: input,
partSize: (client.config.endpoint && (await client.config.endpoint()).hostname === 'storage.googleapis.com')
? 500 * 1024 * 1024
: 8 * 1024 * 1024,
}).done();
}
@bindThis
public delete(meta: Meta, input: DeleteObjectCommandInput) {
const client = this.getS3Client(meta);
return client.send(new DeleteObjectCommand(input));
}
}

View file

@ -15,7 +15,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { bindThis } from '@/decorators.js';
import { Cache } from '@/misc/cache.js';
import { KVCache } from '@/misc/cache.js';
import { StreamMessages } from '@/server/api/stream/types.js';
@Injectable()
@ -23,7 +23,7 @@ export class UserBlockingService implements OnApplicationShutdown {
private logger: Logger;
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
private blockingsByUserIdCache: Cache<User['id'][]>;
private blockingsByUserIdCache: KVCache<User['id'][]>;
constructor(
@Inject(DI.redisSubscriber)
@ -58,7 +58,7 @@ export class UserBlockingService implements OnApplicationShutdown {
) {
this.logger = this.loggerService.getLogger('user-block');
this.blockingsByUserIdCache = new Cache<User['id'][]>(Infinity);
this.blockingsByUserIdCache = new KVCache<User['id'][]>(Infinity);
this.redisSubscriber.on('message', this.onMessage);
}

View file

@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import type { UsersRepository } from '@/models/index.js';
import { Cache } from '@/misc/cache.js';
import { KVCache } from '@/misc/cache.js';
import type { LocalUser, User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@ -11,10 +11,10 @@ import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class UserCacheService implements OnApplicationShutdown {
public userByIdCache: Cache<User>;
public localUserByNativeTokenCache: Cache<LocalUser | null>;
public localUserByIdCache: Cache<LocalUser>;
public uriPersonCache: Cache<User | null>;
public userByIdCache: KVCache<User>;
public localUserByNativeTokenCache: KVCache<LocalUser | null>;
public localUserByIdCache: KVCache<LocalUser>;
public uriPersonCache: KVCache<User | null>;
constructor(
@Inject(DI.redisSubscriber)
@ -27,10 +27,10 @@ export class UserCacheService implements OnApplicationShutdown {
) {
//this.onMessage = this.onMessage.bind(this);
this.userByIdCache = new Cache<User>(Infinity);
this.localUserByNativeTokenCache = new Cache<LocalUser | null>(Infinity);
this.localUserByIdCache = new Cache<LocalUser>(Infinity);
this.uriPersonCache = new Cache<User | null>(Infinity);
this.userByIdCache = new KVCache<User>(Infinity);
this.localUserByNativeTokenCache = new KVCache<LocalUser | null>(Infinity);
this.localUserByIdCache = new KVCache<LocalUser>(Infinity);
this.uriPersonCache = new KVCache<User | null>(Infinity);
this.redisSubscriber.on('message', this.onMessage);
}

View file

@ -17,6 +17,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { MetaService } from '@/core/MetaService.js';
import Logger from '../logger.js';
const logger = new Logger('following/create');
@ -57,6 +58,7 @@ export class UserFollowingService {
private idService: IdService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
private metaService: MetaService,
private notificationService: NotificationService,
private federatedInstanceService: FederatedInstanceService,
private webhookService: WebhookService,
@ -200,14 +202,18 @@ export class UserFollowingService {
//#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(i => {
this.federatedInstanceService.fetch(follower.host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
this.instanceChart.updateFollowing(i.host, true);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, true);
}
});
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.fetch(followee.host).then(i => {
this.federatedInstanceService.fetch(followee.host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
this.instanceChart.updateFollowers(i.host, true);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, true);
}
});
}
//#endregion
@ -320,14 +326,18 @@ export class UserFollowingService {
//#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(i => {
this.federatedInstanceService.fetch(follower.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
this.instanceChart.updateFollowing(i.host, false);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, false);
}
});
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.fetch(followee.host).then(i => {
this.federatedInstanceService.fetch(followee.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
this.instanceChart.updateFollowers(i.host, false);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, false);
}
});
}
//#endregion

View file

@ -1,20 +1,20 @@
import { Inject, Injectable } from '@nestjs/common';
import type { User } from '@/models/entities/User.js';
import type { UserKeypairsRepository } from '@/models/index.js';
import { Cache } from '@/misc/cache.js';
import { KVCache } from '@/misc/cache.js';
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class UserKeypairStoreService {
private cache: Cache<UserKeypair>;
private cache: KVCache<UserKeypair>;
constructor(
@Inject(DI.userKeypairsRepository)
private userKeypairsRepository: UserKeypairsRepository,
) {
this.cache = new Cache<UserKeypair>(Infinity);
this.cache = new KVCache<UserKeypair>(Infinity);
}
@bindThis

View file

@ -37,7 +37,7 @@ export class VideoProcessingService {
});
});
return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 280);
return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 422);
} finally {
cleanup();
}

View file

@ -3,7 +3,7 @@ import escapeRegexp from 'escape-regexp';
import { DI } from '@/di-symbols.js';
import type { MessagingMessagesRepository, NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import { Cache } from '@/misc/cache.js';
import { KVCache } from '@/misc/cache.js';
import type { UserPublickey } from '@/models/entities/UserPublickey.js';
import { UserCacheService } from '@/core/UserCacheService.js';
import type { Note } from '@/models/entities/Note.js';
@ -32,8 +32,8 @@ export type UriParseResult = {
@Injectable()
export class ApDbResolverService {
private publicKeyCache: Cache<UserPublickey | null>;
private publicKeyByUserIdCache: Cache<UserPublickey | null>;
private publicKeyCache: KVCache<UserPublickey | null>;
private publicKeyByUserIdCache: KVCache<UserPublickey | null>;
constructor(
@Inject(DI.config)
@ -54,8 +54,8 @@ export class ApDbResolverService {
private userCacheService: UserCacheService,
private apPersonService: ApPersonService,
) {
this.publicKeyCache = new Cache<UserPublickey | null>(Infinity);
this.publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
this.publicKeyCache = new KVCache<UserPublickey | null>(Infinity);
this.publicKeyByUserIdCache = new KVCache<UserPublickey | null>(Infinity);
}
@bindThis

View file

@ -30,6 +30,7 @@ import { StatusError } from '@/misc/status-error.js';
import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common';
@ -50,6 +51,7 @@ export class ApPersonService implements OnModuleInit {
private userEntityService: UserEntityService;
private idService: IdService;
private globalEventService: GlobalEventService;
private metaService: MetaService;
private federatedInstanceService: FederatedInstanceService;
private fetchInstanceMetadataService: FetchInstanceMetadataService;
private userCacheService: UserCacheService;
@ -92,6 +94,7 @@ export class ApPersonService implements OnModuleInit {
//private userEntityService: UserEntityService,
//private idService: IdService,
//private globalEventService: GlobalEventService,
//private metaService: MetaService,
//private federatedInstanceService: FederatedInstanceService,
//private fetchInstanceMetadataService: FetchInstanceMetadataService,
//private userCacheService: UserCacheService,
@ -112,6 +115,7 @@ export class ApPersonService implements OnModuleInit {
this.userEntityService = this.moduleRef.get('UserEntityService');
this.idService = this.moduleRef.get('IdService');
this.globalEventService = this.moduleRef.get('GlobalEventService');
this.metaService = this.moduleRef.get('MetaService');
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
this.userCacheService = this.moduleRef.get('UserCacheService');
@ -327,10 +331,12 @@ export class ApPersonService implements OnModuleInit {
}
// Register host
this.federatedInstanceService.fetch(host).then(i => {
this.federatedInstanceService.fetch(host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
this.instanceChart.newUser(i.host);
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.newUser(i.host);
}
});
this.usersChart.update(user!, true);

View file

@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import type { AntennasRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import type { Packed } from '@/misc/json-schema.js';
import type { Antenna } from '@/models/entities/Antenna.js';
import { bindThis } from '@/decorators.js';
@ -11,9 +11,6 @@ export class AntennaEntityService {
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
) {
@ -25,7 +22,6 @@ export class AntennaEntityService {
): Promise<Packed<'Antenna'>> {
const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src });
const hasUnreadNote = (await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false })) != null;
const userGroupJoining = antenna.userGroupJoiningId ? await this.userGroupJoiningsRepository.findOneBy({ id: antenna.userGroupJoiningId }) : null;
return {
@ -43,7 +39,7 @@ export class AntennaEntityService {
withReplies: antenna.withReplies,
withFile: antenna.withFile,
isActive: antenna.isActive,
hasUnreadNote,
hasUnreadNote: false, // TODO
};
}
}

View file

@ -1,13 +1,14 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository } from '@/models/index.js';
import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository, NotesRepository } from '@/models/index.js';
import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { Channel } from '@/models/entities/Channel.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
import { NoteEntityService } from './NoteEntityService.js';
import { In } from 'typeorm';
@Injectable()
export class ChannelEntityService {
@ -18,13 +19,19 @@ export class ChannelEntityService {
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.channelFavoritesRepository)
private channelFavoritesRepository: ChannelFavoritesRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private driveFileEntityService: DriveFileEntityService,
) {
}
@ -33,6 +40,7 @@ export class ChannelEntityService {
public async pack(
src: Channel['id'] | Channel,
me?: { id: User['id'] } | null | undefined,
detailed?: boolean,
): Promise<Packed<'Channel'>> {
const channel = typeof src === 'object' ? src : await this.channelsRepository.findOneByOrFail({ id: src });
const meId = me ? me.id : null;
@ -46,6 +54,17 @@ export class ChannelEntityService {
followeeId: channel.id,
}) : null;
const favorite = meId ? await this.channelFavoritesRepository.findOneBy({
userId: meId,
channelId: channel.id,
}) : null;
const pinnedNotes = channel.pinnedNoteIds.length > 0 ? await this.notesRepository.find({
where: {
id: In(channel.pinnedNoteIds),
},
}) : [];
return {
id: channel.id,
createdAt: channel.createdAt.toISOString(),
@ -54,13 +73,19 @@ export class ChannelEntityService {
description: channel.description,
userId: channel.userId,
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
pinnedNoteIds: channel.pinnedNoteIds,
usersCount: channel.usersCount,
notesCount: channel.notesCount,
...(me ? {
isFollowing: following != null,
isFavorited: favorite != null,
hasUnreadNote,
} : {}),
...(detailed ? {
pinnedNotes: await this.noteEntityService.packMany(pinnedNotes, me),
} : {}),
};
}
}

View file

@ -8,11 +8,11 @@ import type { Packed } from '@/misc/json-schema.js';
import type { Promiseable } from '@/misc/prelude/await-all.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import { Cache } from '@/misc/cache.js';
import { KVCache } from '@/misc/cache.js';
import type { Instance } from '@/models/entities/Instance.js';
import type { LocalUser, RemoteUser, 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, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { OnModuleInit } from '@nestjs/common';
@ -52,7 +52,7 @@ export class UserEntityService implements OnModuleInit {
private customEmojiService: CustomEmojiService;
private antennaService: AntennaService;
private roleService: RoleService;
private userInstanceCache: Cache<Instance | null>;
private userInstanceCache: KVCache<Instance | null>;
constructor(
private moduleRef: ModuleRef,
@ -114,9 +114,6 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
@Inject(DI.pagesRepository)
private pagesRepository: PagesRepository,
@ -127,7 +124,7 @@ export class UserEntityService implements OnModuleInit {
//private antennaService: AntennaService,
//private roleService: RoleService,
) {
this.userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
this.userInstanceCache = new KVCache<Instance | null>(1000 * 60 * 60 * 3);
}
onModuleInit() {
@ -259,6 +256,7 @@ export class UserEntityService implements OnModuleInit {
@bindThis
public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
/*
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({
@ -267,6 +265,8 @@ export class UserEntityService implements OnModuleInit {
}) : null;
return unread != null;
*/
return false; // TODO
}
@bindThis

View file

@ -58,14 +58,13 @@ export const DI = {
clipNotesRepository: Symbol('clipNotesRepository'),
clipFavoritesRepository: Symbol('clipFavoritesRepository'),
antennasRepository: Symbol('antennasRepository'),
antennaNotesRepository: Symbol('antennaNotesRepository'),
promoNotesRepository: Symbol('promoNotesRepository'),
promoReadsRepository: Symbol('promoReadsRepository'),
relaysRepository: Symbol('relaysRepository'),
mutedNotesRepository: Symbol('mutedNotesRepository'),
channelsRepository: Symbol('channelsRepository'),
channelFollowingsRepository: Symbol('channelFollowingsRepository'),
channelNotePiningsRepository: Symbol('channelNotePiningsRepository'),
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
registryItemsRepository: Symbol('registryItemsRepository'),
webhooksRepository: Symbol('webhooksRepository'),
adsRepository: Symbol('adsRepository'),

View file

@ -2,11 +2,11 @@ import { bindThis } from '@/decorators.js';
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
export class Cache<T> {
export class KVCache<T> {
public cache: Map<string | null, { date: number; value: T; }>;
private lifetime: number;
constructor(lifetime: Cache<never>['lifetime']) {
constructor(lifetime: KVCache<never>['lifetime']) {
this.cache = new Map();
this.lifetime = lifetime;
}
@ -87,3 +87,88 @@ export class Cache<T> {
return value;
}
}
export class Cache<T> {
private cachedAt: number | null = null;
private value: T | undefined;
private lifetime: number;
constructor(lifetime: Cache<never>['lifetime']) {
this.lifetime = lifetime;
}
@bindThis
public set(value: T): void {
this.cachedAt = Date.now();
this.value = value;
}
@bindThis
public get(): T | undefined {
if (this.cachedAt == null) return undefined;
if ((Date.now() - this.cachedAt) > this.lifetime) {
this.value = undefined;
this.cachedAt = null;
return undefined;
}
return this.value;
}
@bindThis
public delete() {
this.value = undefined;
this.cachedAt = null;
}
/**
* fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
@bindThis
public async fetch(fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
const cachedValue = this.get();
if (cachedValue !== undefined) {
if (validator) {
if (validator(cachedValue)) {
// Cache HIT
return cachedValue;
}
} else {
// Cache HIT
return cachedValue;
}
}
// Cache MISS
const value = await fetcher();
this.set(value);
return value;
}
/**
* fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
@bindThis
public async fetchMaybe(fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
const cachedValue = this.get();
if (cachedValue !== undefined) {
if (validator) {
if (validator(cachedValue)) {
// Cache HIT
return cachedValue;
}
} else {
// Cache HIT
return cachedValue;
}
}
// Cache MISS
const value = await fetcher();
if (value !== undefined) {
this.set(value);
}
return value;
}
}

View file

@ -23,3 +23,8 @@ export function genAid(date: Date): string {
counter++;
return getTime(t) + getNoise();
}
export function parseAid(id: string): { date: Date; } {
const time = parseInt(id.slice(0, 8), 36) + TIME2000;
return { date: new Date(time) };
}

View file

@ -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, RenoteMuting, 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, ClipFavorite } 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, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
@ -322,12 +322,6 @@ const $antennasRepository: Provider = {
inject: [DI.db],
};
const $antennaNotesRepository: Provider = {
provide: DI.antennaNotesRepository,
useFactory: (db: DataSource) => db.getRepository(AntennaNote),
inject: [DI.db],
};
const $promoNotesRepository: Provider = {
provide: DI.promoNotesRepository,
useFactory: (db: DataSource) => db.getRepository(PromoNote),
@ -364,9 +358,9 @@ const $channelFollowingsRepository: Provider = {
inject: [DI.db],
};
const $channelNotePiningsRepository: Provider = {
provide: DI.channelNotePiningsRepository,
useFactory: (db: DataSource) => db.getRepository(ChannelNotePining),
const $channelFavoritesRepository: Provider = {
provide: DI.channelFavoritesRepository,
useFactory: (db: DataSource) => db.getRepository(ChannelFavorite),
inject: [DI.db],
};
@ -481,14 +475,13 @@ const $roleAssignmentsRepository: Provider = {
$clipNotesRepository,
$clipFavoritesRepository,
$antennasRepository,
$antennaNotesRepository,
$promoNotesRepository,
$promoReadsRepository,
$relaysRepository,
$mutedNotesRepository,
$channelsRepository,
$channelFollowingsRepository,
$channelNotePiningsRepository,
$channelFavoritesRepository,
$registryItemsRepository,
$webhooksRepository,
$adsRepository,
@ -553,14 +546,13 @@ const $roleAssignmentsRepository: Provider = {
$clipNotesRepository,
$clipFavoritesRepository,
$antennasRepository,
$antennaNotesRepository,
$promoNotesRepository,
$promoReadsRepository,
$relaysRepository,
$mutedNotesRepository,
$channelsRepository,
$channelFollowingsRepository,
$channelNotePiningsRepository,
$channelFavoritesRepository,
$registryItemsRepository,
$webhooksRepository,
$adsRepository,

View file

@ -1,43 +0,0 @@
import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
import { id } from '../id.js';
import { Note } from './Note.js';
import { Antenna } from './Antenna.js';
@Entity()
@Index(['noteId', 'antennaId'], { unique: true })
export class AntennaNote {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
comment: 'The note ID.',
})
public noteId: Note['id'];
@ManyToOne(type => Note, {
onDelete: 'CASCADE',
})
@JoinColumn()
public note: Note | null;
@Index()
@Column({
...id(),
comment: 'The antenna ID.',
})
public antennaId: Antenna['id'];
@ManyToOne(type => Antenna, {
onDelete: 'CASCADE',
})
@JoinColumn()
public antenna: Antenna | null;
@Index()
@Column('boolean', {
default: false,
})
public read: boolean;
}

View file

@ -59,6 +59,11 @@ export class Channel {
@JoinColumn()
public banner: DriveFile | null;
@Column('varchar', {
array: true, length: 128, default: '{}',
})
public pinnedNoteIds: string[];
@Index()
@Column('integer', {
default: 0,

View file

@ -1,21 +1,24 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from '../id.js';
import { Note } from './Note.js';
import { User } from './User.js';
import { Channel } from './Channel.js';
@Entity()
@Index(['channelId', 'noteId'], { unique: true })
export class ChannelNotePining {
@Index(['userId', 'channelId'], { unique: true })
export class ChannelFavorite {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the ChannelNotePining.',
comment: 'The created date of the ChannelFavorite.',
})
public createdAt: Date;
@Index()
@Column(id())
@Column({
...id(),
})
public channelId: Channel['id'];
@ManyToOne(type => Channel, {
@ -24,12 +27,15 @@ export class ChannelNotePining {
@JoinColumn()
public channel: Channel | null;
@Column(id())
public noteId: Note['id'];
@Index()
@Column({
...id(),
})
public userId: User['id'];
@ManyToOne(type => Note, {
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public note: Note | null;
public user: User | null;
}

View file

@ -42,11 +42,6 @@ export class Meta {
})
public disableRegistration: boolean;
@Column('boolean', {
default: false,
})
public useStarForReactionFallback: boolean;
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
@ -402,6 +397,16 @@ export class Meta {
})
public enableActiveEmailValidation: boolean;
@Column('boolean', {
default: true,
})
public enableChartsForRemoteUser: boolean;
@Column('boolean', {
default: true,
})
public enableChartsForFederatedInstances: boolean;
@Column('jsonb', {
default: { },
})

View file

@ -54,6 +54,16 @@ type CondFormulaValueFollowingMoreThanOrEq = {
value: number;
};
type CondFormulaValueNotesLessThanOrEq = {
type: 'notesLessThanOrEq';
value: number;
};
type CondFormulaValueNotesMoreThanOrEq = {
type: 'notesMoreThanOrEq';
value: number;
};
export type RoleCondFormulaValue =
CondFormulaValueAnd |
CondFormulaValueOr |
@ -65,7 +75,9 @@ export type RoleCondFormulaValue =
CondFormulaValueFollowersLessThanOrEq |
CondFormulaValueFollowersMoreThanOrEq |
CondFormulaValueFollowingLessThanOrEq |
CondFormulaValueFollowingMoreThanOrEq;
CondFormulaValueFollowingMoreThanOrEq |
CondFormulaValueNotesLessThanOrEq |
CondFormulaValueNotesMoreThanOrEq;
@Entity()
export class Role {

View file

@ -4,13 +4,12 @@ import { Ad } from '@/models/entities/Ad.js';
import { Announcement } from '@/models/entities/Announcement.js';
import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js';
import { Antenna } from '@/models/entities/Antenna.js';
import { AntennaNote } from '@/models/entities/AntennaNote.js';
import { App } from '@/models/entities/App.js';
import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js';
import { AuthSession } from '@/models/entities/AuthSession.js';
import { Blocking } from '@/models/entities/Blocking.js';
import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
import { ChannelFavorite } from '@/models/entities/ChannelFavorite.js';
import { Clip } from '@/models/entities/Clip.js';
import { ClipNote } from '@/models/entities/ClipNote.js';
import { ClipFavorite } from '@/models/entities/ClipFavorite.js';
@ -77,13 +76,12 @@ export {
Announcement,
AnnouncementRead,
Antenna,
AntennaNote,
App,
AttestationChallenge,
AuthSession,
Blocking,
ChannelFollowing,
ChannelNotePining,
ChannelFavorite,
Clip,
ClipNote,
ClipFavorite,
@ -149,13 +147,12 @@ export type AdsRepository = Repository<Ad>;
export type AnnouncementsRepository = Repository<Announcement>;
export type AnnouncementReadsRepository = Repository<AnnouncementRead>;
export type AntennasRepository = Repository<Antenna>;
export type AntennaNotesRepository = Repository<AntennaNote>;
export type AppsRepository = Repository<App>;
export type AttestationChallengesRepository = Repository<AttestationChallenge>;
export type AuthSessionsRepository = Repository<AuthSession>;
export type BlockingsRepository = Repository<Blocking>;
export type ChannelFollowingsRepository = Repository<ChannelFollowing>;
export type ChannelNotePiningsRepository = Repository<ChannelNotePining>;
export type ChannelFavoritesRepository = Repository<ChannelFavorite>;
export type ClipsRepository = Repository<Clip>;
export type ClipNotesRepository = Repository<ClipNote>;
export type ClipFavoritesRepository = Repository<ClipFavorite>;

View file

@ -42,10 +42,22 @@ export const packedChannelSchema = {
type: 'boolean',
optional: true, nullable: false,
},
isFavorited: {
type: 'boolean',
optional: true, nullable: false,
},
userId: {
type: 'string',
nullable: true, optional: false,
format: 'id',
},
pinnedNoteIds: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'string',
format: 'id',
},
},
},
} as const;

View file

@ -12,13 +12,12 @@ import { Ad } from '@/models/entities/Ad.js';
import { Announcement } from '@/models/entities/Announcement.js';
import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js';
import { Antenna } from '@/models/entities/Antenna.js';
import { AntennaNote } from '@/models/entities/AntennaNote.js';
import { App } from '@/models/entities/App.js';
import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js';
import { AuthSession } from '@/models/entities/AuthSession.js';
import { Blocking } from '@/models/entities/Blocking.js';
import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
import { ChannelFavorite } from '@/models/entities/ChannelFavorite.js';
import { Clip } from '@/models/entities/Clip.js';
import { ClipNote } from '@/models/entities/ClipNote.js';
import { ClipFavorite } from '@/models/entities/ClipFavorite.js';
@ -176,14 +175,13 @@ export const entities = [
ClipNote,
ClipFavorite,
Antenna,
AntennaNote,
PromoNote,
PromoRead,
Relay,
MutedNote,
Channel,
ChannelFollowing,
ChannelNotePining,
ChannelFavorite,
RegistryItem,
Ad,
PasswordResetRequest,

View file

@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { In, LessThan } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { AntennaNotesRepository, AntennasRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js';
import type { AntennasRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
@ -29,9 +29,6 @@ export class CleanProcessorService {
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
@Inject(DI.roleAssignmentsRepository)
private roleAssignmentsRepository: RoleAssignmentsRepository,

View file

@ -7,7 +7,7 @@ import { MetaService } from '@/core/MetaService.js';
import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
import { Cache } from '@/misc/cache.js';
import { KVCache } from '@/misc/cache.js';
import type { Instance } from '@/models/entities/Instance.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js';
@ -22,7 +22,7 @@ import type { DeliverJobData } from '../types.js';
@Injectable()
export class DeliverProcessorService {
private logger: Logger;
private suspendedHostsCache: Cache<Instance[]>;
private suspendedHostsCache: KVCache<Instance[]>;
private latest: string | null;
constructor(
@ -46,7 +46,7 @@ export class DeliverProcessorService {
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
this.suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60);
this.suspendedHostsCache = new KVCache<Instance[]>(1000 * 60 * 60);
}
@bindThis
@ -88,10 +88,12 @@ export class DeliverProcessorService {
}
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
this.instanceChart.requestSent(i.host, true);
this.apRequestChart.deliverSucc();
this.federationChart.deliverd(i.host, true);
if (meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, true);
}
});
return 'Success';
@ -107,9 +109,12 @@ export class DeliverProcessorService {
});
}
this.instanceChart.requestSent(i.host, false);
this.apRequestChart.deliverFail();
this.federationChart.deliverd(i.host, false);
if (meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, false);
}
});
if (res instanceof StatusError) {

View file

@ -184,9 +184,12 @@ export class InboxProcessorService {
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
this.instanceChart.requestReceived(i.host);
this.apRequestChart.inbox();
this.federationChart.inbox(i.host);
if (meta.enableChartsForFederatedInstances) {
this.instanceChart.requestReceived(i.host);
}
});
// アクティビティを処理

View file

@ -4,7 +4,7 @@ import type { NotesRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Cache } from '@/misc/cache.js';
import { KVCache } from '@/misc/cache.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import NotesChart from '@/core/chart/charts/notes.js';
@ -118,7 +118,7 @@ export class NodeinfoServerService {
};
};
const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
const cache = new KVCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
fastify.get(nodeinfo2_1path, async (request, reply) => {
const base = await cache.fetch(null, () => nodeinfo2());

View file

@ -3,7 +3,7 @@ import { DI } from '@/di-symbols.js';
import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js';
import type { LocalUser } from '@/models/entities/User.js';
import type { AccessToken } from '@/models/entities/AccessToken.js';
import { Cache } from '@/misc/cache.js';
import { KVCache } from '@/misc/cache.js';
import type { App } from '@/models/entities/App.js';
import { UserCacheService } from '@/core/UserCacheService.js';
import isNativeToken from '@/misc/is-native-token.js';
@ -18,7 +18,7 @@ export class AuthenticationError extends Error {
@Injectable()
export class AuthenticateService {
private appCache: Cache<App>;
private appCache: KVCache<App>;
constructor(
@Inject(DI.usersRepository)
@ -32,7 +32,7 @@ export class AuthenticateService {
private userCacheService: UserCacheService,
) {
this.appCache = new Cache<App>(Infinity);
this.appCache = new KVCache<App>(Infinity);
}
@bindThis

View file

@ -95,6 +95,9 @@ import * as ep___channels_show from './endpoints/channels/show.js';
import * as ep___channels_timeline from './endpoints/channels/timeline.js';
import * as ep___channels_unfollow from './endpoints/channels/unfollow.js';
import * as ep___channels_update from './endpoints/channels/update.js';
import * as ep___channels_favorite from './endpoints/channels/favorite.js';
import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
import * as ep___charts_drive from './endpoints/charts/drive.js';
@ -443,6 +446,9 @@ const $channels_show: Provider = { provide: 'ep:channels/show', useClass: ep___c
const $channels_timeline: Provider = { provide: 'ep:channels/timeline', useClass: ep___channels_timeline.default };
const $channels_unfollow: Provider = { provide: 'ep:channels/unfollow', useClass: ep___channels_unfollow.default };
const $channels_update: Provider = { provide: 'ep:channels/update', useClass: ep___channels_update.default };
const $channels_favorite: Provider = { provide: 'ep:channels/favorite', useClass: ep___channels_favorite.default };
const $channels_unfavorite: Provider = { provide: 'ep:channels/unfavorite', useClass: ep___channels_unfavorite.default };
const $channels_myFavorites: Provider = { provide: 'ep:channels/my-favorites', useClass: ep___channels_myFavorites.default };
const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useClass: ep___charts_activeUsers.default };
const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default };
const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default };
@ -795,6 +801,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$channels_timeline,
$channels_unfollow,
$channels_update,
$channels_favorite,
$channels_unfavorite,
$channels_myFavorites,
$charts_activeUsers,
$charts_apRequest,
$charts_drive,
@ -1141,6 +1150,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$channels_timeline,
$channels_unfollow,
$channels_update,
$channels_favorite,
$channels_unfavorite,
$channels_myFavorites,
$charts_activeUsers,
$charts_apRequest,
$charts_drive,

View file

@ -95,6 +95,9 @@ import * as ep___channels_show from './endpoints/channels/show.js';
import * as ep___channels_timeline from './endpoints/channels/timeline.js';
import * as ep___channels_unfollow from './endpoints/channels/unfollow.js';
import * as ep___channels_update from './endpoints/channels/update.js';
import * as ep___channels_favorite from './endpoints/channels/favorite.js';
import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
import * as ep___charts_drive from './endpoints/charts/drive.js';
@ -441,6 +444,9 @@ const eps = [
['channels/timeline', ep___channels_timeline],
['channels/unfollow', ep___channels_unfollow],
['channels/update', ep___channels_update],
['channels/favorite', ep___channels_favorite],
['channels/unfavorite', ep___channels_unfavorite],
['channels/my-favorites', ep___channels_myFavorites],
['charts/active-users', ep___charts_activeUsers],
['charts/ap-request', ep___charts_apRequest],
['charts/drive', ep___charts_drive],

View file

@ -247,6 +247,14 @@ export const meta = {
type: 'boolean',
optional: true, nullable: false,
},
enableChartsForRemoteUser: {
type: 'boolean',
optional: false, nullable: false,
},
enableChartsForFederatedInstances: {
type: 'boolean',
optional: false, nullable: false,
},
policies: {
type: 'object',
optional: false, nullable: false,
@ -311,7 +319,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// pinnedPages: instance.pinnedPages,
// pinnedClipId: instance.pinnedClipId,
cacheRemoteFiles: instance.cacheRemoteFiles,
useStarForReactionFallback: instance.useStarForReactionFallback,
pinnedUsers: instance.pinnedUsers,
hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts,
@ -349,6 +356,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
deeplIsPro: instance.deeplIsPro,
enableIpLogging: instance.enableIpLogging,
enableActiveEmailValidation: instance.enableActiveEmailValidation,
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
};
});

View file

@ -17,7 +17,6 @@ export const paramDef = {
type: 'object',
properties: {
disableRegistration: { type: 'boolean', nullable: true },
useStarForReactionFallback: { type: 'boolean', nullable: true },
pinnedUsers: { type: 'array', nullable: true, items: {
type: 'string',
} },
@ -94,6 +93,8 @@ export const paramDef = {
objectStorageS3ForcePathStyle: { type: 'boolean' },
enableIpLogging: { type: 'boolean' },
enableActiveEmailValidation: { type: 'boolean' },
enableChartsForRemoteUser: { type: 'boolean' },
enableChartsForFederatedInstances: { type: 'boolean' },
},
required: [],
} as const;
@ -115,10 +116,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.disableRegistration = ps.disableRegistration;
}
if (typeof ps.useStarForReactionFallback === 'boolean') {
set.useStarForReactionFallback = ps.useStarForReactionFallback;
}
if (Array.isArray(ps.pinnedUsers)) {
set.pinnedUsers = ps.pinnedUsers.filter(Boolean);
}
@ -391,6 +388,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.enableActiveEmailValidation = ps.enableActiveEmailValidation;
}
if (ps.enableChartsForRemoteUser !== undefined) {
set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser;
}
if (ps.enableChartsForFederatedInstances !== undefined) {
set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances;
}
await this.metaService.update(set);
this.moderationLogService.insertModerationLog(me, 'updateMeta');
});

View file

@ -89,7 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.keywords.length === 0) {
if ((ps.keywords.length === 0) || ps.keywords[0].every(x => x === '')) {
throw new Error('invalid param');
}

View file

@ -1,10 +1,12 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { NotesRepository, AntennaNotesRepository, AntennasRepository } from '@/models/index.js';
import type { NotesRepository, AntennasRepository } from '@/models/index.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { DI } from '@/di-symbols.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { IdService } from '@/core/IdService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -50,15 +52,16 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
private idService: IdService,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private noteReadService: NoteReadService,
@ -73,9 +76,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchAntenna);
}
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.innerJoin(this.antennaNotesRepository.metadata.targetName, 'antennaNote', 'antennaNote.noteId = note.id')
const noteIdsRes = await this.redisClient.xrevrange(
`antennaTimeline:${antenna.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
'-',
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
if (noteIdsRes.length === 0) {
return [];
}
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
if (noteIds.length === 0) {
return [];
}
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
@ -86,16 +104,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
.andWhere('antennaNote.antennaId = :antennaId', { antennaId: antenna.id });
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
const notes = await query
.take(ps.limit)
.getMany();
const notes = await query.getMany();
notes.sort((a, b) => a.id > b.id ? -1 : 1);
if (notes.length > 0) {
this.noteReadService.read(me.id, notes);

View file

@ -0,0 +1,61 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['channels'],
requireCredential: true,
kind: 'write:channels',
errors: {
noSuchChannel: {
message: 'No such channel.',
code: 'NO_SUCH_CHANNEL',
id: '4938f5f3-6167-4c04-9149-6607b7542861',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
channelId: { type: 'string', format: 'misskey:id' },
},
required: ['channelId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
@Inject(DI.channelFavoritesRepository)
private channelFavoritesRepository: ChannelFavoritesRepository,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({
id: ps.channelId,
});
if (channel == null) {
throw new ApiError(meta.errors.noSuchChannel);
}
await this.channelFavoritesRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
userId: me.id,
channelId: channel.id,
});
});
}
}

Some files were not shown because too many files have changed in this diff Show more