Compare commits
12 Commits
chat
...
view-trans
Author | SHA1 | Date | |
---|---|---|---|
![]() |
76dc7affe0 | ||
![]() |
1c9d9923f4 | ||
![]() |
c8db2043b5 | ||
![]() |
f40c5f27dd | ||
![]() |
386494dd6c | ||
![]() |
f5c946b44d | ||
![]() |
5fe23d3f69 | ||
![]() |
7d86efd087 | ||
![]() |
361f810da8 | ||
![]() |
be16622de2 | ||
![]() |
f930cd7842 | ||
![]() |
f1014bc7f7 |
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
@@ -24,6 +24,9 @@ updates:
|
||||
aws-sdk:
|
||||
patterns:
|
||||
- "@aws-sdk/*"
|
||||
bull-board:
|
||||
patterns:
|
||||
- "@bull-board/*"
|
||||
nestjs:
|
||||
patterns:
|
||||
- "@nestjs/*"
|
||||
|
4
.github/workflows/storybook.yml
vendored
4
.github/workflows/storybook.yml
vendored
@@ -50,8 +50,8 @@ jobs:
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Check pnpm-lock.yaml
|
||||
run: git diff --exit-code pnpm-lock.yaml
|
||||
- name: Build dependent packages
|
||||
run: pnpm -F misskey-js -F misskey-bubble-game -F misskey-reversi build
|
||||
- name: Build misskey-js
|
||||
run: pnpm --filter misskey-js build
|
||||
- name: Build storybook
|
||||
run: pnpm --filter frontend build-storybook
|
||||
- name: Publish to Chromatic
|
||||
|
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,51 +1,23 @@
|
||||
## 2025.3.2
|
||||
|
||||
### General
|
||||
- Feat: チャットがリニューアルして復活しました
|
||||
- 既存のDM機能よりも便利で効率的になっています
|
||||
- チャットを受け付ける相手を制限できます
|
||||
- チャット機能を開放するかどうかをロールで制御できます
|
||||
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
|
||||
- Misskeyネイティブでダッシュボードを実装予定です
|
||||
-
|
||||
|
||||
### Client
|
||||
- Feat: 設定の管理が強化されました
|
||||
- 内部処理が一新され、安定性とパフォーマンスが向上しました
|
||||
- 全てのクライアント設定がエクスポート(バックアップ)/インポート対象に含まれるようになりました
|
||||
- プラグイン、テーマ、クライアントに追加されたすべてのアカウント情報も含まれるようになりました
|
||||
- 自動で設定データをサーバーにバックアップできるように
|
||||
- 設定→設定のプロファイル→自動バックアップ で有効にできます
|
||||
- 新しいデバイスからログインしたり、ブラウザから設定データが消えてしまったときに自動で復元されます(復元をスキップすることも可能)
|
||||
- 自動でバックアップされるように
|
||||
- 任意の設定項目をデバイス間で同期できるように
|
||||
- 設定項目の「...」メニュー→「デバイス間で同期」
|
||||
- 同期をオンにした際にサーバーに保存された値とローカルの値が競合する場合はどちらを優先するか選択できます
|
||||
- 任意の設定項目を初期値にリセットできるように
|
||||
- 設定項目の「...」メニュー→「初期値にリセット」
|
||||
- アカウントごとに設定値が分離される設定とそうでないクライアント設定が混在していた(かつ分離するかどうかを設定不可だった)のを、基本的に一律でクライアント全体に適用されるようにし、個別でアカウントごとに異なる設定を行えるように
|
||||
- 設定項目の「...」メニュー→「アカウントで上書き」をオンにすることで、設定値をそのアカウントでだけ適用するようにできます
|
||||
- ログアウトすると設定データもブラウザから消去されるようになりプライバシーが向上しました
|
||||
- 再度ログインすればサーバーのバックアップから設定データを復元可能です
|
||||
- エクスポートした設定データを他のサーバーでインポートして適用すること(設定の持ち運び)が可能になりました
|
||||
- Feat: 画面を重ねて表示するオプションを実装(実験的)
|
||||
- 設定 → その他 → 実験的機能 → Enable stacking router view
|
||||
- Enhance: プラグインの管理が強化されました
|
||||
- インストール/アンインストール/設定の変更時にリロード不要になりました
|
||||
- Enhance: ログアウト時、ブラウザに保存されたWebクライアントのデータを全て消去するように
|
||||
- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに
|
||||
- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように
|
||||
- Enhance: テーマ設定画面のデザインを改善
|
||||
- Enhance: 投稿フォームの設定メニューを改良
|
||||
- 投稿フォームをリセットできるように
|
||||
- 文字数カウントを復活
|
||||
- Enhance: 2段階認証時のリカバリーコードのファイル名にサーバーURLを含めるように
|
||||
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance 全体的なパフォーマンス向上
|
||||
- Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正
|
||||
- Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正
|
||||
- Fix: 連合無しモードでも外部から照会可能だった問題を修正
|
||||
- Fix: テスト用WebHookのペイロードの`emojis`パラメータが実際のものと異なる問題を修正
|
||||
|
||||
## 2025.3.1
|
||||
|
||||
|
@@ -273,6 +273,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
|
||||
query?: Record<string, string>;
|
||||
loginRequired?: boolean;
|
||||
hash?: string;
|
||||
globalCacheKey?: string;
|
||||
children?: RouteDef[];
|
||||
}
|
||||
```
|
||||
|
@@ -1013,7 +1013,6 @@ flip: "اقلب"
|
||||
lastNDays: "آخر {n} أيام"
|
||||
surrender: "ألغِ"
|
||||
postForm: "أنشئ ملاحظة"
|
||||
information: "عن"
|
||||
_delivery:
|
||||
stop: "مُعلّق"
|
||||
_initialAccountSetting:
|
||||
|
@@ -853,7 +853,6 @@ renotes: "রিনোট"
|
||||
sourceCode: "সোর্স কোড"
|
||||
flip: "উল্টান"
|
||||
postForm: "নোট লিখুন"
|
||||
information: "আপনার সম্পর্কে"
|
||||
_delivery:
|
||||
stop: "স্থগিত করা হয়েছে"
|
||||
_type:
|
||||
|
@@ -698,7 +698,6 @@ userSaysSomethingAbout: "{name} està parlant sobre \"{word}\""
|
||||
makeActive: "Activar"
|
||||
display: "Veure"
|
||||
copy: "Copiar"
|
||||
copiedToClipboard: "Copiat al porta papers"
|
||||
metrics: "Mètriques"
|
||||
overview: "Visió General"
|
||||
logs: "Registres"
|
||||
@@ -1140,7 +1139,7 @@ channelArchiveConfirmDescription: "Un Canal arxivat no apareixerà a la llista d
|
||||
thisChannelArchived: "Aquest Canal ha sigut arxivat."
|
||||
displayOfNote: "Mostrar notes"
|
||||
initialAccountSetting: "Configuració del perfil"
|
||||
youFollowing: "Segueixes "
|
||||
youFollowing: "Seguint"
|
||||
preventAiLearning: "Descartar l'ús d'aprenentatge automàtic (IA Generativa)"
|
||||
preventAiLearningDescription: "Demanar els indexadors no fer servir els texts, imatges, etc. en cap conjunt de dades per alimentar l'aprenentatge automàtic (IA Predictiva/ Generativa). Això s'aconsegueix afegint la etiqueta \"noai\" com a resposta HTML al contingut corresponent. Prevenir aquest ús totalment pot ser que no sigui aconseguit, ja que molts indexadors poden obviar aquesta etiqueta."
|
||||
options: "Opcions"
|
||||
@@ -1191,7 +1190,7 @@ pastAnnouncements: "Informes passats"
|
||||
youHaveUnreadAnnouncements: "Tens informes per llegir."
|
||||
useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey."
|
||||
replies: "Respostes"
|
||||
renotes: "Impulsos"
|
||||
renotes: "Impulsar"
|
||||
loadReplies: "Mostrar les respostes"
|
||||
loadConversation: "Mostrar la conversació "
|
||||
pinnedList: "Llista fixada"
|
||||
@@ -1327,19 +1326,7 @@ restore: "Restaurar "
|
||||
syncBetweenDevices: "Sincronització entre dispositius"
|
||||
preferenceSyncConflictTitle: "Els valors de la configuració ja existeixen al dispositiu"
|
||||
preferenceSyncConflictText: "Un element de la configuració amb sincronització activada desa els seus valors al servidor, però s'ha trobat un valor a la configuració desat al servidor per aquest element de la configuració. Quin valor us sobreescriure?"
|
||||
preferenceSyncConflictChoiceServer: "Valors de configuració del servidor"
|
||||
preferenceSyncConflictChoiceDevice: "Punts d'ajustos del dispositiu "
|
||||
preferenceSyncConflictChoiceCancel: "Cancel·lar l'activació de la sincronització "
|
||||
paste: "Pegar"
|
||||
emojiPalette: "Calaix d'emojis"
|
||||
postForm: "Formulari de publicació"
|
||||
textCount: "Nombre de caràcters "
|
||||
information: "Informació"
|
||||
_emojiPalette:
|
||||
palettes: "Calaixos d'emojis"
|
||||
enableSyncBetweenDevicesForPalettes: "Activa la sincronització dels calaixos d'emojis entre dispositius"
|
||||
paletteForMain: "Calaix d'emojis principal"
|
||||
paletteForReaction: "Calaix d'emojis per reaccions"
|
||||
_settings:
|
||||
driveBanner: "Pots gestionar i configurar el Disc, comprovar el seu ús i establir una configuració per a la càrrega d'arxius."
|
||||
pluginBanner: "Els complements poden fer-se servir per ampliar les funcionalitats del client. Els complements poden instal·lar-se, configurar-se individualment i gestionar-se."
|
||||
@@ -1357,9 +1344,6 @@ _settings:
|
||||
preferencesBanner: "Pots configurar el comportament general del client segons les teves preferències."
|
||||
appearanceBanner: "Pots configurar les preferències relacionades amb la visualització i l'aspecte del client segons el teu parer."
|
||||
soundsBanner: "Configuració dels sons que reproduirà el client."
|
||||
timelineAndNote: "Línia de temps i nota"
|
||||
makeEveryTextElementsSelectable: "Fes que tots els elements del text siguin seleccionables"
|
||||
makeEveryTextElementsSelectable_description: "L'activació pot reduir la usabilitat en determinades ocasions."
|
||||
_preferencesProfile:
|
||||
profileName: "Nom del perfil"
|
||||
profileNameDescription: "Estableix un nom que identifiqui aquest dispositiu."
|
||||
@@ -2072,7 +2056,7 @@ _theme:
|
||||
hashtag: "Etiqueta"
|
||||
mention: "Menció"
|
||||
mentionMe: "Mencions (jo)"
|
||||
renote: "Impulsar"
|
||||
renote: "Renotar"
|
||||
modalBg: "Fons del modal"
|
||||
divider: "Divisor"
|
||||
scrollbarHandle: "Maneta de la barra de desplaçament"
|
||||
@@ -2514,7 +2498,7 @@ _notification:
|
||||
follow: "Segueix-me"
|
||||
mention: "Menció"
|
||||
reply: "Respostes"
|
||||
renote: "Impulsos"
|
||||
renote: "Impulsar"
|
||||
quote: "Citar"
|
||||
reaction: "Reaccions"
|
||||
pollEnded: "Enquesta terminada"
|
||||
@@ -2524,13 +2508,12 @@ _notification:
|
||||
achievementEarned: "Assoliment desbloquejat"
|
||||
exportCompleted: "Exportació completada"
|
||||
login: "Iniciar sessió"
|
||||
createToken: "Creació de tokens d'accés "
|
||||
test: "Prova la notificació"
|
||||
app: "Notificacions d'aplicacions"
|
||||
_actions:
|
||||
followBack: "També et segueix"
|
||||
reply: "Respondre"
|
||||
renote: "Impulsar"
|
||||
renote: "Impulsos"
|
||||
_deck:
|
||||
alwaysShowMainColumn: "Mostrar sempre la columna principal"
|
||||
columnAlign: "Alinea les columnes"
|
||||
@@ -2674,8 +2657,10 @@ _externalResourceInstaller:
|
||||
checkVendorBeforeInstall: "Assegura't que qui distribueix aquest recurs és fiable abans d'instal·lar-ho."
|
||||
_plugin:
|
||||
title: "Vols instal·lar aquest afegit?"
|
||||
metaTitle: "Informació de l'afegit "
|
||||
_theme:
|
||||
title: "Vols instal·lar aquest tema?"
|
||||
metaTitle: "Informació del tema"
|
||||
_meta:
|
||||
base: "Paleta de colors base"
|
||||
_vendorInfo:
|
||||
|
@@ -169,9 +169,6 @@ addAccount: "Přidat účet"
|
||||
reloadAccountsList: "Obnovit list účtů"
|
||||
loginFailed: "Přihlášení se nezdařilo."
|
||||
showOnRemote: "Více na původním profilu"
|
||||
continueOnRemote: "Pokračujte na původní profil"
|
||||
chooseServerOnMisskeyHub: "Vyberete si server z Misskey Hubu"
|
||||
inputHostName: "Zadejte doménu"
|
||||
general: "Obecně"
|
||||
wallpaper: "Obrázek na pozadí"
|
||||
setWallpaper: "Nastavení obrázku na pozadí"
|
||||
@@ -196,7 +193,6 @@ perHour: "za hodinu"
|
||||
perDay: "za den"
|
||||
stopActivityDelivery: "Přestat zasílat aktivitu"
|
||||
blockThisInstance: "Blokovat tuto instanci"
|
||||
silenceThisInstance: "Utišit tuto instanci"
|
||||
operations: "Operace"
|
||||
software: "Software"
|
||||
version: "Verze"
|
||||
@@ -1103,7 +1099,6 @@ flip: "Otočit"
|
||||
lastNDays: "Posledních {n} dnů"
|
||||
surrender: "Zrušit"
|
||||
postForm: "Formulář pro odeslání"
|
||||
information: "Informace"
|
||||
_delivery:
|
||||
stop: "Suspendováno"
|
||||
_type:
|
||||
|
@@ -49,7 +49,7 @@ pin: "An dein Profil anheften"
|
||||
unpin: "Von deinem Profil lösen"
|
||||
copyContent: "Inhalt kopieren"
|
||||
copyLink: "Link kopieren"
|
||||
copyRemoteLink: "Remote-Link kopieren"
|
||||
copyRemoteLink: "Renote-Link kopieren"
|
||||
copyLinkRenote: "Renote-Link kopieren"
|
||||
delete: "Löschen"
|
||||
deleteAndEdit: "Löschen und Bearbeiten"
|
||||
@@ -1309,7 +1309,6 @@ availableRoles: "Verfügbare Rollen"
|
||||
federationSpecified: "Dieser Server arbeitet mit Whitelist-Föderation. Er kann nicht mit anderen als den vom Administrator angegebenen Servern interagieren."
|
||||
federationDisabled: "Föderation ist auf diesem Server deaktiviert. Es ist nicht möglich, mit Benutzern auf anderen Servern zu interagieren."
|
||||
postForm: "Notizfenster"
|
||||
information: "Über"
|
||||
_settings:
|
||||
webhook: "Webhook"
|
||||
_accountSettings:
|
||||
@@ -2525,8 +2524,10 @@ _externalResourceInstaller:
|
||||
checkVendorBeforeInstall: "Überprüfe vor Installation die Vertrauenswürdigkeit des Vertreibers."
|
||||
_plugin:
|
||||
title: "Möchtest du dieses Plugin installieren?"
|
||||
metaTitle: "Plugininformation"
|
||||
_theme:
|
||||
title: "Möchten du dieses Farbschema installieren?"
|
||||
metaTitle: "Farbschemainfo"
|
||||
_meta:
|
||||
base: "Farbschemavorlage"
|
||||
_vendorInfo:
|
||||
|
@@ -289,7 +289,6 @@ icon: "Εικονίδιο"
|
||||
replies: "Απάντηση"
|
||||
renotes: "Κοινοποίηση σημειώματος"
|
||||
postForm: "Φόρμα δημοσίευσης"
|
||||
information: "Πληροφορίες"
|
||||
_email:
|
||||
_follow:
|
||||
title: "Έχετε ένα νέο ακόλουθο"
|
||||
|
@@ -698,7 +698,6 @@ userSaysSomethingAbout: "{name} said something about \"{word}\""
|
||||
makeActive: "Activate"
|
||||
display: "Display"
|
||||
copy: "Copy"
|
||||
copiedToClipboard: "Copied to clipboard"
|
||||
metrics: "Metrics"
|
||||
overview: "Overview"
|
||||
logs: "Logs"
|
||||
@@ -1317,29 +1316,14 @@ unmarkAsSensitiveConfirm: "Do you want to remove the sensitive designation for t
|
||||
preferences: "Preferences"
|
||||
accessibility: "Accessibility"
|
||||
preferencesProfile: "Preferences profile"
|
||||
copyPreferenceId: "Copy the preference ID"
|
||||
copyPreferenceId: "Copy the proference ID"
|
||||
resetToDefaultValue: "Revert to default"
|
||||
overrideByAccount: "Override by the account"
|
||||
untitled: "Untitled"
|
||||
noName: "No name"
|
||||
skip: "Skip"
|
||||
restore: "Restore"
|
||||
syncBetweenDevices: "Sync between devices"
|
||||
preferenceSyncConflictTitle: "The configured value exists on the server."
|
||||
preferenceSyncConflictText: "The sync enabled settings will save their values to the server. However, there are existing values on the server. Which set of values would you like to overwrite?"
|
||||
preferenceSyncConflictChoiceServer: "Configured value on server"
|
||||
preferenceSyncConflictChoiceDevice: "Configured value on device"
|
||||
preferenceSyncConflictChoiceCancel: "Cancel enabling sync"
|
||||
paste: "Paste"
|
||||
emojiPalette: "Emoji palette"
|
||||
postForm: "Posting form"
|
||||
textCount: "Character count"
|
||||
information: "About"
|
||||
_emojiPalette:
|
||||
palettes: "Palette"
|
||||
enableSyncBetweenDevicesForPalettes: "Enable palette sync between devices"
|
||||
paletteForMain: "Main palette"
|
||||
paletteForReaction: "Reaction palette"
|
||||
_settings:
|
||||
driveBanner: "You can manage and configure the drive, check usage, and configure file upload settings."
|
||||
pluginBanner: "You can extend client features with plugins. You can install plugins, configure and manage individually."
|
||||
@@ -1357,9 +1341,6 @@ _settings:
|
||||
preferencesBanner: "You can configure the overall behavior of the client according to your preferences."
|
||||
appearanceBanner: "You can configure the appearance and display settings for the client according to your preferences."
|
||||
soundsBanner: "You can configure the sound settings for playback in the client."
|
||||
timelineAndNote: "Timeline and note"
|
||||
makeEveryTextElementsSelectable: "Make all text elements selectable"
|
||||
makeEveryTextElementsSelectable_description: "Enabling this may reduce usability in some situations."
|
||||
_preferencesProfile:
|
||||
profileName: "Profile name"
|
||||
profileNameDescription: "Set a name that identifies this device."
|
||||
@@ -2524,7 +2505,6 @@ _notification:
|
||||
achievementEarned: "Achievement unlocked"
|
||||
exportCompleted: "The export has been completed"
|
||||
login: "Sign In"
|
||||
createToken: "Create access token"
|
||||
test: "Notification test"
|
||||
app: "Notifications from linked apps"
|
||||
_actions:
|
||||
@@ -2552,7 +2532,6 @@ _deck:
|
||||
useSimpleUiForNonRootPages: "Use simple UI for navigated pages"
|
||||
usedAsMinWidthWhenFlexible: "Minimum width will be used for this when the \"Auto-adjust width\" option is enabled"
|
||||
flexible: "Auto-adjust width"
|
||||
enableSyncBetweenDevicesForProfiles: "Enable profile information sync between devices"
|
||||
_columns:
|
||||
main: "Main"
|
||||
widgets: "Widgets"
|
||||
@@ -2674,8 +2653,10 @@ _externalResourceInstaller:
|
||||
checkVendorBeforeInstall: "Make sure the distributor of this resource is trustworthy before installation."
|
||||
_plugin:
|
||||
title: "Do you want to install this plugin?"
|
||||
metaTitle: "Plugin information"
|
||||
_theme:
|
||||
title: "Do you want to install this theme?"
|
||||
metaTitle: "Theme information"
|
||||
_meta:
|
||||
base: "Base color scheme"
|
||||
_vendorInfo:
|
||||
@@ -2876,8 +2857,8 @@ _selfXssPrevention:
|
||||
description2: "If you do not understand exactly what you are trying to paste, %cstop working right now and close this window."
|
||||
description3: "For more information, please refer to this. {link}"
|
||||
_followRequest:
|
||||
recieved: "Received request"
|
||||
sent: "Sent request"
|
||||
recieved: "Received application"
|
||||
sent: "Sent application"
|
||||
_remoteLookupErrors:
|
||||
_federationNotAllowed:
|
||||
title: "Unable to communicate with this server"
|
||||
|
@@ -1300,7 +1300,6 @@ target: "Para"
|
||||
federationSpecified: "Este servidor opera en una federación de listas blancas. No puede interactuar con otros servidores que no sean los especificados por el administrador."
|
||||
federationDisabled: "La federación está desactivada en este servidor. No puede interactuar con usuarios de otros servidores"
|
||||
postForm: "Formulario"
|
||||
information: "Información"
|
||||
_settings:
|
||||
webhook: "Webhook"
|
||||
_accountSettings:
|
||||
@@ -2522,8 +2521,10 @@ _externalResourceInstaller:
|
||||
checkVendorBeforeInstall: "Asegúrate de que el distribuidor de este recurso es de confianza antes de proceder a la instalación."
|
||||
_plugin:
|
||||
title: "¿Quieres instalar este plugin?"
|
||||
metaTitle: "Información del plugin"
|
||||
_theme:
|
||||
title: "¿Quieres instalar este tema?"
|
||||
metaTitle: "Información del tema"
|
||||
_meta:
|
||||
base: "Esquema de color base"
|
||||
_vendorInfo:
|
||||
|
@@ -1278,7 +1278,6 @@ lockdown: "Verrouiller"
|
||||
pleaseSelectAccount: "Sélectionner un compte"
|
||||
availableRoles: "Rôles disponibles"
|
||||
postForm: "Formulaire de publication"
|
||||
information: "Informations"
|
||||
_abuseUserReport:
|
||||
forward: "Transférer"
|
||||
forwardDescription: "Transférer le signalement vers une instance distante en tant qu'anonyme."
|
||||
@@ -2296,8 +2295,10 @@ _externalResourceInstaller:
|
||||
checkVendorBeforeInstall: "Veuillez confirmer que le distributeur est fiable avant l'installation."
|
||||
_plugin:
|
||||
title: "Voulez-vous installer cette extension ?"
|
||||
metaTitle: "Informations sur l'extension"
|
||||
_theme:
|
||||
title: "Voulez-vous installer ce thème ?"
|
||||
metaTitle: "Informations sur le thème"
|
||||
_meta:
|
||||
base: "Palette de couleurs de base"
|
||||
_vendorInfo:
|
||||
|
@@ -1262,7 +1262,6 @@ modified: "Diubah"
|
||||
thereAreNChanges: "Ada {n} perubahan"
|
||||
prohibitedWordsForNameOfUser: "Kata yang dilarang untuk nama pengguna"
|
||||
postForm: "Buat catatan"
|
||||
information: "Informasi"
|
||||
_settings:
|
||||
webhook: "Webhook"
|
||||
_abuseUserReport:
|
||||
@@ -2493,8 +2492,10 @@ _externalResourceInstaller:
|
||||
checkVendorBeforeInstall: "Pastikan sumber dari sumber daya ini terpercaya sebelum melakukan pemasangan."
|
||||
_plugin:
|
||||
title: "Apakah kamu ingin memasang plugin ini?"
|
||||
metaTitle: "Informasi plugin"
|
||||
_theme:
|
||||
title: "Apakah kamu ingin memasang tema ini?"
|
||||
metaTitle: "Informasi tema"
|
||||
_meta:
|
||||
base: "Skema warna dasar"
|
||||
_vendorInfo:
|
||||
|
197
locales/index.d.ts
vendored
197
locales/index.d.ts
vendored
@@ -1174,6 +1174,10 @@ export interface Locale extends ILocale {
|
||||
* 保存しました
|
||||
*/
|
||||
"saved": string;
|
||||
/**
|
||||
* チャット
|
||||
*/
|
||||
"messaging": string;
|
||||
/**
|
||||
* アップロード
|
||||
*/
|
||||
@@ -1223,9 +1227,9 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"noMoreHistory": string;
|
||||
/**
|
||||
* チャットを始める
|
||||
* チャットを開始
|
||||
*/
|
||||
"startChat": string;
|
||||
"startMessaging": string;
|
||||
/**
|
||||
* {n}人が読みました
|
||||
*/
|
||||
@@ -1978,6 +1982,14 @@ export interface Locale extends ILocale {
|
||||
* クリップボードのテキストが長いです。テキストファイルとして添付しますか?
|
||||
*/
|
||||
"attachAsFileQuestion": string;
|
||||
/**
|
||||
* まだチャットはありません
|
||||
*/
|
||||
"noMessagesYet": string;
|
||||
/**
|
||||
* 新しいメッセージがあります
|
||||
*/
|
||||
"newMessageExists": string;
|
||||
/**
|
||||
* メッセージに添付できるファイルはひとつです
|
||||
*/
|
||||
@@ -2798,10 +2810,6 @@ export interface Locale extends ILocale {
|
||||
* コピー
|
||||
*/
|
||||
"copy": string;
|
||||
/**
|
||||
* クリップボードにコピーされました
|
||||
*/
|
||||
"copiedToClipboard": string;
|
||||
/**
|
||||
* メトリクス
|
||||
*/
|
||||
@@ -5338,151 +5346,6 @@ export interface Locale extends ILocale {
|
||||
* 投稿フォーム
|
||||
*/
|
||||
"postForm": string;
|
||||
/**
|
||||
* 文字数
|
||||
*/
|
||||
"textCount": string;
|
||||
/**
|
||||
* 情報
|
||||
*/
|
||||
"information": string;
|
||||
/**
|
||||
* チャット
|
||||
*/
|
||||
"chat": string;
|
||||
"_chat": {
|
||||
/**
|
||||
* まだメッセージはありません
|
||||
*/
|
||||
"noMessagesYet": string;
|
||||
/**
|
||||
* 新しいメッセージ
|
||||
*/
|
||||
"newMessage": string;
|
||||
/**
|
||||
* 個人チャット
|
||||
*/
|
||||
"individualChat": string;
|
||||
/**
|
||||
* 特定ユーザーとの一対一のチャットができます。
|
||||
*/
|
||||
"individualChat_description": string;
|
||||
/**
|
||||
* ルームチャット
|
||||
*/
|
||||
"roomChat": string;
|
||||
/**
|
||||
* 複数人でのチャットができます。
|
||||
* また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。
|
||||
*/
|
||||
"roomChat_description": string;
|
||||
/**
|
||||
* ルームを作成
|
||||
*/
|
||||
"createRoom": string;
|
||||
/**
|
||||
* ユーザーを招待してチャットを始めましょう
|
||||
*/
|
||||
"inviteUserToChat": string;
|
||||
/**
|
||||
* 作成したルーム
|
||||
*/
|
||||
"yourRooms": string;
|
||||
/**
|
||||
* 参加中のルーム
|
||||
*/
|
||||
"joiningRooms": string;
|
||||
/**
|
||||
* 招待
|
||||
*/
|
||||
"invitations": string;
|
||||
/**
|
||||
* 招待はありません
|
||||
*/
|
||||
"noInvitations": string;
|
||||
/**
|
||||
* 履歴
|
||||
*/
|
||||
"history": string;
|
||||
/**
|
||||
* 履歴はありません
|
||||
*/
|
||||
"noHistory": string;
|
||||
/**
|
||||
* ルームはありません
|
||||
*/
|
||||
"noRooms": string;
|
||||
/**
|
||||
* ユーザーを招待
|
||||
*/
|
||||
"inviteUser": string;
|
||||
/**
|
||||
* 参加
|
||||
*/
|
||||
"join": string;
|
||||
/**
|
||||
* 無視
|
||||
*/
|
||||
"ignore": string;
|
||||
/**
|
||||
* このユーザーとのチャットを開始できません
|
||||
*/
|
||||
"cannotChatWithTheUser": string;
|
||||
/**
|
||||
* チャットが使えない状態になっているか、相手がチャットを開放していません。
|
||||
*/
|
||||
"cannotChatWithTheUser_description": string;
|
||||
/**
|
||||
* チャットする
|
||||
*/
|
||||
"chatWithThisUser": string;
|
||||
/**
|
||||
* このユーザーはフォロワーからのみチャットを受け付けています。
|
||||
*/
|
||||
"thisUserAllowsChatOnlyFromFollowers": string;
|
||||
/**
|
||||
* このユーザーはフォローしているユーザーからのみチャットを受け付けています。
|
||||
*/
|
||||
"thisUserAllowsChatOnlyFromFollowing": string;
|
||||
/**
|
||||
* このユーザーは相互フォローのユーザーからのみチャットを受け付けています。
|
||||
*/
|
||||
"thisUserAllowsChatOnlyFromMutualFollowing": string;
|
||||
/**
|
||||
* このユーザーは誰からもチャットを受け付けていません。
|
||||
*/
|
||||
"thisUserNotAllowedChatAnyone": string;
|
||||
/**
|
||||
* チャットを許可する相手
|
||||
*/
|
||||
"chatAllowedUsers": string;
|
||||
/**
|
||||
* 自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。
|
||||
*/
|
||||
"chatAllowedUsers_note": string;
|
||||
"_chatAllowedUsers": {
|
||||
/**
|
||||
* 誰でも
|
||||
*/
|
||||
"everyone": string;
|
||||
/**
|
||||
* 自分のフォロワーのみ
|
||||
*/
|
||||
"followers": string;
|
||||
/**
|
||||
* 自分がフォローしているユーザーのみ
|
||||
*/
|
||||
"following": string;
|
||||
/**
|
||||
* 相互フォローのユーザーのみ
|
||||
*/
|
||||
"mutual": string;
|
||||
/**
|
||||
* 誰も許可しない
|
||||
*/
|
||||
"none": string;
|
||||
};
|
||||
};
|
||||
"_emojiPalette": {
|
||||
/**
|
||||
* パレット
|
||||
@@ -5570,14 +5433,6 @@ export interface Locale extends ILocale {
|
||||
* タイムラインとノート
|
||||
*/
|
||||
"timelineAndNote": string;
|
||||
/**
|
||||
* 全てのテキスト要素を選択可能にする
|
||||
*/
|
||||
"makeEveryTextElementsSelectable": string;
|
||||
/**
|
||||
* 有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。
|
||||
*/
|
||||
"makeEveryTextElementsSelectable_description": string;
|
||||
};
|
||||
"_preferencesProfile": {
|
||||
/**
|
||||
@@ -7410,10 +7265,6 @@ export interface Locale extends ILocale {
|
||||
* リストのインポートを許可
|
||||
*/
|
||||
"canImportUserLists": string;
|
||||
/**
|
||||
* チャットを許可
|
||||
*/
|
||||
"canChat": string;
|
||||
};
|
||||
"_condition": {
|
||||
/**
|
||||
@@ -8855,14 +8706,6 @@ export interface Locale extends ILocale {
|
||||
* 違反を報告する
|
||||
*/
|
||||
"write:report-abuse": string;
|
||||
/**
|
||||
* チャットを操作する
|
||||
*/
|
||||
"write:chat": string;
|
||||
/**
|
||||
* チャットを閲覧する
|
||||
*/
|
||||
"read:chat": string;
|
||||
};
|
||||
"_auth": {
|
||||
/**
|
||||
@@ -9934,10 +9777,6 @@ export interface Locale extends ILocale {
|
||||
* ログイン
|
||||
*/
|
||||
"login": string;
|
||||
/**
|
||||
* アクセストークンの作成
|
||||
*/
|
||||
"createToken": string;
|
||||
/**
|
||||
* 通知のテスト
|
||||
*/
|
||||
@@ -10500,12 +10339,20 @@ export interface Locale extends ILocale {
|
||||
* このプラグインをインストールしますか?
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
* プラグイン情報
|
||||
*/
|
||||
"metaTitle": string;
|
||||
};
|
||||
"_theme": {
|
||||
/**
|
||||
* このテーマをインストールしますか?
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
* テーマ情報
|
||||
*/
|
||||
"metaTitle": string;
|
||||
};
|
||||
"_meta": {
|
||||
/**
|
||||
|
@@ -698,7 +698,6 @@ userSaysSomethingAbout: "{name} ha Notato a riguardo di \"{word}\""
|
||||
makeActive: "Attiva"
|
||||
display: "Visualizza"
|
||||
copy: "Copia"
|
||||
copiedToClipboard: "Copiato negli appunti"
|
||||
metrics: "Statistiche"
|
||||
overview: "Anteprima"
|
||||
logs: "Log"
|
||||
@@ -974,7 +973,7 @@ check: "Verifica"
|
||||
driveCapOverrideLabel: "Modificare la capienza del Drive per questo profilo"
|
||||
driveCapOverrideCaption: "Se viene specificato meno di 0, viene annullato."
|
||||
requireAdminForView: "Per visualizzarli, è necessario aver effettuato l'accesso con un profilo amministratore."
|
||||
isSystemAccount: "Si tratta di un profilo creato e gestito automaticamente dal sistema."
|
||||
isSystemAccount: "Questi profili vengono creati e gestiti automaticamente dal sistema"
|
||||
typeToConfirm: "Digita {x} per continuare"
|
||||
deleteAccount: "Eliminazione profilo"
|
||||
document: "Documentazione"
|
||||
@@ -1324,22 +1323,7 @@ untitled: "Senza titolo"
|
||||
noName: "Senza nome"
|
||||
skip: "Salta"
|
||||
restore: "Ripristina"
|
||||
syncBetweenDevices: "Sincronizzazione tra i dispositivi"
|
||||
preferenceSyncConflictTitle: "Sul server esiste già il valore impostato"
|
||||
preferenceSyncConflictText: "Le impostazione sincronizzata salverà il valore sul server. Però, bada che esiste già un valore sul server. Quale vorresti sovrascrivere?"
|
||||
preferenceSyncConflictChoiceServer: "Valore del server"
|
||||
preferenceSyncConflictChoiceDevice: "Valore del dispositivo"
|
||||
preferenceSyncConflictChoiceCancel: "Annulla la sincronizzazione"
|
||||
paste: "Incolla"
|
||||
emojiPalette: "Tavolozza emoji"
|
||||
postForm: "Finestra di pubblicazione"
|
||||
textCount: "Il numero di caratteri"
|
||||
information: "Informazioni"
|
||||
_emojiPalette:
|
||||
palettes: "Tavolozza"
|
||||
enableSyncBetweenDevicesForPalettes: "Attiva la sincronizzazione tra dispositivi"
|
||||
paletteForMain: "Tavolozza principale"
|
||||
paletteForReaction: "Tavolozza per reazioni"
|
||||
_settings:
|
||||
driveBanner: "Permette di gestire e configurare il Drive, controllare il consumo di spazio e configurare il caricamento dei file."
|
||||
pluginBanner: "Consentono di migliorare le funzionalità. Le estensioni si possono configurare e gestire singolarmente."
|
||||
@@ -1357,9 +1341,6 @@ _settings:
|
||||
preferencesBanner: "Puoi personalizzare il comportamento del tuo dispositivo."
|
||||
appearanceBanner: "Puoi personalizzare l'aspetto nel dispositivo, in base alle tue preferenze."
|
||||
soundsBanner: "Puoi personalizzare i suoni emessi dagli eventi sul tuo dispositivo."
|
||||
timelineAndNote: "Note e Timeline"
|
||||
makeEveryTextElementsSelectable: "Imposta ogni elemento come selezionabile"
|
||||
makeEveryTextElementsSelectable_description: "Potrebbe ridurre l'usabilità in alcune situazioni."
|
||||
_preferencesProfile:
|
||||
profileName: "Nome del profilo"
|
||||
profileNameDescription: "Impostare il nome che indentifica questo dispositivo."
|
||||
@@ -2524,7 +2505,6 @@ _notification:
|
||||
achievementEarned: "Risultato raggiunto"
|
||||
exportCompleted: "Esportazione completata"
|
||||
login: "Accessi"
|
||||
createToken: "Creare un token di accesso"
|
||||
test: "Notifiche di test"
|
||||
app: "Notifiche da applicazioni"
|
||||
_actions:
|
||||
@@ -2552,7 +2532,6 @@ _deck:
|
||||
useSimpleUiForNonRootPages: "Visualizza sotto pagine con interfaccia web semplice"
|
||||
usedAsMinWidthWhenFlexible: "Se \"larghezza flessibile\" è abilitato, questa diventa la larghezza minima"
|
||||
flexible: "Larghezza flessibile"
|
||||
enableSyncBetweenDevicesForProfiles: "Abilita la sincronizzazione delle informazioni profilo tra dispositivi"
|
||||
_columns:
|
||||
main: "Principale"
|
||||
widgets: "Riquadri"
|
||||
@@ -2674,8 +2653,10 @@ _externalResourceInstaller:
|
||||
checkVendorBeforeInstall: "Prima di installare, assicurati che la fonte sia affidabile."
|
||||
_plugin:
|
||||
title: "Vuoi davvero installare questo componente aggiuntivo?"
|
||||
metaTitle: "Informazioni sul componente aggiuntivo"
|
||||
_theme:
|
||||
title: "Vuoi davvero installare questa variazione grafica?"
|
||||
metaTitle: "Informazioni sulla variazione grafica"
|
||||
_meta:
|
||||
base: "Combinazione base di colori"
|
||||
_vendorInfo:
|
||||
|
@@ -289,6 +289,7 @@ deleteAreYouSure: "「{x}」を削除しますか?"
|
||||
resetAreYouSure: "リセットしますか?"
|
||||
areYouSure: "よろしいですか?"
|
||||
saved: "保存しました"
|
||||
messaging: "チャット"
|
||||
upload: "アップロード"
|
||||
keepOriginalUploading: "オリジナル画像を保持"
|
||||
keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。"
|
||||
@@ -301,7 +302,7 @@ uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がか
|
||||
explore: "みつける"
|
||||
messageRead: "既読"
|
||||
noMoreHistory: "これより過去の履歴はありません"
|
||||
startChat: "チャットを始める"
|
||||
startMessaging: "チャットを開始"
|
||||
nUsersRead: "{n}人が読みました"
|
||||
agreeTo: "{0}に同意"
|
||||
agree: "同意する"
|
||||
@@ -490,6 +491,8 @@ noteOf: "{user}のノート"
|
||||
quoteAttached: "引用付き"
|
||||
quoteQuestion: "引用として添付しますか?"
|
||||
attachAsFileQuestion: "クリップボードのテキストが長いです。テキストファイルとして添付しますか?"
|
||||
noMessagesYet: "まだチャットはありません"
|
||||
newMessageExists: "新しいメッセージがあります"
|
||||
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
|
||||
signinRequired: "続行する前に、登録またはログインが必要です"
|
||||
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
|
||||
@@ -695,7 +698,6 @@ userSaysSomethingAbout: "{name}が「{word}」について何かを言いまし
|
||||
makeActive: "アクティブにする"
|
||||
display: "表示"
|
||||
copy: "コピー"
|
||||
copiedToClipboard: "クリップボードにコピーされました"
|
||||
metrics: "メトリクス"
|
||||
overview: "概要"
|
||||
logs: "ログ"
|
||||
@@ -1330,44 +1332,6 @@ preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル"
|
||||
paste: "ペースト"
|
||||
emojiPalette: "絵文字パレット"
|
||||
postForm: "投稿フォーム"
|
||||
textCount: "文字数"
|
||||
information: "情報"
|
||||
chat: "チャット"
|
||||
|
||||
_chat:
|
||||
noMessagesYet: "まだメッセージはありません"
|
||||
newMessage: "新しいメッセージ"
|
||||
individualChat: "個人チャット"
|
||||
individualChat_description: "特定ユーザーとの一対一のチャットができます。"
|
||||
roomChat: "ルームチャット"
|
||||
roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。"
|
||||
createRoom: "ルームを作成"
|
||||
inviteUserToChat: "ユーザーを招待してチャットを始めましょう"
|
||||
yourRooms: "作成したルーム"
|
||||
joiningRooms: "参加中のルーム"
|
||||
invitations: "招待"
|
||||
noInvitations: "招待はありません"
|
||||
history: "履歴"
|
||||
noHistory: "履歴はありません"
|
||||
noRooms: "ルームはありません"
|
||||
inviteUser: "ユーザーを招待"
|
||||
join: "参加"
|
||||
ignore: "無視"
|
||||
cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
|
||||
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
|
||||
chatWithThisUser: "チャットする"
|
||||
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。"
|
||||
thisUserAllowsChatOnlyFromFollowing: "このユーザーはフォローしているユーザーからのみチャットを受け付けています。"
|
||||
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。"
|
||||
thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。"
|
||||
chatAllowedUsers: "チャットを許可する相手"
|
||||
chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。"
|
||||
_chatAllowedUsers:
|
||||
everyone: "誰でも"
|
||||
followers: "自分のフォロワーのみ"
|
||||
following: "自分がフォローしているユーザーのみ"
|
||||
mutual: "相互フォローのユーザーのみ"
|
||||
none: "誰も許可しない"
|
||||
|
||||
_emojiPalette:
|
||||
palettes: "パレット"
|
||||
@@ -1393,8 +1357,6 @@ _settings:
|
||||
appearanceBanner: "好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。"
|
||||
soundsBanner: "クライアントで再生するサウンドの設定が行えます。"
|
||||
timelineAndNote: "タイムラインとノート"
|
||||
makeEveryTextElementsSelectable: "全てのテキスト要素を選択可能にする"
|
||||
makeEveryTextElementsSelectable_description: "有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。"
|
||||
|
||||
_preferencesProfile:
|
||||
profileName: "プロファイル名"
|
||||
@@ -1919,7 +1881,6 @@ _role:
|
||||
canImportFollowing: "フォローのインポートを許可"
|
||||
canImportMuting: "ミュートのインポートを許可"
|
||||
canImportUserLists: "リストのインポートを許可"
|
||||
canChat: "チャットを許可"
|
||||
_condition:
|
||||
roleAssignedTo: "マニュアルロールにアサイン済み"
|
||||
isLocal: "ローカルユーザー"
|
||||
@@ -2325,8 +2286,6 @@ _permissions:
|
||||
"read:clip-favorite": "クリップのいいねを見る"
|
||||
"read:federation": "連合に関する情報を取得する"
|
||||
"write:report-abuse": "違反を報告する"
|
||||
"write:chat": "チャットを操作する"
|
||||
"read:chat": "チャットを閲覧する"
|
||||
|
||||
_auth:
|
||||
shareAccessTitle: "アプリへのアクセス許可"
|
||||
@@ -2625,7 +2584,6 @@ _notification:
|
||||
achievementEarned: "実績の獲得"
|
||||
exportCompleted: "エクスポートが完了した"
|
||||
login: "ログイン"
|
||||
createToken: "アクセストークンの作成"
|
||||
test: "通知のテスト"
|
||||
app: "連携アプリからの通知"
|
||||
|
||||
@@ -2786,8 +2744,10 @@ _externalResourceInstaller:
|
||||
checkVendorBeforeInstall: "配布元が信頼できるかを確認した上でインストールしてください。"
|
||||
_plugin:
|
||||
title: "このプラグインをインストールしますか?"
|
||||
metaTitle: "プラグイン情報"
|
||||
_theme:
|
||||
title: "このテーマをインストールしますか?"
|
||||
metaTitle: "テーマ情報"
|
||||
_meta:
|
||||
base: "基本のカラースキーム"
|
||||
_vendorInfo:
|
||||
|
@@ -1312,7 +1312,6 @@ federationDisabled: "このサーバーは連合が無効化されてるで。
|
||||
confirmOnReact: "ツッコむときに確認とる"
|
||||
reactAreYouSure: "\" {emoji} \" でツッコむ?"
|
||||
postForm: "投稿フォーム"
|
||||
information: "情報"
|
||||
_settings:
|
||||
webhook: "Webhook"
|
||||
_accountSettings:
|
||||
@@ -2611,8 +2610,10 @@ _externalResourceInstaller:
|
||||
checkVendorBeforeInstall: "配ってるとこが信頼できるか確認した上でインストールしてな。"
|
||||
_plugin:
|
||||
title: "このプラグイン、インストールする?"
|
||||
metaTitle: "プラグイン情報"
|
||||
_theme:
|
||||
title: "このテーマインストールする?"
|
||||
metaTitle: "テーマ情報"
|
||||
_meta:
|
||||
base: ""
|
||||
_vendorInfo:
|
||||
|
@@ -655,7 +655,6 @@ replies: "답하기"
|
||||
renotes: "리노트"
|
||||
attach: "옇기"
|
||||
surrender: "아이예"
|
||||
information: "정보"
|
||||
_delivery:
|
||||
stop: "고만 보내예"
|
||||
_type:
|
||||
|
@@ -1330,7 +1330,6 @@ preferenceSyncConflictChoiceDevice: "장치 설정값"
|
||||
paste: "붙여넣기"
|
||||
emojiPalette: "이모지 팔레트"
|
||||
postForm: "글 입력란"
|
||||
information: "정보"
|
||||
_emojiPalette:
|
||||
palettes: "팔레트"
|
||||
paletteForMain: "메인으로 사용할 팔레트"
|
||||
@@ -2651,8 +2650,10 @@ _externalResourceInstaller:
|
||||
checkVendorBeforeInstall: "제공자를 신뢰할 수 있는 경우에만 설치하십시오."
|
||||
_plugin:
|
||||
title: "이 플러그인을 설치하시겠습니까?"
|
||||
metaTitle: "플러그인 정보"
|
||||
_theme:
|
||||
title: "이 테마를 설치하시겠습니까?"
|
||||
metaTitle: "테마 정보"
|
||||
_meta:
|
||||
base: "기본 컬러 스키마"
|
||||
_vendorInfo:
|
||||
|
@@ -394,7 +394,6 @@ searchByGoogle: "ຄົ້ນຫາ"
|
||||
file: "ໄຟລ໌"
|
||||
replies: "ຕອບກັບ"
|
||||
renotes: "Renote"
|
||||
information: "ກ່ຽວກັບ"
|
||||
_delivery:
|
||||
stop: "ໂຈະ"
|
||||
_type:
|
||||
|
@@ -462,7 +462,6 @@ loggedInAsBot: "Momenteel als bot ingelogd"
|
||||
icon: "Avatar"
|
||||
replies: "Antwoord"
|
||||
renotes: "Herdelen"
|
||||
information: "Over"
|
||||
_delivery:
|
||||
stop: "Opgeschort"
|
||||
_type:
|
||||
|
@@ -463,7 +463,6 @@ icon: "Avatar"
|
||||
replies: "Svar"
|
||||
renotes: "Renote"
|
||||
surrender: "Avbryt"
|
||||
information: "Informasjon"
|
||||
_delivery:
|
||||
stop: "Suspendert"
|
||||
_initialAccountSetting:
|
||||
|
@@ -1045,7 +1045,6 @@ lastNDays: "W ciągu ostatnich {n} dni"
|
||||
surrender: "Odrzuć"
|
||||
gameRetry: "Spróbuj ponownie"
|
||||
postForm: "Formularz tworzenia wpisu"
|
||||
information: "Informacje"
|
||||
_delivery:
|
||||
stop: "Zawieszono"
|
||||
_type:
|
||||
|
@@ -1302,7 +1302,6 @@ pleaseSelectAccount: "Selecione uma conta"
|
||||
availableRoles: "Cargos disponíveis"
|
||||
acknowledgeNotesAndEnable: "Ative após compreender as precauções."
|
||||
postForm: "Campo de postagem"
|
||||
information: "Informações"
|
||||
_settings:
|
||||
webhook: "Webhook"
|
||||
_accountSettings:
|
||||
@@ -2599,8 +2598,10 @@ _externalResourceInstaller:
|
||||
checkVendorBeforeInstall: "Tenha certeza de que o distribuidor desse recurso é confiável antes da instalação."
|
||||
_plugin:
|
||||
title: "Deseja instalar esse plugin?"
|
||||
metaTitle: "Informações do plugin"
|
||||
_theme:
|
||||
title: "Deseja instalar esse tema?"
|
||||
metaTitle: "Informações do tema"
|
||||
_meta:
|
||||
base: "Paleta de cores base"
|
||||
_vendorInfo:
|
||||
|
@@ -646,7 +646,6 @@ show: "Arată"
|
||||
icon: "Avatar"
|
||||
replies: "Răspunde"
|
||||
renotes: "Re-notează"
|
||||
information: "Despre"
|
||||
_delivery:
|
||||
stop: "Suspendat"
|
||||
_type:
|
||||
|
@@ -1182,7 +1182,6 @@ alwaysConfirmFollow: "Всегда подтверждать подписку"
|
||||
inquiry: "Связаться"
|
||||
messageToFollower: "Сообщение подписчикам"
|
||||
postForm: "Форма отправки"
|
||||
information: "Описание"
|
||||
_settings:
|
||||
webhook: "Вебхук"
|
||||
_delivery:
|
||||
|
@@ -918,7 +918,6 @@ sourceCode: "Zdrojový kód"
|
||||
flip: "Preklopiť"
|
||||
lastNDays: "Posledných {n} dní"
|
||||
postForm: "Napísať poznámku"
|
||||
information: "Informácie"
|
||||
_delivery:
|
||||
stop: "Zmrazené"
|
||||
_type:
|
||||
|
@@ -562,7 +562,6 @@ inquiry: "Kontakt"
|
||||
tryAgain: "Försök igen senare"
|
||||
signinWithPasskey: "Logga in med nyckel"
|
||||
unknownWebAuthnKey: "Okänd nyckel"
|
||||
information: "Om"
|
||||
_delivery:
|
||||
stop: "Suspenderad"
|
||||
_type:
|
||||
|
@@ -1293,7 +1293,6 @@ prohibitedWordsForNameOfUserDescription: "หากมีสตริงใด
|
||||
yourNameContainsProhibitedWords: "ชื่อของคุณนั้นมีคำที่ต้องห้าม"
|
||||
yourNameContainsProhibitedWordsDescription: "ถ้าหากคุณต้องการใช้ชื่อนี้ กรุณาติดต่อผู้ดูแลระบบของเซิร์ฟเวอร์นะค่ะ"
|
||||
postForm: "แบบฟอร์มการโพสต์"
|
||||
information: "เกี่ยวกับ"
|
||||
_settings:
|
||||
webhook: "Webhook"
|
||||
_abuseUserReport:
|
||||
@@ -2573,8 +2572,10 @@ _externalResourceInstaller:
|
||||
checkVendorBeforeInstall: "โปรดตรวจสอบให้แน่ใจว่าแหล่งแจกหน่ายมีความน่าเชื่อถือก่อนทำการติดตั้ง"
|
||||
_plugin:
|
||||
title: "ต้องการติดตั้งปลั๊กอินนี้ใช่ไหม?"
|
||||
metaTitle: "ข้อมูลส่วนเสริม"
|
||||
_theme:
|
||||
title: "ต้องการติดตั้งธีมนี้ใช่ไหม?"
|
||||
metaTitle: "ข้อมูลธีม"
|
||||
_meta:
|
||||
base: "โทนสีพื้นฐาน"
|
||||
_vendorInfo:
|
||||
|
@@ -910,7 +910,6 @@ sourceCode: "Вихідний код"
|
||||
flip: "Перевернути"
|
||||
lastNDays: "Останні {n} днів"
|
||||
postForm: "Створення нотатки"
|
||||
information: "Інформація"
|
||||
_delivery:
|
||||
stop: "Призупинено"
|
||||
_type:
|
||||
|
@@ -841,7 +841,6 @@ icon: "Avatar"
|
||||
replies: "Javob berish"
|
||||
renotes: "Qayta qayd etish"
|
||||
flip: "Teskari"
|
||||
information: "Haqida"
|
||||
_delivery:
|
||||
stop: "To'xtatilgan"
|
||||
_type:
|
||||
|
@@ -1120,7 +1120,6 @@ cwNotationRequired: "Nếu \"Ẩn nội dung\" được bật thì cần phải
|
||||
lastNDays: "{n} ngày trước"
|
||||
surrender: "Từ chối"
|
||||
postForm: "Mẫu đăng"
|
||||
information: "Giới thiệu"
|
||||
_delivery:
|
||||
stop: "Đã vô hiệu hóa"
|
||||
_type:
|
||||
|
@@ -698,7 +698,6 @@ userSaysSomethingAbout: "{name} 说了关于「{word}」的什么"
|
||||
makeActive: "启用"
|
||||
display: "显示"
|
||||
copy: "复制"
|
||||
copiedToClipboard: "已复制到剪贴板"
|
||||
metrics: "指标"
|
||||
overview: "概览"
|
||||
logs: "日志"
|
||||
@@ -747,7 +746,7 @@ confirmToUnclipAlreadyClippedNote: "本帖已包含在便签 \"{name}\" 里。
|
||||
public: "公开"
|
||||
private: "私密"
|
||||
i18nInfo: "Misskey 已经被志愿者们翻译成了各种语言。如果你也有兴趣,可以通过 {link} 帮助翻译。"
|
||||
manageAccessTokens: "管理访问令牌"
|
||||
manageAccessTokens: "管理 Access Tokens"
|
||||
accountInfo: "账户信息"
|
||||
notesCount: "帖子数量"
|
||||
repliesCount: "回复数量"
|
||||
@@ -1333,8 +1332,6 @@ preferenceSyncConflictChoiceCancel: "取消同步"
|
||||
paste: "粘贴"
|
||||
emojiPalette: "表情符号调色板"
|
||||
postForm: "投稿窗口"
|
||||
textCount: "字数"
|
||||
information: "关于"
|
||||
_emojiPalette:
|
||||
palettes: "调色板"
|
||||
enableSyncBetweenDevicesForPalettes: "启用调色板的设备间同步"
|
||||
@@ -1342,24 +1339,13 @@ _emojiPalette:
|
||||
paletteForReaction: "回应用调色板"
|
||||
_settings:
|
||||
driveBanner: "可在此管理和设置网盘、确认使用量及配置上传文件的设置。"
|
||||
pluginBanner: "使用插件可以扩展客户端的功能。可以在此安装、单独管理插件。"
|
||||
notificationsBanner: "可在此设置从服务器接收的通知的种类和范围,以及推送通知的设置。"
|
||||
api: "API"
|
||||
webhook: "Webhook"
|
||||
serviceConnection: "连接服务"
|
||||
serviceConnectionBanner: "可在此管理用于连接外部应用或服务的访问令牌及 Webhook。"
|
||||
accountData: "账户数据"
|
||||
accountDataBanner: "可在此导入或导出帐户数据的存档。"
|
||||
muteAndBlockBanner: "可在此设置隐藏内容,或限制指定用户能进行的操作。"
|
||||
accessibilityBanner: "可在此设置客户端的显示及动态效果等辅助设置。"
|
||||
privacyBanner: "可在此设置如内容可见性、可发现性、批准关注请求等账户隐私设置。"
|
||||
securityBanner: "可在此设置如密码、登入方式、验证器、Passkey 等账户安全性设置。"
|
||||
preferencesBanner: "可在此设置客户端的整体运作行为。"
|
||||
appearanceBanner: "可在此设置客户端的外观及显示方式。"
|
||||
soundsBanner: "可在此设置客户端播放的声音。"
|
||||
timelineAndNote: "时间线和帖子"
|
||||
makeEveryTextElementsSelectable: "使所有的文字均可选择"
|
||||
makeEveryTextElementsSelectable_description: "若开启,在某些情况下可能降低用户体验。"
|
||||
_preferencesProfile:
|
||||
profileName: "配置名"
|
||||
profileNameDescription: "请指定用于识别此设备的名称"
|
||||
@@ -2524,7 +2510,6 @@ _notification:
|
||||
achievementEarned: "取得的成就"
|
||||
exportCompleted: "已完成导出"
|
||||
login: "登录"
|
||||
createToken: "创建访问令牌"
|
||||
test: "测试通知"
|
||||
app: "关联应用的通知"
|
||||
_actions:
|
||||
@@ -2674,8 +2659,10 @@ _externalResourceInstaller:
|
||||
checkVendorBeforeInstall: "请在安装前确保来源可靠"
|
||||
_plugin:
|
||||
title: "要安装此插件吗?"
|
||||
metaTitle: "插件信息"
|
||||
_theme:
|
||||
title: "要安装此主题吗?"
|
||||
metaTitle: "主题信息"
|
||||
_meta:
|
||||
base: "基本配色方案"
|
||||
_vendorInfo:
|
||||
|
@@ -698,7 +698,6 @@ userSaysSomethingAbout: "{name} 說了一些關於「{word}」的話"
|
||||
makeActive: "啟用"
|
||||
display: "檢視"
|
||||
copy: "複製"
|
||||
copiedToClipboard: "已複製到剪貼簿"
|
||||
metrics: "指標"
|
||||
overview: "概覽"
|
||||
logs: "日誌"
|
||||
@@ -1333,8 +1332,6 @@ preferenceSyncConflictChoiceCancel: "取消啟用同步"
|
||||
paste: "貼上"
|
||||
emojiPalette: "表情符號調色盤"
|
||||
postForm: "發文視窗"
|
||||
textCount: "字數"
|
||||
information: "關於"
|
||||
_emojiPalette:
|
||||
palettes: "調色盤"
|
||||
enableSyncBetweenDevicesForPalettes: "啟用裝置與裝置之間的調色盤同步化"
|
||||
@@ -1358,8 +1355,6 @@ _settings:
|
||||
appearanceBanner: "您可以根據喜好設定與用戶端外觀和顯示方式相關的設定。"
|
||||
soundsBanner: "您可以調整用戶端播放的聲音設定。"
|
||||
timelineAndNote: "時間軸及貼文"
|
||||
makeEveryTextElementsSelectable: "允許選取所有文字"
|
||||
makeEveryTextElementsSelectable_description: "啟用此功能後,可能會在某些情境下降低可用性。"
|
||||
_preferencesProfile:
|
||||
profileName: "設定檔案名稱"
|
||||
profileNameDescription: "設定一個名稱來識別此裝置。"
|
||||
@@ -2524,7 +2519,6 @@ _notification:
|
||||
achievementEarned: "獲得成就"
|
||||
exportCompleted: "已完成匯出。"
|
||||
login: "登入"
|
||||
createToken: "建立存取權杖"
|
||||
test: "通知測試"
|
||||
app: "應用程式通知"
|
||||
_actions:
|
||||
@@ -2674,8 +2668,10 @@ _externalResourceInstaller:
|
||||
checkVendorBeforeInstall: "安裝前請確認提供者是可信賴的。"
|
||||
_plugin:
|
||||
title: "要安裝此外掛嘛?"
|
||||
metaTitle: "外掛資訊"
|
||||
_theme:
|
||||
title: "要安裝此佈景主題嗎?"
|
||||
metaTitle: "佈景主題資訊"
|
||||
_meta:
|
||||
base: "基本配色方案"
|
||||
_vendorInfo:
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.3.2-beta.9",
|
||||
"version": "2025.3.2-beta.1",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Chat1742203321812 {
|
||||
name = 'Chat1742203321812'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "chat_room" ("id" character varying(32) NOT NULL, "name" character varying(256) NOT NULL, "ownerId" character varying(32) NOT NULL, CONSTRAINT "PK_8aa3a52cf74c96469f0ef9fbe3e" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f0d8ad64243fa2ca2800da0dfd" ON "chat_room" ("ownerId") `);
|
||||
await queryRunner.query(`CREATE TABLE "chat_message" ("id" character varying(32) NOT NULL, "fromUserId" character varying(32) NOT NULL, "toUserId" character varying(32), "toRoomId" character varying(32), "text" character varying(4096), "uri" character varying(512), "reads" character varying(32) array NOT NULL DEFAULT '{}', "fileId" character varying(32), "reactions" character varying(1024) array NOT NULL DEFAULT '{}', CONSTRAINT "PK_3cc0d85193aade457d3077dd06b" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_79a26e7a4d9afa5e4fc05f134e" ON "chat_message" ("fromUserId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_25e097b51d7622c249452c6f75" ON "chat_message" ("toUserId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f006b8a76efd1abf9f221c175c" ON "chat_message" ("toRoomId") `);
|
||||
await queryRunner.query(`CREATE TABLE "chat_room_membership" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "roomId" character varying(32) NOT NULL, CONSTRAINT "PK_2bd59c741e571b283c048beb69a" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_d99c5279460fb77ef58c596ce5" ON "chat_room_membership" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_c25143ebab714e930aeca1c0e8" ON "chat_room_membership" ("roomId") `);
|
||||
await queryRunner.query(`ALTER TABLE "chat_room" ADD CONSTRAINT "FK_f0d8ad64243fa2ca2800da0dfd6" FOREIGN KEY ("ownerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_79a26e7a4d9afa5e4fc05f134ed" FOREIGN KEY ("fromUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_25e097b51d7622c249452c6f757" FOREIGN KEY ("toUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_f006b8a76efd1abf9f221c175ce" FOREIGN KEY ("toRoomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_fd0f9a4879430239715ad4f8e2a" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD CONSTRAINT "FK_d99c5279460fb77ef58c596ce51" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD CONSTRAINT "FK_c25143ebab714e930aeca1c0e8d" FOREIGN KEY ("roomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP CONSTRAINT "FK_c25143ebab714e930aeca1c0e8d"`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP CONSTRAINT "FK_d99c5279460fb77ef58c596ce51"`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_fd0f9a4879430239715ad4f8e2a"`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_f006b8a76efd1abf9f221c175ce"`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_25e097b51d7622c249452c6f757"`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_79a26e7a4d9afa5e4fc05f134ed"`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_room" DROP CONSTRAINT "FK_f0d8ad64243fa2ca2800da0dfd6"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_c25143ebab714e930aeca1c0e8"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_d99c5279460fb77ef58c596ce5"`);
|
||||
await queryRunner.query(`DROP TABLE "chat_room_membership"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_f006b8a76efd1abf9f221c175c"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_25e097b51d7622c249452c6f75"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_79a26e7a4d9afa5e4fc05f134e"`);
|
||||
await queryRunner.query(`DROP TABLE "chat_message"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_f0d8ad64243fa2ca2800da0dfd"`);
|
||||
await queryRunner.query(`DROP TABLE "chat_room"`);
|
||||
}
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Chat21742608337548 {
|
||||
name = 'Chat21742608337548'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "chatScope" character varying(128) NOT NULL DEFAULT 'mutual'`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_185b6b5afa707b5d36d1ce3144" ON "chat_room_membership" ("userId", "roomId") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_185b6b5afa707b5d36d1ce3144"`);
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "chatScope"`);
|
||||
}
|
||||
}
|
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Chat31742617546147 {
|
||||
name = 'Chat31742617546147'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "chat_approval" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "otherId" character varying(32) NOT NULL, CONSTRAINT "PK_fbbb95d60acf5c85388345b5f5d" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_530257863e1381a7f2f1d3282f" ON "chat_approval" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_b1d46037f23d170da5c05fdf75" ON "chat_approval" ("otherId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_12c4768a2f706fc267f2078903" ON "chat_approval" ("userId", "otherId") `);
|
||||
await queryRunner.query(`ALTER TABLE "chat_approval" ADD CONSTRAINT "FK_530257863e1381a7f2f1d3282fe" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_approval" ADD CONSTRAINT "FK_b1d46037f23d170da5c05fdf755" FOREIGN KEY ("otherId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "chat_approval" DROP CONSTRAINT "FK_b1d46037f23d170da5c05fdf755"`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_approval" DROP CONSTRAINT "FK_530257863e1381a7f2f1d3282fe"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_12c4768a2f706fc267f2078903"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_b1d46037f23d170da5c05fdf75"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_530257863e1381a7f2f1d3282f"`);
|
||||
await queryRunner.query(`DROP TABLE "chat_approval"`);
|
||||
}
|
||||
}
|
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Chat41742707840715 {
|
||||
name = 'Chat41742707840715'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "chat_room_invitation" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "roomId" character varying(32) NOT NULL, CONSTRAINT "PK_9d489521a312dd28225672de2dc" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_8552bb38e7ed038c5bdd398a38" ON "chat_room_invitation" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_5f265075b215fc390a57523b12" ON "chat_room_invitation" ("roomId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_044f2a7962b8ee5bbfaa02e8a3" ON "chat_room_invitation" ("userId", "roomId") `);
|
||||
await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD CONSTRAINT "FK_8552bb38e7ed038c5bdd398a384" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD CONSTRAINT "FK_5f265075b215fc390a57523b12a" FOREIGN KEY ("roomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP CONSTRAINT "FK_5f265075b215fc390a57523b12a"`);
|
||||
await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP CONSTRAINT "FK_8552bb38e7ed038c5bdd398a384"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_044f2a7962b8ee5bbfaa02e8a3"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_5f265075b215fc390a57523b12"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_8552bb38e7ed038c5bdd398a38"`);
|
||||
await queryRunner.query(`DROP TABLE "chat_room_invitation"`);
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Chat51742721896936 {
|
||||
name = 'Chat51742721896936'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD "ignored" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP COLUMN "ignored"`);
|
||||
}
|
||||
}
|
@@ -37,17 +37,17 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
"@swc/core-darwin-arm64": "1.11.11",
|
||||
"@swc/core-darwin-x64": "1.11.11",
|
||||
"@swc/core-darwin-arm64": "1.10.16",
|
||||
"@swc/core-darwin-x64": "1.10.16",
|
||||
"@swc/core-freebsd-x64": "1.3.11",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.11.11",
|
||||
"@swc/core-linux-arm64-gnu": "1.11.11",
|
||||
"@swc/core-linux-arm64-musl": "1.11.11",
|
||||
"@swc/core-linux-x64-gnu": "1.11.11",
|
||||
"@swc/core-linux-x64-musl": "1.11.11",
|
||||
"@swc/core-win32-arm64-msvc": "1.11.11",
|
||||
"@swc/core-win32-ia32-msvc": "1.11.11",
|
||||
"@swc/core-win32-x64-msvc": "1.11.11",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.10.16",
|
||||
"@swc/core-linux-arm64-gnu": "1.10.16",
|
||||
"@swc/core-linux-arm64-musl": "1.10.16",
|
||||
"@swc/core-linux-x64-gnu": "1.10.16",
|
||||
"@swc/core-linux-x64-musl": "1.10.16",
|
||||
"@swc/core-win32-arm64-msvc": "1.10.16",
|
||||
"@swc/core-win32-ia32-msvc": "1.10.16",
|
||||
"@swc/core-win32-x64-msvc": "1.10.16",
|
||||
"@tensorflow/tfjs": "4.22.0",
|
||||
"@tensorflow/tfjs-node": "4.22.0",
|
||||
"bufferutil": "4.0.9",
|
||||
@@ -67,23 +67,26 @@
|
||||
"utf-8-validate": "6.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.772.0",
|
||||
"@aws-sdk/lib-storage": "3.772.0",
|
||||
"@aws-sdk/client-s3": "3.749.0",
|
||||
"@aws-sdk/lib-storage": "3.749.0",
|
||||
"@bull-board/api": "6.7.7",
|
||||
"@bull-board/fastify": "6.7.7",
|
||||
"@bull-board/ui": "6.7.7",
|
||||
"@discordapp/twemoji": "15.1.0",
|
||||
"@fastify/accepts": "5.0.2",
|
||||
"@fastify/cookie": "11.0.2",
|
||||
"@fastify/cors": "10.1.0",
|
||||
"@fastify/cors": "10.0.2",
|
||||
"@fastify/express": "4.0.2",
|
||||
"@fastify/http-proxy": "10.0.2",
|
||||
"@fastify/multipart": "9.0.3",
|
||||
"@fastify/static": "8.1.1",
|
||||
"@fastify/static": "8.1.0",
|
||||
"@fastify/view": "10.0.2",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.2.0",
|
||||
"@napi-rs/canvas": "0.1.68",
|
||||
"@nestjs/common": "11.0.12",
|
||||
"@nestjs/core": "11.0.12",
|
||||
"@nestjs/testing": "11.0.12",
|
||||
"@napi-rs/canvas": "0.1.67",
|
||||
"@nestjs/common": "11.0.9",
|
||||
"@nestjs/core": "11.0.9",
|
||||
"@nestjs/testing": "11.0.9",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "8.55.0",
|
||||
"@sentry/profiling-node": "8.55.0",
|
||||
@@ -91,7 +94,7 @@
|
||||
"@sinonjs/fake-timers": "11.3.1",
|
||||
"@smithy/node-http-handler": "2.5.0",
|
||||
"@swc/cli": "0.6.0",
|
||||
"@swc/core": "1.11.11",
|
||||
"@swc/core": "1.10.16",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.17.1",
|
||||
@@ -100,7 +103,7 @@
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.3",
|
||||
"bullmq": "5.44.1",
|
||||
"bullmq": "5.41.1",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.2",
|
||||
"chalk": "5.4.1",
|
||||
@@ -122,7 +125,7 @@
|
||||
"hpagent": "1.2.0",
|
||||
"htmlescape": "1.1.1",
|
||||
"http-link-header": "1.1.3",
|
||||
"ioredis": "5.6.0",
|
||||
"ioredis": "5.5.0",
|
||||
"ip-cidr": "4.0.2",
|
||||
"ipaddr.js": "2.2.0",
|
||||
"is-svg": "5.1.0",
|
||||
@@ -131,26 +134,26 @@
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.3.3",
|
||||
"jsrsasign": "11.1.0",
|
||||
"juice": "11.0.1",
|
||||
"meilisearch": "0.49.0",
|
||||
"juice": "11.0.0",
|
||||
"meilisearch": "0.48.2",
|
||||
"mfm-js": "0.24.0",
|
||||
"microformats-parser": "2.0.2",
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"nanoid": "5.1.5",
|
||||
"nanoid": "5.1.0",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.10.0",
|
||||
"nsfwjs": "4.2.0",
|
||||
"oauth": "0.10.2",
|
||||
"oauth": "0.10.0",
|
||||
"oauth2orize": "1.12.0",
|
||||
"oauth2orize-pkce": "0.1.2",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.3.6",
|
||||
"parse5": "7.2.1",
|
||||
"pg": "8.14.1",
|
||||
"pg": "8.13.3",
|
||||
"pkce-challenge": "4.1.0",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
@@ -163,8 +166,8 @@
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rename": "1.0.4",
|
||||
"rss-parser": "3.13.0",
|
||||
"rxjs": "7.8.2",
|
||||
"sanitize-html": "2.15.0",
|
||||
"rxjs": "7.8.1",
|
||||
"sanitize-html": "2.14.0",
|
||||
"secure-json-parse": "3.0.2",
|
||||
"sharp": "0.33.5",
|
||||
"slacc": "0.0.10",
|
||||
@@ -173,14 +176,14 @@
|
||||
"systeminformation": "5.25.11",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.3",
|
||||
"tsc-alias": "1.8.11",
|
||||
"tsc-alias": "1.8.10",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.21",
|
||||
"typescript": "5.8.2",
|
||||
"ulid": "2.4.0",
|
||||
"typeorm": "0.3.20",
|
||||
"typescript": "5.7.3",
|
||||
"ulid": "2.3.0",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.7",
|
||||
"ws": "8.18.1",
|
||||
"ws": "8.18.0",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -204,7 +207,7 @@
|
||||
"@types/jsrsasign": "10.5.15",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "22.13.10",
|
||||
"@types/node": "22.13.4",
|
||||
"@types/nodemailer": "6.4.17",
|
||||
"@types/oauth": "0.9.6",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
@@ -223,9 +226,9 @@
|
||||
"@types/tmp": "0.2.6",
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.4",
|
||||
"@types/ws": "8.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.27.0",
|
||||
"@typescript-eslint/parser": "8.27.0",
|
||||
"@types/ws": "8.5.14",
|
||||
"@typescript-eslint/eslint-plugin": "8.24.0",
|
||||
"@typescript-eslint/parser": "8.24.0",
|
||||
"aws-sdk-client-mock": "4.1.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
|
@@ -1,602 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { ChatApprovalsRepository, ChatMessagesRepository, ChatRoomInvitationsRepository, ChatRoomMembershipsRepository, ChatRoomsRepository, MiChatMessage, MiChatRoom, MiChatRoomMembership, MiDriveFile, MiUser, MutingsRepository, UsersRepository } from '@/models/_.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
const MAX_ROOM_MEMBERS = 30;
|
||||
|
||||
@Injectable()
|
||||
export class ChatService {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.chatMessagesRepository)
|
||||
private chatMessagesRepository: ChatMessagesRepository,
|
||||
|
||||
@Inject(DI.chatApprovalsRepository)
|
||||
private chatApprovalsRepository: ChatApprovalsRepository,
|
||||
|
||||
@Inject(DI.chatRoomsRepository)
|
||||
private chatRoomsRepository: ChatRoomsRepository,
|
||||
|
||||
@Inject(DI.chatRoomInvitationsRepository)
|
||||
private chatRoomInvitationsRepository: ChatRoomInvitationsRepository,
|
||||
|
||||
@Inject(DI.chatRoomMembershipsRepository)
|
||||
private chatRoomMembershipsRepository: ChatRoomMembershipsRepository,
|
||||
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private chatEntityService: ChatEntityService,
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private apRendererService: ApRendererService,
|
||||
private queueService: QueueService,
|
||||
private pushNotificationService: PushNotificationService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
private queryService: QueryService,
|
||||
private roleService: RoleService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createMessageToUser(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toUser: MiUser, params: {
|
||||
text?: string | null;
|
||||
file?: MiDriveFile | null;
|
||||
uri?: string | null;
|
||||
}): Promise<Packed<'ChatMessageLite'>> {
|
||||
if (fromUser.id === toUser.id) {
|
||||
throw new Error('yourself');
|
||||
}
|
||||
|
||||
const approvals = await this.chatApprovalsRepository.createQueryBuilder('approval')
|
||||
.where(new Brackets(qb => { // 自分が相手を許可しているか
|
||||
qb.where('approval.userId = :fromUserId', { fromUserId: fromUser.id })
|
||||
.andWhere('approval.otherId = :toUserId', { toUserId: toUser.id });
|
||||
}))
|
||||
.orWhere(new Brackets(qb => { // 相手が自分を許可しているか
|
||||
qb.where('approval.userId = :toUserId', { toUserId: toUser.id })
|
||||
.andWhere('approval.otherId = :fromUserId', { fromUserId: fromUser.id });
|
||||
}))
|
||||
.take(2)
|
||||
.getMany();
|
||||
|
||||
const otherApprovedMe = approvals.some(approval => approval.userId === toUser.id);
|
||||
const iApprovedOther = approvals.some(approval => approval.userId === fromUser.id);
|
||||
|
||||
if (!otherApprovedMe) {
|
||||
if (toUser.chatScope === 'none') {
|
||||
throw new Error('recipient is cannot chat (none)');
|
||||
} else if (toUser.chatScope === 'followers') {
|
||||
const isFollower = await this.userFollowingService.isFollowing(fromUser.id, toUser.id);
|
||||
if (!isFollower) {
|
||||
throw new Error('recipient is cannot chat (followers)');
|
||||
}
|
||||
} else if (toUser.chatScope === 'following') {
|
||||
const isFollowing = await this.userFollowingService.isFollowing(toUser.id, fromUser.id);
|
||||
if (!isFollowing) {
|
||||
throw new Error('recipient is cannot chat (following)');
|
||||
}
|
||||
} else if (toUser.chatScope === 'mutual') {
|
||||
const isMutual = await this.userFollowingService.isMutual(fromUser.id, toUser.id);
|
||||
if (!isMutual) {
|
||||
throw new Error('recipient is cannot chat (mutual)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await this.roleService.getUserPolicies(toUser.id)).canChat) {
|
||||
throw new Error('recipient is cannot chat (policy)');
|
||||
}
|
||||
|
||||
const blocked = await this.userBlockingService.checkBlocked(toUser.id, fromUser.id);
|
||||
if (blocked) {
|
||||
throw new Error('blocked');
|
||||
}
|
||||
|
||||
const message = {
|
||||
id: this.idService.gen(),
|
||||
fromUserId: fromUser.id,
|
||||
toUserId: toUser.id,
|
||||
text: params.text ? params.text.trim() : null,
|
||||
fileId: params.file ? params.file.id : null,
|
||||
reads: [],
|
||||
uri: params.uri ?? null,
|
||||
} satisfies Partial<MiChatMessage>;
|
||||
|
||||
const inserted = await this.chatMessagesRepository.insertOne(message);
|
||||
|
||||
// 相手を許可しておく
|
||||
if (!iApprovedOther) {
|
||||
this.chatApprovalsRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
userId: fromUser.id,
|
||||
otherId: toUser.id,
|
||||
});
|
||||
}
|
||||
|
||||
const packedMessage = await this.chatEntityService.packMessageLite(inserted);
|
||||
|
||||
if (this.userEntityService.isLocalUser(toUser)) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.set(`newUserChatMessageExists:${toUser.id}:${fromUser.id}`, message.id);
|
||||
redisPipeline.sadd(`newChatMessagesExists:${toUser.id}`, `user:${fromUser.id}`);
|
||||
redisPipeline.exec();
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(fromUser)) {
|
||||
// 自分のストリーム
|
||||
this.globalEventService.publishChatUserStream(fromUser.id, toUser.id, 'message', packedMessage);
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(toUser)) {
|
||||
// 相手のストリーム
|
||||
this.globalEventService.publishChatUserStream(toUser.id, fromUser.id, 'message', packedMessage);
|
||||
}
|
||||
|
||||
// 3秒経っても既読にならなかったらイベント発行
|
||||
if (this.userEntityService.isLocalUser(toUser)) {
|
||||
setTimeout(async () => {
|
||||
const marker = await this.redisClient.get(`newUserChatMessageExists:${toUser.id}:${fromUser.id}`);
|
||||
|
||||
if (marker == null) return; // 既読
|
||||
|
||||
const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted, toUser);
|
||||
this.globalEventService.publishMainStream(toUser.id, 'newChatMessage', packedMessageForTo);
|
||||
this.pushNotificationService.pushNotification(toUser.id, 'newChatMessage', packedMessageForTo);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
return packedMessage;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createMessageToRoom(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toRoom: MiChatRoom, params: {
|
||||
text?: string | null;
|
||||
file?: MiDriveFile | null;
|
||||
uri?: string | null;
|
||||
}): Promise<Packed<'ChatMessageLite'>> {
|
||||
const memberships = await this.chatRoomMembershipsRepository.findBy({ roomId: toRoom.id });
|
||||
|
||||
if (toRoom.ownerId !== fromUser.id && !memberships.some(member => member.userId === fromUser.id)) {
|
||||
throw new Error('you are not a member of the room');
|
||||
}
|
||||
|
||||
const message = {
|
||||
id: this.idService.gen(),
|
||||
fromUserId: fromUser.id,
|
||||
toRoomId: toRoom.id,
|
||||
text: params.text ? params.text.trim() : null,
|
||||
fileId: params.file ? params.file.id : null,
|
||||
reads: [],
|
||||
uri: params.uri ?? null,
|
||||
} satisfies Partial<MiChatMessage>;
|
||||
|
||||
const inserted = await this.chatMessagesRepository.insertOne(message);
|
||||
|
||||
const packedMessage = await this.chatEntityService.packMessageLiteForRoom(inserted);
|
||||
|
||||
this.globalEventService.publishChatRoomStream(toRoom.id, 'message', packedMessage);
|
||||
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
for (const membership of memberships) {
|
||||
redisPipeline.set(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`, message.id);
|
||||
redisPipeline.sadd(`newChatMessagesExists:${membership.userId}`, `room:${toRoom.id}`);
|
||||
}
|
||||
redisPipeline.exec();
|
||||
|
||||
// 3秒経っても既読にならなかったらイベント発行
|
||||
setTimeout(async () => {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
for (const membership of memberships) {
|
||||
redisPipeline.get(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`);
|
||||
}
|
||||
const markers = await redisPipeline.exec();
|
||||
|
||||
if (markers.every(marker => marker[1] == null)) return;
|
||||
|
||||
const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted);
|
||||
|
||||
for (let i = 0; i < memberships.length; i++) {
|
||||
const marker = markers[i][1];
|
||||
if (marker == null) continue;
|
||||
|
||||
this.globalEventService.publishMainStream(memberships[i].userId, 'newChatMessage', packedMessageForTo);
|
||||
this.pushNotificationService.pushNotification(memberships[i].userId, 'newChatMessage', packedMessageForTo);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
return packedMessage;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async readUserChatMessage(
|
||||
readerId: MiUser['id'],
|
||||
senderId: MiUser['id'],
|
||||
): Promise<void> {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.del(`newUserChatMessageExists:${readerId}:${senderId}`);
|
||||
redisPipeline.srem(`newChatMessagesExists:${readerId}`, `user:${senderId}`);
|
||||
await redisPipeline.exec();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async readRoomChatMessage(
|
||||
readerId: MiUser['id'],
|
||||
roomId: MiChatRoom['id'],
|
||||
): Promise<void> {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.del(`newRoomChatMessageExists:${readerId}:${roomId}`);
|
||||
redisPipeline.srem(`newChatMessagesExists:${readerId}`, `room:${roomId}`);
|
||||
await redisPipeline.exec();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public findMessageById(messageId: MiChatMessage['id']) {
|
||||
return this.chatMessagesRepository.findOneBy({ id: messageId });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public findMyMessageById(userId: MiUser['id'], messageId: MiChatMessage['id']) {
|
||||
return this.chatMessagesRepository.findOneBy({ id: messageId, fromUserId: userId });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deleteMessage(message: MiChatMessage) {
|
||||
await this.chatMessagesRepository.delete(message.id);
|
||||
|
||||
if (message.toUserId) {
|
||||
const [fromUser, toUser] = await Promise.all([
|
||||
this.usersRepository.findOneByOrFail({ id: message.fromUserId }),
|
||||
this.usersRepository.findOneByOrFail({ id: message.toUserId }),
|
||||
]);
|
||||
|
||||
if (this.userEntityService.isLocalUser(fromUser)) this.globalEventService.publishChatUserStream(message.fromUserId, message.toUserId, 'deleted', message.id);
|
||||
if (this.userEntityService.isLocalUser(toUser)) this.globalEventService.publishChatUserStream(message.toUserId, message.fromUserId, 'deleted', message.id);
|
||||
|
||||
if (this.userEntityService.isLocalUser(fromUser) && this.userEntityService.isRemoteUser(toUser)) {
|
||||
//const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), fromUser));
|
||||
//this.queueService.deliver(fromUser, activity, toUser.inbox);
|
||||
}
|
||||
} else if (message.toRoomId) {
|
||||
this.globalEventService.publishChatRoomStream(message.toRoomId, 'deleted', message.id);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async userTimeline(meId: MiUser['id'], otherId: MiUser['id'], limit: number, sinceId?: MiChatMessage['id'] | null, untilId?: MiChatMessage['id'] | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), sinceId, untilId)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where(new Brackets(qb => {
|
||||
qb
|
||||
.where('message.fromUserId = :meId')
|
||||
.andWhere('message.toUserId = :otherId');
|
||||
}))
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('message.fromUserId = :otherId')
|
||||
.andWhere('message.toUserId = :meId');
|
||||
}));
|
||||
}))
|
||||
.setParameter('meId', meId)
|
||||
.setParameter('otherId', otherId);
|
||||
|
||||
const messages = await query.take(limit).getMany();
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async roomTimeline(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatMessage['id'] | null, untilId?: MiChatMessage['id'] | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), sinceId, untilId)
|
||||
.where('message.toRoomId = :roomId', { roomId });
|
||||
|
||||
const messages = await query.take(limit).getMany();
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async userHistory(meId: MiUser['id'], limit: number): Promise<MiChatMessage[]> {
|
||||
const history: MiChatMessage[] = [];
|
||||
|
||||
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
||||
.select('muting.muteeId')
|
||||
.where('muting.muterId = :muterId', { muterId: meId });
|
||||
|
||||
for (let i = 0; i < limit; i++) {
|
||||
const found = history.map(m => (m.fromUserId === meId) ? m.toUserId! : m.fromUserId!);
|
||||
|
||||
const query = this.chatMessagesRepository.createQueryBuilder('message')
|
||||
.orderBy('message.id', 'DESC')
|
||||
.where(new Brackets(qb => {
|
||||
qb
|
||||
.where('message.fromUserId = :meId', { meId: meId })
|
||||
.orWhere('message.toUserId = :meId', { meId: meId });
|
||||
}))
|
||||
.andWhere('message.toRoomId IS NULL')
|
||||
.andWhere(`message.fromUserId NOT IN (${ mutingQuery.getQuery() })`)
|
||||
.andWhere(`message.toUserId NOT IN (${ mutingQuery.getQuery() })`);
|
||||
|
||||
if (found.length > 0) {
|
||||
query.andWhere('message.fromUserId NOT IN (:...found)', { found: found });
|
||||
query.andWhere('message.toUserId NOT IN (:...found)', { found: found });
|
||||
}
|
||||
|
||||
query.setParameters(mutingQuery.getParameters());
|
||||
|
||||
const message = await query.getOne();
|
||||
|
||||
if (message) {
|
||||
history.push(message);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async roomHistory(meId: MiUser['id'], limit: number): Promise<MiChatMessage[]> {
|
||||
// TODO: 一回のクエリにまとめられるかも
|
||||
const [memberRoomIds, ownedRoomIds] = await Promise.all([
|
||||
this.chatRoomMembershipsRepository.findBy({
|
||||
userId: meId,
|
||||
}).then(xs => xs.map(x => x.roomId)),
|
||||
this.chatRoomsRepository.findBy({
|
||||
ownerId: meId,
|
||||
}).then(xs => xs.map(x => x.id)),
|
||||
]);
|
||||
|
||||
const roomIds = memberRoomIds.concat(ownedRoomIds);
|
||||
|
||||
if (memberRoomIds.length === 0 && ownedRoomIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const history: MiChatMessage[] = [];
|
||||
|
||||
for (let i = 0; i < limit; i++) {
|
||||
const found = history.map(m => m.toRoomId!);
|
||||
|
||||
const query = this.chatMessagesRepository.createQueryBuilder('message')
|
||||
.orderBy('message.id', 'DESC')
|
||||
.where('message.toRoomId IN (:...roomIds)', { roomIds });
|
||||
|
||||
if (found.length > 0) {
|
||||
query.andWhere('message.toRoomId NOT IN (:...found)', { found: found });
|
||||
}
|
||||
|
||||
const message = await query.getOne();
|
||||
|
||||
if (message) {
|
||||
history.push(message);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getUserReadStateMap(userId: MiUser['id'], otherIds: MiUser['id'][]) {
|
||||
const readStateMap: Record<MiUser['id'], boolean> = {};
|
||||
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
|
||||
for (const otherId of otherIds) {
|
||||
redisPipeline.get(`newUserChatMessageExists:${userId}:${otherId}`);
|
||||
}
|
||||
|
||||
const markers = await redisPipeline.exec();
|
||||
|
||||
for (let i = 0; i < otherIds.length; i++) {
|
||||
const marker = markers[i][1];
|
||||
readStateMap[otherIds[i]] = marker == null;
|
||||
}
|
||||
|
||||
return readStateMap;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getRoomReadStateMap(userId: MiUser['id'], roomIds: MiChatRoom['id'][]) {
|
||||
const readStateMap: Record<MiChatRoom['id'], boolean> = {};
|
||||
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
|
||||
for (const roomId of roomIds) {
|
||||
redisPipeline.get(`newRoomChatMessageExists:${userId}:${roomId}`);
|
||||
}
|
||||
|
||||
const markers = await redisPipeline.exec();
|
||||
|
||||
for (let i = 0; i < roomIds.length; i++) {
|
||||
const marker = markers[i][1];
|
||||
readStateMap[roomIds[i]] = marker == null;
|
||||
}
|
||||
|
||||
return readStateMap;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async hasUnreadMessages(userId: MiUser['id']) {
|
||||
const card = await this.redisClient.scard(`newChatMessagesExists:${userId}`);
|
||||
return card > 0;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createRoom(owner: MiUser, name: string) {
|
||||
const room = {
|
||||
id: this.idService.gen(),
|
||||
name: name,
|
||||
ownerId: owner.id,
|
||||
} satisfies Partial<MiChatRoom>;
|
||||
|
||||
const created = await this.chatRoomsRepository.insertOne(room);
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deleteRoom(room: MiChatRoom) {
|
||||
await this.chatRoomsRepository.delete(room.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findMyRoomById(ownerId: MiUser['id'], roomId: MiChatRoom['id']) {
|
||||
return this.chatRoomsRepository.findOneBy({ id: roomId, ownerId: ownerId });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findRoomById(roomId: MiChatRoom['id']) {
|
||||
return this.chatRoomsRepository.findOneBy({ id: roomId });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async isRoomMember(roomId: MiChatRoom['id'], userId: MiUser['id']) {
|
||||
const membership = await this.chatRoomMembershipsRepository.findOneBy({ roomId, userId });
|
||||
return membership != null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createRoomInvitation(inviterId: MiUser['id'], roomId: MiChatRoom['id'], inviteeId: MiUser['id']) {
|
||||
if (inviterId === inviteeId) {
|
||||
throw new Error('yourself');
|
||||
}
|
||||
|
||||
const room = await this.chatRoomsRepository.findOneByOrFail({ id: roomId, ownerId: inviterId });
|
||||
|
||||
const existingInvitation = await this.chatRoomInvitationsRepository.findOneBy({ roomId, userId: inviteeId });
|
||||
if (existingInvitation) {
|
||||
throw new Error('already invited');
|
||||
}
|
||||
|
||||
const membershipsCount = await this.chatRoomMembershipsRepository.countBy({ roomId });
|
||||
if (membershipsCount >= MAX_ROOM_MEMBERS) {
|
||||
throw new Error('room is full');
|
||||
}
|
||||
|
||||
// TODO: cehck block
|
||||
|
||||
const invitation = {
|
||||
id: this.idService.gen(),
|
||||
roomId: room.id,
|
||||
userId: inviteeId,
|
||||
} satisfies Partial<MiChatRoomInvitation>;
|
||||
|
||||
const created = await this.chatRoomInvitationsRepository.insertOne(invitation);
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getOwnedRoomsWithPagination(ownerId: MiUser['id'], limit: number, sinceId?: MiChatRoom['id'] | null, untilId?: MiChatRoom['id'] | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.chatRoomsRepository.createQueryBuilder('room'), sinceId, untilId)
|
||||
.where('room.ownerId = :ownerId', { ownerId });
|
||||
|
||||
const rooms = await query.take(limit).getMany();
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getReceivedRoomInvitationsWithPagination(userId: MiUser['id'], limit: number, sinceId?: MiChatRoomInvitation['id'] | null, untilId?: MiChatRoomInvitation['id'] | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.chatRoomInvitationsRepository.createQueryBuilder('invitation'), sinceId, untilId)
|
||||
.where('invitation.userId = :userId', { userId })
|
||||
.andWhere('invitation.ignored = FALSE');
|
||||
|
||||
const invitations = await query.take(limit).getMany();
|
||||
|
||||
return invitations;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async joinToRoom(userId: MiUser['id'], roomId: MiChatRoom['id']) {
|
||||
const invitation = await this.chatRoomInvitationsRepository.findOneByOrFail({ roomId, userId });
|
||||
|
||||
const membershipsCount = await this.chatRoomMembershipsRepository.countBy({ roomId });
|
||||
if (membershipsCount >= MAX_ROOM_MEMBERS) {
|
||||
throw new Error('room is full');
|
||||
}
|
||||
|
||||
const membership = {
|
||||
id: this.idService.gen(),
|
||||
roomId: roomId,
|
||||
userId: userId,
|
||||
} satisfies Partial<MiChatRoomMembership>;
|
||||
|
||||
// TODO: transaction
|
||||
await this.chatRoomMembershipsRepository.insertOne(membership);
|
||||
await this.chatRoomInvitationsRepository.delete(invitation.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async ignoreRoomInvitation(userId: MiUser['id'], roomId: MiChatRoom['id']) {
|
||||
const invitation = await this.chatRoomInvitationsRepository.findOneByOrFail({ roomId, userId });
|
||||
await this.chatRoomInvitationsRepository.update(invitation.id, { ignored: true });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async leaveRoom(userId: MiUser['id'], roomId: MiChatRoom['id']) {
|
||||
const membership = await this.chatRoomMembershipsRepository.findOneByOrFail({ roomId, userId });
|
||||
await this.chatRoomMembershipsRepository.delete(membership.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updateRoom(room: MiChatRoom, params: {
|
||||
name?: string;
|
||||
}): Promise<MiChatRoom> {
|
||||
return this.chatRoomsRepository.createQueryBuilder().update()
|
||||
.set(params)
|
||||
.where('id = :id', { id: room.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => {
|
||||
return response.raw[0];
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getRoomMembershipsWithPagination(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatRoomMembership['id'] | null, untilId?: MiChatRoomMembership['id'] | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.chatRoomMembershipsRepository.createQueryBuilder('membership'), sinceId, untilId)
|
||||
.where('membership.roomId = :roomId', { roomId });
|
||||
|
||||
const memberships = await query.take(limit).getMany();
|
||||
|
||||
return memberships;
|
||||
}
|
||||
}
|
@@ -44,6 +44,7 @@ import { ModerationLogService } from './ModerationLogService.js';
|
||||
import { NoteCreateService } from './NoteCreateService.js';
|
||||
import { NoteDeleteService } from './NoteDeleteService.js';
|
||||
import { NotePiningService } from './NotePiningService.js';
|
||||
import { NoteReadService } from './NoteReadService.js';
|
||||
import { NotificationService } from './NotificationService.js';
|
||||
import { PollService } from './PollService.js';
|
||||
import { PushNotificationService } from './PushNotificationService.js';
|
||||
@@ -74,7 +75,6 @@ import { ClipService } from './ClipService.js';
|
||||
import { FeaturedService } from './FeaturedService.js';
|
||||
import { FanoutTimelineService } from './FanoutTimelineService.js';
|
||||
import { ChannelFollowingService } from './ChannelFollowingService.js';
|
||||
import { ChatService } from './ChatService.js';
|
||||
import { RegistryApiService } from './RegistryApiService.js';
|
||||
import { ReversiService } from './ReversiService.js';
|
||||
|
||||
@@ -100,7 +100,6 @@ import { AppEntityService } from './entities/AppEntityService.js';
|
||||
import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js';
|
||||
import { BlockingEntityService } from './entities/BlockingEntityService.js';
|
||||
import { ChannelEntityService } from './entities/ChannelEntityService.js';
|
||||
import { ChatEntityService } from './entities/ChatEntityService.js';
|
||||
import { ClipEntityService } from './entities/ClipEntityService.js';
|
||||
import { DriveFileEntityService } from './entities/DriveFileEntityService.js';
|
||||
import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js';
|
||||
@@ -185,6 +184,7 @@ const $ModerationLogService: Provider = { provide: 'ModerationLogService', useEx
|
||||
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
|
||||
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
|
||||
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
|
||||
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
|
||||
const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService };
|
||||
const $PollService: Provider = { provide: 'PollService', useExisting: PollService };
|
||||
const $SystemAccountService: Provider = { provide: 'SystemAccountService', useExisting: SystemAccountService };
|
||||
@@ -221,7 +221,6 @@ const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: Fe
|
||||
const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService };
|
||||
const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService };
|
||||
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
|
||||
const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatService };
|
||||
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
|
||||
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
|
||||
|
||||
@@ -248,7 +247,6 @@ const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting:
|
||||
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
|
||||
const $BlockingEntityService: Provider = { provide: 'BlockingEntityService', useExisting: BlockingEntityService };
|
||||
const $ChannelEntityService: Provider = { provide: 'ChannelEntityService', useExisting: ChannelEntityService };
|
||||
const $ChatEntityService: Provider = { provide: 'ChatEntityService', useExisting: ChatEntityService };
|
||||
const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting: ClipEntityService };
|
||||
const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService };
|
||||
const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useExisting: DriveFolderEntityService };
|
||||
@@ -335,6 +333,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
NoteCreateService,
|
||||
NoteDeleteService,
|
||||
NotePiningService,
|
||||
NoteReadService,
|
||||
NotificationService,
|
||||
PollService,
|
||||
SystemAccountService,
|
||||
@@ -371,7 +370,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FanoutTimelineService,
|
||||
FanoutTimelineEndpointService,
|
||||
ChannelFollowingService,
|
||||
ChatService,
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
|
||||
@@ -398,7 +396,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
AuthSessionEntityService,
|
||||
BlockingEntityService,
|
||||
ChannelEntityService,
|
||||
ChatEntityService,
|
||||
ClipEntityService,
|
||||
DriveFileEntityService,
|
||||
DriveFolderEntityService,
|
||||
@@ -481,6 +478,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$NoteCreateService,
|
||||
$NoteDeleteService,
|
||||
$NotePiningService,
|
||||
$NoteReadService,
|
||||
$NotificationService,
|
||||
$PollService,
|
||||
$SystemAccountService,
|
||||
@@ -517,7 +515,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FanoutTimelineService,
|
||||
$FanoutTimelineEndpointService,
|
||||
$ChannelFollowingService,
|
||||
$ChatService,
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
|
||||
@@ -544,7 +541,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$AuthSessionEntityService,
|
||||
$BlockingEntityService,
|
||||
$ChannelEntityService,
|
||||
$ChatEntityService,
|
||||
$ClipEntityService,
|
||||
$DriveFileEntityService,
|
||||
$DriveFolderEntityService,
|
||||
@@ -628,6 +624,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
NoteCreateService,
|
||||
NoteDeleteService,
|
||||
NotePiningService,
|
||||
NoteReadService,
|
||||
NotificationService,
|
||||
PollService,
|
||||
SystemAccountService,
|
||||
@@ -664,7 +661,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FanoutTimelineService,
|
||||
FanoutTimelineEndpointService,
|
||||
ChannelFollowingService,
|
||||
ChatService,
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
|
||||
@@ -690,7 +686,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
AuthSessionEntityService,
|
||||
BlockingEntityService,
|
||||
ChannelEntityService,
|
||||
ChatEntityService,
|
||||
ClipEntityService,
|
||||
DriveFileEntityService,
|
||||
DriveFolderEntityService,
|
||||
@@ -773,6 +768,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$NoteCreateService,
|
||||
$NoteDeleteService,
|
||||
$NotePiningService,
|
||||
$NoteReadService,
|
||||
$NotificationService,
|
||||
$PollService,
|
||||
$SystemAccountService,
|
||||
@@ -808,7 +804,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FanoutTimelineService,
|
||||
$FanoutTimelineEndpointService,
|
||||
$ChannelFollowingService,
|
||||
$ChatService,
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
|
||||
@@ -834,7 +829,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$AuthSessionEntityService,
|
||||
$BlockingEntityService,
|
||||
$ChannelEntityService,
|
||||
$ChatEntityService,
|
||||
$ClipEntityService,
|
||||
$DriveFileEntityService,
|
||||
$DriveFolderEntityService,
|
||||
|
@@ -20,7 +20,7 @@ import type { MiPage } from '@/models/Page.js';
|
||||
import type { MiWebhook } from '@/models/Webhook.js';
|
||||
import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
import { MiAvatarDecoration, MiChatRoom, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -72,8 +72,12 @@ export interface MainEventTypes {
|
||||
readAllNotifications: undefined;
|
||||
notificationFlushed: undefined;
|
||||
unreadNotification: Packed<'Notification'>;
|
||||
unreadMention: MiNote['id'];
|
||||
readAllUnreadMentions: undefined;
|
||||
unreadSpecifiedNote: MiNote['id'];
|
||||
readAllUnreadSpecifiedNotes: undefined;
|
||||
readAllAntennas: undefined;
|
||||
unreadAntenna: MiAntenna;
|
||||
newChatMessage: Packed<'ChatMessage'>;
|
||||
readAllAnnouncements: undefined;
|
||||
myTokenRegenerated: undefined;
|
||||
signin: {
|
||||
@@ -159,11 +163,6 @@ export interface AdminEventTypes {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChatEventTypes {
|
||||
message: Packed<'ChatMessage'>;
|
||||
deleted: Packed<'ChatMessage'>['id'];
|
||||
}
|
||||
|
||||
export interface ReversiEventTypes {
|
||||
matched: {
|
||||
game: Packed<'ReversiGameDetailed'>;
|
||||
@@ -203,7 +202,7 @@ export interface ReversiGameEventTypes {
|
||||
type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
|
||||
type EventUnionFromDictionary<
|
||||
T extends object,
|
||||
U = Events<T>,
|
||||
U = Events<T>
|
||||
> = U[keyof U];
|
||||
|
||||
type SerializedAll<T> = {
|
||||
@@ -296,14 +295,6 @@ export type GlobalEvents = {
|
||||
name: 'notesStream';
|
||||
payload: Serialized<Packed<'Note'>>;
|
||||
};
|
||||
chat: {
|
||||
name: `chatUserStream:${MiUser['id']}-${MiUser['id']}`;
|
||||
payload: EventTypesToEventPayload<ChatEventTypes>;
|
||||
};
|
||||
chatRoom: {
|
||||
name: `chatRoomStream:${MiChatRoom['id']}`;
|
||||
payload: EventTypesToEventPayload<ChatEventTypes>;
|
||||
};
|
||||
reversi: {
|
||||
name: `reversiStream:${MiUser['id']}`;
|
||||
payload: EventTypesToEventPayload<ReversiEventTypes>;
|
||||
@@ -402,16 +393,6 @@ export class GlobalEventService {
|
||||
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishChatUserStream<K extends keyof ChatEventTypes>(fromUserId: MiUser['id'], toUserId: MiUser['id'], type: K, value?: ChatEventTypes[K]): void {
|
||||
this.publish(`chatUserStream:${fromUserId}-${toUserId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishChatRoomStream<K extends keyof ChatEventTypes>(toRoomId: MiChatRoom['id'], type: K, value?: ChatEventTypes[K]): void {
|
||||
this.publish(`chatRoomStream:${toRoomId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishReversiStream<K extends keyof ReversiEventTypes>(userId: MiUser['id'], type: K, value?: ReversiEventTypes[K]): void {
|
||||
this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||
|
@@ -42,6 +42,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||
import { NoteReadService } from '@/core/NoteReadService.js';
|
||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
@@ -198,6 +199,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
private globalEventService: GlobalEventService,
|
||||
private queueService: QueueService,
|
||||
private fanoutTimelineService: FanoutTimelineService,
|
||||
private noteReadService: NoteReadService,
|
||||
private notificationService: NotificationService,
|
||||
private relayService: RelayService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
@@ -580,6 +582,31 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
if (!silent) {
|
||||
if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user);
|
||||
|
||||
// 未読通知を作成
|
||||
if (data.visibility === 'specified') {
|
||||
if (data.visibleUsers == null) throw new Error('invalid param');
|
||||
|
||||
for (const u of data.visibleUsers) {
|
||||
// ローカルユーザーのみ
|
||||
if (!this.userEntityService.isLocalUser(u)) continue;
|
||||
|
||||
this.noteReadService.insertNoteUnread(u.id, note, {
|
||||
isSpecified: true,
|
||||
isMentioned: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (const u of mentionedUsers) {
|
||||
// ローカルユーザーのみ
|
||||
if (!this.userEntityService.isLocalUser(u)) continue;
|
||||
|
||||
this.noteReadService.insertNoteUnread(u.id, note, {
|
||||
isSpecified: false,
|
||||
isMentioned: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Pack the note
|
||||
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });
|
||||
|
||||
|
143
packages/backend/src/core/NoteReadService.ts
Normal file
143
packages/backend/src/core/NoteReadService.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteReadService implements OnApplicationShutdown {
|
||||
#shutdownController = new AbortController();
|
||||
|
||||
constructor(
|
||||
@Inject(DI.noteUnreadsRepository)
|
||||
private noteUnreadsRepository: NoteUnreadsRepository,
|
||||
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
@Inject(DI.noteThreadMutingsRepository)
|
||||
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async insertNoteUnread(userId: MiUser['id'], note: MiNote, params: {
|
||||
// NOTE: isSpecifiedがtrueならisMentionedは必ずfalse
|
||||
isSpecified: boolean;
|
||||
isMentioned: boolean;
|
||||
}): Promise<void> {
|
||||
//#region ミュートしているなら無視
|
||||
const mute = await this.mutingsRepository.findBy({
|
||||
muterId: userId,
|
||||
});
|
||||
if (mute.map(m => m.muteeId).includes(note.userId)) return;
|
||||
//#endregion
|
||||
|
||||
// スレッドミュート
|
||||
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
|
||||
where: {
|
||||
userId: userId,
|
||||
threadId: note.threadId ?? note.id,
|
||||
},
|
||||
});
|
||||
if (isThreadMuted) return;
|
||||
|
||||
const unread = {
|
||||
id: this.idService.gen(),
|
||||
noteId: note.id,
|
||||
userId: userId,
|
||||
isSpecified: params.isSpecified,
|
||||
isMentioned: params.isMentioned,
|
||||
noteUserId: note.userId,
|
||||
};
|
||||
|
||||
await this.noteUnreadsRepository.insert(unread);
|
||||
|
||||
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
|
||||
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
|
||||
const exist = await this.noteUnreadsRepository.exists({ where: { id: unread.id } });
|
||||
|
||||
if (!exist) return;
|
||||
|
||||
if (params.isMentioned) {
|
||||
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
|
||||
}
|
||||
if (params.isSpecified) {
|
||||
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
|
||||
}
|
||||
}, () => { /* aborted, ignore it */ });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async read(
|
||||
userId: MiUser['id'],
|
||||
notes: (MiNote | Packed<'Note'>)[],
|
||||
): Promise<void> {
|
||||
if (notes.length === 0) return;
|
||||
|
||||
const noteIds = new Set<MiNote['id']>();
|
||||
|
||||
for (const note of notes) {
|
||||
if (note.mentions && note.mentions.includes(userId)) {
|
||||
noteIds.add(note.id);
|
||||
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
|
||||
noteIds.add(note.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (noteIds.size === 0) return;
|
||||
|
||||
// Remove the record
|
||||
await this.noteUnreadsRepository.delete({
|
||||
userId: userId,
|
||||
noteId: In(Array.from(noteIds)),
|
||||
});
|
||||
|
||||
// TODO: ↓まとめてクエリしたい
|
||||
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isMentioned: true,
|
||||
}).then(mentionsCount => {
|
||||
if (mentionsCount === 0) {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||
}
|
||||
}));
|
||||
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isSpecified: true,
|
||||
}).then(specifiedCount => {
|
||||
if (specifiedCount === 0) {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.#shutdownController.abort();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
@@ -63,7 +63,6 @@ export type RolePolicies = {
|
||||
canImportFollowing: boolean;
|
||||
canImportMuting: boolean;
|
||||
canImportUserLists: boolean;
|
||||
canChat: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_POLICIES: RolePolicies = {
|
||||
@@ -98,7 +97,6 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
canImportFollowing: true,
|
||||
canImportMuting: true,
|
||||
canImportUserLists: true,
|
||||
canChat: true,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -402,7 +400,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
|
||||
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
|
||||
canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
|
||||
canChat: calc('canChat', vs => vs.some(v => v === true)),
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { Brackets, IsNull } from 'typeorm';
|
||||
import { IsNull } from 'typeorm';
|
||||
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
@@ -736,30 +736,4 @@ export class UserFollowingService implements OnModuleInit {
|
||||
.where('following.followerId = :followerId', { followerId: userId })
|
||||
.getMany();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isFollowing(followerId: MiUser['id'], followeeId: MiUser['id']) {
|
||||
return this.followingsRepository.exists({
|
||||
where: {
|
||||
followerId,
|
||||
followeeId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async isMutual(aUserId: MiUser['id'], bUserId: MiUser['id']) {
|
||||
const count = await this.followingsRepository.createQueryBuilder('following')
|
||||
.where(new Brackets(qb => {
|
||||
qb.where('following.followerId = :aUserId', { aUserId })
|
||||
.andWhere('following.followeeId = :bUserId', { bUserId });
|
||||
}))
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb.where('following.followerId = :bUserId', { bUserId })
|
||||
.andWhere('following.followeeId = :aUserId', { aUserId });
|
||||
}))
|
||||
.getCount();
|
||||
|
||||
return count === 2;
|
||||
}
|
||||
}
|
||||
|
@@ -7,16 +7,42 @@ import { Injectable } from '@nestjs/common';
|
||||
import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||
import { type AbuseReportPayload, SystemWebhookPayload, SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { type Packed } from '@/misc/json-schema.js';
|
||||
import { AbuseReportPayload, SystemWebhookPayload, SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { type WebhookEventTypes } from '@/models/Webhook.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||
|
||||
const oneDayMillis = 24 * 60 * 60 * 1000;
|
||||
|
||||
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseReportPayload {
|
||||
const result: MiAbuseUserReport = {
|
||||
id: 'dummy-abuse-report1',
|
||||
targetUserId: 'dummy-target-user',
|
||||
targetUser: null,
|
||||
reporterId: 'dummy-reporter-user',
|
||||
reporter: null,
|
||||
assigneeId: null,
|
||||
assignee: null,
|
||||
resolved: false,
|
||||
forwarded: false,
|
||||
comment: 'This is a dummy report for testing purposes.',
|
||||
targetUserHost: null,
|
||||
reporterHost: null,
|
||||
resolvedAs: null,
|
||||
moderationNote: 'foo',
|
||||
...override,
|
||||
};
|
||||
|
||||
return {
|
||||
...result,
|
||||
targetUser: result.targetUser ? toPackedUserLite(result.targetUser) : null,
|
||||
reporter: result.reporter ? toPackedUserLite(result.reporter) : null,
|
||||
assignee: result.assignee ? toPackedUserLite(result.assignee) : null,
|
||||
};
|
||||
}
|
||||
|
||||
function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
||||
return {
|
||||
id: 'dummy-user-1',
|
||||
@@ -108,6 +134,124 @@ function generateDummyNote(override?: Partial<MiNote>): MiNote {
|
||||
};
|
||||
}
|
||||
|
||||
function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> {
|
||||
return {
|
||||
id: note.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
text: note.text,
|
||||
cw: note.cw,
|
||||
userId: note.userId,
|
||||
user: toPackedUserLite(note.user ?? generateDummyUser()),
|
||||
replyId: note.replyId,
|
||||
renoteId: note.renoteId,
|
||||
isHidden: false,
|
||||
visibility: note.visibility,
|
||||
mentions: note.mentions,
|
||||
visibleUserIds: note.visibleUserIds,
|
||||
fileIds: note.fileIds,
|
||||
files: [],
|
||||
tags: note.tags,
|
||||
poll: null,
|
||||
emojis: note.emojis,
|
||||
channelId: note.channelId,
|
||||
channel: note.channel,
|
||||
localOnly: note.localOnly,
|
||||
reactionAcceptance: note.reactionAcceptance,
|
||||
reactionEmojis: {},
|
||||
reactions: {},
|
||||
reactionCount: 0,
|
||||
renoteCount: note.renoteCount,
|
||||
repliesCount: note.repliesCount,
|
||||
uri: note.uri ?? undefined,
|
||||
url: note.url ?? undefined,
|
||||
reactionAndUserPairCache: note.reactionAndUserPairCache,
|
||||
...(detail ? {
|
||||
clippedCount: note.clippedCount,
|
||||
reply: note.reply ? toPackedNote(note.reply, false) : null,
|
||||
renote: note.renote ? toPackedNote(note.renote, true) : null,
|
||||
myReaction: null,
|
||||
} : {}),
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
avatarUrl: user.avatarUrl,
|
||||
avatarBlurhash: user.avatarBlurhash,
|
||||
avatarDecorations: user.avatarDecorations.map(it => ({
|
||||
id: it.id,
|
||||
angle: it.angle,
|
||||
flipH: it.flipH,
|
||||
url: 'https://example.com/dummy-image001.png',
|
||||
offsetX: it.offsetX,
|
||||
offsetY: it.offsetY,
|
||||
})),
|
||||
isBot: user.isBot,
|
||||
isCat: user.isCat,
|
||||
emojis: user.emojis,
|
||||
onlineStatus: 'active',
|
||||
badgeRoles: [],
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> {
|
||||
return {
|
||||
...toPackedUserLite(user),
|
||||
url: null,
|
||||
uri: null,
|
||||
movedTo: null,
|
||||
alsoKnownAs: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: user.updatedAt?.toISOString() ?? null,
|
||||
lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
|
||||
bannerUrl: user.bannerUrl,
|
||||
bannerBlurhash: user.bannerBlurhash,
|
||||
isLocked: user.isLocked,
|
||||
isSilenced: false,
|
||||
isSuspended: user.isSuspended,
|
||||
description: null,
|
||||
location: null,
|
||||
birthday: null,
|
||||
lang: null,
|
||||
fields: [],
|
||||
verifiedLinks: [],
|
||||
followersCount: user.followersCount,
|
||||
followingCount: user.followingCount,
|
||||
notesCount: user.notesCount,
|
||||
pinnedNoteIds: [],
|
||||
pinnedNotes: [],
|
||||
pinnedPageId: null,
|
||||
pinnedPage: null,
|
||||
publicReactions: true,
|
||||
followersVisibility: 'public',
|
||||
followingVisibility: 'public',
|
||||
twoFactorEnabled: false,
|
||||
usePasswordLessLogin: false,
|
||||
securityKeys: false,
|
||||
roles: [],
|
||||
memo: null,
|
||||
moderationNote: undefined,
|
||||
isFollowing: false,
|
||||
isFollowed: false,
|
||||
hasPendingFollowRequestFromYou: false,
|
||||
hasPendingFollowRequestToYou: false,
|
||||
isBlocking: false,
|
||||
isBlocked: false,
|
||||
isMuted: false,
|
||||
isRenoteMuted: false,
|
||||
notify: 'none',
|
||||
withReplies: true,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
const dummyUser1 = generateDummyUser();
|
||||
const dummyUser2 = generateDummyUser({
|
||||
id: 'dummy-user-2',
|
||||
@@ -140,7 +284,6 @@ export class WebhookTestService {
|
||||
};
|
||||
|
||||
constructor(
|
||||
private customEmojiService: CustomEmojiService,
|
||||
private userWebhookService: UserWebhookService,
|
||||
private systemWebhookService: SystemWebhookService,
|
||||
private queueService: QueueService,
|
||||
@@ -211,31 +354,31 @@ export class WebhookTestService {
|
||||
|
||||
switch (params.type) {
|
||||
case 'note': {
|
||||
send('note', { note: await this.toPackedNote(dummyNote1) });
|
||||
send('note', { note: toPackedNote(dummyNote1) });
|
||||
break;
|
||||
}
|
||||
case 'reply': {
|
||||
send('reply', { note: await this.toPackedNote(dummyReply1) });
|
||||
send('reply', { note: toPackedNote(dummyReply1) });
|
||||
break;
|
||||
}
|
||||
case 'renote': {
|
||||
send('renote', { note: await this.toPackedNote(dummyRenote1) });
|
||||
send('renote', { note: toPackedNote(dummyRenote1) });
|
||||
break;
|
||||
}
|
||||
case 'mention': {
|
||||
send('mention', { note: await this.toPackedNote(dummyMention1) });
|
||||
send('mention', { note: toPackedNote(dummyMention1) });
|
||||
break;
|
||||
}
|
||||
case 'follow': {
|
||||
send('follow', { user: await this.toPackedUserDetailedNotMe(dummyUser1) });
|
||||
send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) });
|
||||
break;
|
||||
}
|
||||
case 'followed': {
|
||||
send('followed', { user: await this.toPackedUserLite(dummyUser2) });
|
||||
send('followed', { user: toPackedUserLite(dummyUser2) });
|
||||
break;
|
||||
}
|
||||
case 'unfollow': {
|
||||
send('unfollow', { user: await this.toPackedUserDetailedNotMe(dummyUser3) });
|
||||
send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) });
|
||||
break;
|
||||
}
|
||||
// まだ実装されていない (#9485)
|
||||
@@ -284,7 +427,7 @@ export class WebhookTestService {
|
||||
|
||||
switch (params.type) {
|
||||
case 'abuseReport': {
|
||||
send('abuseReport', await this.generateAbuseReport({
|
||||
send('abuseReport', generateAbuseReport({
|
||||
targetUserId: dummyUser1.id,
|
||||
targetUser: dummyUser1,
|
||||
reporterId: dummyUser2.id,
|
||||
@@ -293,7 +436,7 @@ export class WebhookTestService {
|
||||
break;
|
||||
}
|
||||
case 'abuseReportResolved': {
|
||||
send('abuseReportResolved', await this.generateAbuseReport({
|
||||
send('abuseReportResolved', generateAbuseReport({
|
||||
targetUserId: dummyUser1.id,
|
||||
targetUser: dummyUser1,
|
||||
reporterId: dummyUser2.id,
|
||||
@@ -305,7 +448,7 @@ export class WebhookTestService {
|
||||
break;
|
||||
}
|
||||
case 'userCreated': {
|
||||
send('userCreated', await this.toPackedUserLite(dummyUser1));
|
||||
send('userCreated', toPackedUserLite(dummyUser1));
|
||||
break;
|
||||
}
|
||||
case 'inactiveModeratorsWarning': {
|
||||
@@ -331,153 +474,4 @@ export class WebhookTestService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async generateAbuseReport(override?: Partial<MiAbuseUserReport>): Promise<AbuseReportPayload> {
|
||||
const result: MiAbuseUserReport = {
|
||||
id: 'dummy-abuse-report1',
|
||||
targetUserId: 'dummy-target-user',
|
||||
targetUser: null,
|
||||
reporterId: 'dummy-reporter-user',
|
||||
reporter: null,
|
||||
assigneeId: null,
|
||||
assignee: null,
|
||||
resolved: false,
|
||||
forwarded: false,
|
||||
comment: 'This is a dummy report for testing purposes.',
|
||||
targetUserHost: null,
|
||||
reporterHost: null,
|
||||
resolvedAs: null,
|
||||
moderationNote: 'foo',
|
||||
...override,
|
||||
};
|
||||
|
||||
return {
|
||||
...result,
|
||||
targetUser: result.targetUser ? await this.toPackedUserLite(result.targetUser) : null,
|
||||
reporter: result.reporter ? await this.toPackedUserLite(result.reporter) : null,
|
||||
assignee: result.assignee ? await this.toPackedUserLite(result.assignee) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Promise<Packed<'Note'>> {
|
||||
return {
|
||||
id: note.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
text: note.text,
|
||||
cw: note.cw,
|
||||
userId: note.userId,
|
||||
user: await this.toPackedUserLite(note.user ?? generateDummyUser()),
|
||||
replyId: note.replyId,
|
||||
renoteId: note.renoteId,
|
||||
isHidden: false,
|
||||
visibility: note.visibility,
|
||||
mentions: note.mentions,
|
||||
visibleUserIds: note.visibleUserIds,
|
||||
fileIds: note.fileIds,
|
||||
files: [],
|
||||
tags: note.tags,
|
||||
poll: null,
|
||||
emojis: await this.customEmojiService.populateEmojis(note.emojis, note.userHost),
|
||||
channelId: note.channelId,
|
||||
channel: note.channel,
|
||||
localOnly: note.localOnly,
|
||||
reactionAcceptance: note.reactionAcceptance,
|
||||
reactionEmojis: {},
|
||||
reactions: {},
|
||||
reactionCount: 0,
|
||||
renoteCount: note.renoteCount,
|
||||
repliesCount: note.repliesCount,
|
||||
uri: note.uri ?? undefined,
|
||||
url: note.url ?? undefined,
|
||||
reactionAndUserPairCache: note.reactionAndUserPairCache,
|
||||
...(detail ? {
|
||||
clippedCount: note.clippedCount,
|
||||
reply: note.reply ? await this.toPackedNote(note.reply, false) : null,
|
||||
renote: note.renote ? await this.toPackedNote(note.renote, true) : null,
|
||||
myReaction: null,
|
||||
} : {}),
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Promise<Packed<'UserLite'>> {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
avatarUrl: user.avatarUrl,
|
||||
avatarBlurhash: user.avatarBlurhash,
|
||||
avatarDecorations: user.avatarDecorations.map(it => ({
|
||||
id: it.id,
|
||||
angle: it.angle,
|
||||
flipH: it.flipH,
|
||||
url: 'https://example.com/dummy-image001.png',
|
||||
offsetX: it.offsetX,
|
||||
offsetY: it.offsetY,
|
||||
})),
|
||||
isBot: user.isBot,
|
||||
isCat: user.isCat,
|
||||
emojis: await this.customEmojiService.populateEmojis(user.emojis, user.host),
|
||||
onlineStatus: 'active',
|
||||
badgeRoles: [],
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Promise<Packed<'UserDetailedNotMe'>> {
|
||||
return {
|
||||
...await this.toPackedUserLite(user),
|
||||
url: null,
|
||||
uri: null,
|
||||
movedTo: null,
|
||||
alsoKnownAs: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: user.updatedAt?.toISOString() ?? null,
|
||||
lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
|
||||
bannerUrl: user.bannerUrl,
|
||||
bannerBlurhash: user.bannerBlurhash,
|
||||
isLocked: user.isLocked,
|
||||
isSilenced: false,
|
||||
isSuspended: user.isSuspended,
|
||||
description: null,
|
||||
location: null,
|
||||
birthday: null,
|
||||
lang: null,
|
||||
fields: [],
|
||||
verifiedLinks: [],
|
||||
followersCount: user.followersCount,
|
||||
followingCount: user.followingCount,
|
||||
notesCount: user.notesCount,
|
||||
pinnedNoteIds: [],
|
||||
pinnedNotes: [],
|
||||
pinnedPageId: null,
|
||||
pinnedPage: null,
|
||||
publicReactions: true,
|
||||
followersVisibility: 'public',
|
||||
followingVisibility: 'public',
|
||||
twoFactorEnabled: false,
|
||||
usePasswordLessLogin: false,
|
||||
securityKeys: false,
|
||||
roles: [],
|
||||
memo: null,
|
||||
moderationNote: undefined,
|
||||
isFollowing: false,
|
||||
isFollowed: false,
|
||||
hasPendingFollowRequestFromYou: false,
|
||||
hasPendingFollowRequestToYou: false,
|
||||
isBlocking: false,
|
||||
isBlocked: false,
|
||||
isMuted: false,
|
||||
isRenoteMuted: false,
|
||||
notify: 'none',
|
||||
withReplies: true,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -1,282 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiUser, ChatMessagesRepository, MiChatMessage, ChatRoomsRepository, MiChatRoom, MiChatRoomInvitation, ChatRoomInvitationsRepository, MiChatRoomMembership, ChatRoomMembershipsRepository } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
export class ChatEntityService {
|
||||
constructor(
|
||||
@Inject(DI.chatMessagesRepository)
|
||||
private chatMessagesRepository: ChatMessagesRepository,
|
||||
|
||||
@Inject(DI.chatRoomsRepository)
|
||||
private chatRoomsRepository: ChatRoomsRepository,
|
||||
|
||||
@Inject(DI.chatRoomInvitationsRepository)
|
||||
private chatRoomInvitationsRepository: ChatRoomInvitationsRepository,
|
||||
|
||||
@Inject(DI.chatRoomMembershipsRepository)
|
||||
private chatRoomMembershipsRepository: ChatRoomMembershipsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packMessageDetailed(
|
||||
src: MiChatMessage['id'] | MiChatMessage,
|
||||
me?: { id: MiUser['id'] },
|
||||
options?: {
|
||||
_hint_?: {
|
||||
packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>;
|
||||
packedUsers: Map<MiChatMessage['id'], Packed<'UserLite'>>;
|
||||
packedRooms: Map<MiChatMessage['toRoomId'], Packed<'ChatRoom'> | null>;
|
||||
};
|
||||
},
|
||||
): Promise<Packed<'ChatMessage'>> {
|
||||
const packedUsers = options?._hint_?.packedUsers;
|
||||
const packedFiles = options?._hint_?.packedFiles;
|
||||
const packedRooms = options?._hint_?.packedRooms;
|
||||
|
||||
const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
createdAt: this.idService.parse(message.id).date.toISOString(),
|
||||
text: message.text,
|
||||
fromUserId: message.fromUserId,
|
||||
fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId, me),
|
||||
toUserId: message.toUserId,
|
||||
toUser: message.toUserId ? (packedUsers?.get(message.toUserId) ?? await this.userEntityService.pack(message.toUser ?? message.toUserId, me)) : undefined,
|
||||
toRoomId: message.toRoomId,
|
||||
toRoom: message.toRoomId ? (packedRooms?.get(message.toRoomId) ?? await this.packRoom(message.toRoom ?? message.toRoomId, me)) : undefined,
|
||||
fileId: message.fileId,
|
||||
file: message.file ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file)) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packMessagesDetailed(
|
||||
messages: MiChatMessage[],
|
||||
me: { id: MiUser['id'] },
|
||||
) {
|
||||
if (messages.length === 0) return [];
|
||||
|
||||
const excludeMe = (x: MiUser | string) => {
|
||||
if (typeof x === 'string') {
|
||||
return x !== me.id;
|
||||
} else {
|
||||
return x.id !== me.id;
|
||||
}
|
||||
};
|
||||
|
||||
const users = [
|
||||
...messages.map((m) => m.fromUser ?? m.fromUserId).filter(excludeMe),
|
||||
...messages.map((m) => m.toUser ?? m.toUserId).filter(x => x != null).filter(excludeMe),
|
||||
];
|
||||
|
||||
const [packedUsers, packedFiles] = await Promise.all([
|
||||
this.userEntityService.packMany(users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u]))),
|
||||
this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)),
|
||||
]);
|
||||
|
||||
return Promise.all(messages.map(message => this.packMessageDetailed(message, me, { _hint_: { packedUsers, packedFiles } })));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packMessageLite(
|
||||
src: MiChatMessage['id'] | MiChatMessage,
|
||||
options?: {
|
||||
_hint_?: {
|
||||
packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>;
|
||||
};
|
||||
},
|
||||
): Promise<Packed<'ChatMessageLite'>> {
|
||||
const packedFiles = options?._hint_?.packedFiles;
|
||||
|
||||
const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
createdAt: this.idService.parse(message.id).date.toISOString(),
|
||||
text: message.text,
|
||||
fromUserId: message.fromUserId,
|
||||
toUserId: message.toUserId,
|
||||
fileId: message.fileId,
|
||||
file: message.file ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file)) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packMessagesLite(
|
||||
messages: MiChatMessage[],
|
||||
) {
|
||||
if (messages.length === 0) return [];
|
||||
|
||||
const [packedFiles] = await Promise.all([
|
||||
this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)),
|
||||
]);
|
||||
|
||||
return Promise.all(messages.map(message => this.packMessageLite(message, { _hint_: { packedFiles } })));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packMessageLiteForRoom(
|
||||
src: MiChatMessage['id'] | MiChatMessage,
|
||||
options?: {
|
||||
_hint_?: {
|
||||
packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>;
|
||||
packedUsers: Map<MiChatMessage['id'], Packed<'UserLite'>>;
|
||||
};
|
||||
},
|
||||
): Promise<Packed<'ChatMessageLite'>> {
|
||||
const packedFiles = options?._hint_?.packedFiles;
|
||||
const packedUsers = options?._hint_?.packedUsers;
|
||||
|
||||
const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
createdAt: this.idService.parse(message.id).date.toISOString(),
|
||||
text: message.text,
|
||||
fromUserId: message.fromUserId,
|
||||
fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId),
|
||||
toRoomId: message.toRoomId,
|
||||
fileId: message.fileId,
|
||||
file: message.file ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file)) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packMessagesLiteForRoom(
|
||||
messages: MiChatMessage[],
|
||||
) {
|
||||
if (messages.length === 0) return [];
|
||||
|
||||
const [packedUsers, packedFiles] = await Promise.all([
|
||||
this.userEntityService.packMany(messages.map(x => x.fromUser ?? x.fromUserId))
|
||||
.then(users => new Map(users.map(u => [u.id, u]))),
|
||||
this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)),
|
||||
]);
|
||||
|
||||
return Promise.all(messages.map(message => this.packMessageLiteForRoom(message, { _hint_: { packedFiles, packedUsers } })));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packRoom(
|
||||
src: MiChatRoom['id'] | MiChatRoom,
|
||||
me?: { id: MiUser['id'] },
|
||||
options?: {
|
||||
_hint_?: {
|
||||
packedOwners: Map<MiChatRoom['id'], Packed<'UserLite'>>;
|
||||
};
|
||||
},
|
||||
): Promise<Packed<'ChatRoom'>> {
|
||||
const room = typeof src === 'object' ? src : await this.chatRoomsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: room.id,
|
||||
createdAt: this.idService.parse(room.id).date.toISOString(),
|
||||
name: room.name,
|
||||
ownerId: room.ownerId,
|
||||
owner: options?._hint_?.packedOwners.get(room.ownerId) ?? await this.userEntityService.pack(room.owner ?? room.ownerId, me),
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packRooms(
|
||||
rooms: MiChatRoom[],
|
||||
me: { id: MiUser['id'] },
|
||||
) {
|
||||
if (rooms.length === 0) return [];
|
||||
|
||||
const owners = rooms.map(x => x.owner ?? x.ownerId);
|
||||
|
||||
const packedOwners = await this.userEntityService.packMany(owners, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
|
||||
return Promise.all(rooms.map(room => this.packRoom(room, me, { _hint_: { packedOwners } })));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packRoomInvitation(
|
||||
src: MiChatRoomInvitation['id'] | MiChatRoomInvitation,
|
||||
me: { id: MiUser['id'] },
|
||||
options?: {
|
||||
_hint_?: {
|
||||
packedRooms: Map<MiChatRoomInvitation['roomId'], Packed<'ChatRoom'>>;
|
||||
packedUsers: Map<MiChatRoomInvitation['id'], Packed<'UserLite'>>;
|
||||
};
|
||||
},
|
||||
): Promise<Packed<'ChatRoomInvitation'>> {
|
||||
const invitation = typeof src === 'object' ? src : await this.chatRoomInvitationsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: invitation.id,
|
||||
createdAt: this.idService.parse(invitation.id).date.toISOString(),
|
||||
roomId: invitation.roomId,
|
||||
room: options?._hint_?.packedRooms.get(invitation.roomId) ?? await this.packRoom(invitation.room ?? invitation.roomId, me),
|
||||
userId: invitation.userId,
|
||||
user: options?._hint_?.packedUsers.get(invitation.userId) ?? await this.userEntityService.pack(invitation.user ?? invitation.userId, me),
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packRoomInvitations(
|
||||
invitations: MiChatRoomInvitation[],
|
||||
me: { id: MiUser['id'] },
|
||||
) {
|
||||
if (invitations.length === 0) return [];
|
||||
|
||||
return Promise.all(invitations.map(invitation => this.packRoomInvitation(invitation, me)));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packRoomMembership(
|
||||
src: MiChatRoomMembership['id'] | MiChatRoomMembership,
|
||||
me: { id: MiUser['id'] },
|
||||
options?: {
|
||||
_hint_?: {
|
||||
packedUsers: Map<MiChatRoomMembership['id'], Packed<'UserLite'>>;
|
||||
};
|
||||
},
|
||||
): Promise<Packed<'ChatRoomMembership'>> {
|
||||
const membership = typeof src === 'object' ? src : await this.chatRoomMembershipsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: membership.id,
|
||||
createdAt: this.idService.parse(membership.id).date.toISOString(),
|
||||
userId: membership.userId,
|
||||
user: options?._hint_?.packedUsers.get(membership.userId) ?? await this.userEntityService.pack(membership.user ?? membership.userId, me),
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packRoomMemberships(
|
||||
memberships: MiChatRoomMembership[],
|
||||
me: { id: MiUser['id'] },
|
||||
) {
|
||||
if (memberships.length === 0) return [];
|
||||
|
||||
const users = memberships.map(x => x.user ?? x.userId);
|
||||
|
||||
const packedUsers = await this.userEntityService.packMany(users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
|
||||
return Promise.all(memberships.map(membership => this.packRoomMembership(membership, me, { _hint_: { packedUsers } })));
|
||||
}
|
||||
}
|
@@ -32,6 +32,7 @@ import type {
|
||||
MiUserNotePining,
|
||||
MiUserProfile,
|
||||
MutingsRepository,
|
||||
NoteUnreadsRepository,
|
||||
RenoteMutingsRepository,
|
||||
UserMemoRepository,
|
||||
UserNotePiningsRepository,
|
||||
@@ -47,9 +48,9 @@ import { IdService } from '@/core/IdService.js';
|
||||
import type { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||
import { ChatService } from '@/core/ChatService.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
import type { NoteEntityService } from './NoteEntityService.js';
|
||||
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
import type { PageEntityService } from './PageEntityService.js';
|
||||
|
||||
const Ajv = _Ajv.default;
|
||||
@@ -93,7 +94,6 @@ export class UserEntityService implements OnModuleInit {
|
||||
private federatedInstanceService: FederatedInstanceService;
|
||||
private idService: IdService;
|
||||
private avatarDecorationService: AvatarDecorationService;
|
||||
private chatService: ChatService;
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
@@ -128,6 +128,9 @@ export class UserEntityService implements OnModuleInit {
|
||||
@Inject(DI.renoteMutingsRepository)
|
||||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
@Inject(DI.noteUnreadsRepository)
|
||||
private noteUnreadsRepository: NoteUnreadsRepository,
|
||||
|
||||
@Inject(DI.userNotePiningsRepository)
|
||||
private userNotePiningsRepository: UserNotePiningsRepository,
|
||||
|
||||
@@ -149,7 +152,6 @@ export class UserEntityService implements OnModuleInit {
|
||||
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
|
||||
this.idService = this.moduleRef.get('IdService');
|
||||
this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService');
|
||||
this.chatService = this.moduleRef.get('ChatService');
|
||||
}
|
||||
|
||||
//#region Validators
|
||||
@@ -556,7 +558,6 @@ export class UserEntityService implements OnModuleInit {
|
||||
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
|
||||
followersVisibility: profile!.followersVisibility,
|
||||
followingVisibility: profile!.followingVisibility,
|
||||
chatScope: user.chatScope,
|
||||
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
@@ -597,9 +598,14 @@ export class UserEntityService implements OnModuleInit {
|
||||
isDeleted: user.isDeleted,
|
||||
twoFactorBackupCodesStock: profile?.twoFactorBackupSecret?.length === 5 ? 'full' : (profile?.twoFactorBackupSecret?.length ?? 0) > 0 ? 'partial' : 'none',
|
||||
hideOnlineStatus: user.hideOnlineStatus,
|
||||
hasUnreadSpecifiedNotes: false, // 後方互換性のため
|
||||
hasUnreadMentions: false, // 後方互換性のため
|
||||
hasUnreadChatMessages: this.chatService.hasUnreadMessages(user.id),
|
||||
hasUnreadSpecifiedNotes: this.noteUnreadsRepository.count({
|
||||
where: { userId: user.id, isSpecified: true },
|
||||
take: 1,
|
||||
}).then(count => count > 0),
|
||||
hasUnreadMentions: this.noteUnreadsRepository.count({
|
||||
where: { userId: user.id, isMentioned: true },
|
||||
take: 1,
|
||||
}).then(count => count > 0),
|
||||
hasUnreadAnnouncement: unreadAnnouncements!.length > 0,
|
||||
unreadAnnouncements,
|
||||
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
||||
|
@@ -24,6 +24,7 @@ export const DI = {
|
||||
noteFavoritesRepository: Symbol('noteFavoritesRepository'),
|
||||
noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
|
||||
noteReactionsRepository: Symbol('noteReactionsRepository'),
|
||||
noteUnreadsRepository: Symbol('noteUnreadsRepository'),
|
||||
pollsRepository: Symbol('pollsRepository'),
|
||||
pollVotesRepository: Symbol('pollVotesRepository'),
|
||||
userProfilesRepository: Symbol('userProfilesRepository'),
|
||||
@@ -82,11 +83,6 @@ export const DI = {
|
||||
flashsRepository: Symbol('flashsRepository'),
|
||||
flashLikesRepository: Symbol('flashLikesRepository'),
|
||||
userMemosRepository: Symbol('userMemosRepository'),
|
||||
chatMessagesRepository: Symbol('chatMessagesRepository'),
|
||||
chatApprovalsRepository: Symbol('chatApprovalsRepository'),
|
||||
chatRoomsRepository: Symbol('chatRoomsRepository'),
|
||||
chatRoomMembershipsRepository: Symbol('chatRoomMembershipsRepository'),
|
||||
chatRoomInvitationsRepository: Symbol('chatRoomInvitationsRepository'),
|
||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||
//#endregion
|
||||
|
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { WritableStream } from 'node:stream/web';
|
||||
import type { PathLike } from 'node:fs';
|
||||
|
||||
/**
|
||||
|
@@ -63,10 +63,6 @@ import {
|
||||
} from '@/models/json-schema/meta.js';
|
||||
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
|
||||
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
|
||||
import { packedChatMessageSchema, packedChatMessageLiteSchema } from '@/models/json-schema/chat-message.js';
|
||||
import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js';
|
||||
import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js';
|
||||
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
|
||||
|
||||
export const refs = {
|
||||
UserLite: packedUserLiteSchema,
|
||||
@@ -124,11 +120,6 @@ export const refs = {
|
||||
MetaDetailed: packedMetaDetailedSchema,
|
||||
SystemWebhook: packedSystemWebhookSchema,
|
||||
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
|
||||
ChatMessage: packedChatMessageSchema,
|
||||
ChatMessageLite: packedChatMessageLiteSchema,
|
||||
ChatRoom: packedChatRoomSchema,
|
||||
ChatRoomInvitation: packedChatRoomInvitationSchema,
|
||||
ChatRoomMembership: packedChatRoomMembershipSchema,
|
||||
};
|
||||
|
||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||
@@ -175,16 +166,15 @@ export interface Schema extends OfSchema {
|
||||
readonly maximum?: number;
|
||||
readonly minimum?: number;
|
||||
readonly pattern?: string;
|
||||
readonly additionalProperties?: Schema | boolean;
|
||||
}
|
||||
|
||||
type RequiredPropertyNames<s extends Obj> = {
|
||||
[K in keyof s]:
|
||||
// K is not optional
|
||||
s[K]['optional'] extends false ? K :
|
||||
// K has default value
|
||||
s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K :
|
||||
never
|
||||
// K is not optional
|
||||
s[K]['optional'] extends false ? K :
|
||||
// K has default value
|
||||
s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K :
|
||||
never
|
||||
}[keyof s];
|
||||
|
||||
export type Obj = Record<string, Schema>;
|
||||
@@ -223,18 +213,11 @@ type ObjectSchemaTypeDef<p extends Schema> =
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? p['anyOf'][number]['required'] extends ReadonlyArray<keyof p['properties']> ?
|
||||
UnionObjType<p['properties'], NonNullable<p['anyOf'][number]['required']>> & ObjType<p['properties'], NonNullable<p['required']>>
|
||||
: never
|
||||
: ObjType<p['properties'], NonNullable<p['required']>>
|
||||
:
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
|
||||
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
|
||||
p['additionalProperties'] extends true ? Record<string, any> :
|
||||
p['additionalProperties'] extends Schema ?
|
||||
p['additionalProperties'] extends infer AdditionalProperties ?
|
||||
AdditionalProperties extends Schema ?
|
||||
Record<string, SchemaType<AdditionalProperties>> :
|
||||
never :
|
||||
never :
|
||||
any;
|
||||
: ObjType<p['properties'], NonNullable<p['required']>>
|
||||
:
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
|
||||
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
|
||||
any;
|
||||
|
||||
type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;
|
||||
|
||||
@@ -244,30 +227,30 @@ export type SchemaTypeDef<p extends Schema> =
|
||||
p['type'] extends 'number' ? number :
|
||||
p['type'] extends 'string' ? (
|
||||
p['enum'] extends readonly (string | null)[] ?
|
||||
p['enum'][number] :
|
||||
p['format'] extends 'date-time' ? string : // Dateにする??
|
||||
string
|
||||
p['enum'][number] :
|
||||
p['format'] extends 'date-time' ? string : // Dateにする??
|
||||
string
|
||||
) :
|
||||
p['type'] extends 'boolean' ? boolean :
|
||||
p['type'] extends 'object' ? ObjectSchemaTypeDef<p> :
|
||||
p['type'] extends 'array' ? (
|
||||
p['items'] extends OfSchema ? (
|
||||
p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] :
|
||||
p['items']['oneOf'] extends ReadonlyArray<Schema> ? ArrayUnion<UnionSchemaType<NonNullable<p['items']['oneOf']>>> :
|
||||
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
|
||||
never
|
||||
) :
|
||||
p['prefixItems'] extends ReadonlyArray<Schema> ? (
|
||||
p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] :
|
||||
p['items'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||
p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||
[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
|
||||
) :
|
||||
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
|
||||
any[]
|
||||
p['type'] extends 'boolean' ? boolean :
|
||||
p['type'] extends 'object' ? ObjectSchemaTypeDef<p> :
|
||||
p['type'] extends 'array' ? (
|
||||
p['items'] extends OfSchema ? (
|
||||
p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] :
|
||||
p['items']['oneOf'] extends ReadonlyArray<Schema> ? ArrayUnion<UnionSchemaType<NonNullable<p['items']['oneOf']>>> :
|
||||
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
|
||||
never
|
||||
) :
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
|
||||
p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
|
||||
any;
|
||||
p['prefixItems'] extends ReadonlyArray<Schema> ? (
|
||||
p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] :
|
||||
p['items'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||
p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||
[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
|
||||
) :
|
||||
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
|
||||
any[]
|
||||
) :
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
|
||||
p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
|
||||
any;
|
||||
|
||||
export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>;
|
||||
|
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
@Entity('chat_approval')
|
||||
@Index(['userId', 'otherId'], { unique: true })
|
||||
export class MiChatApproval {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public otherId: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public other: MiUser | null;
|
||||
}
|
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
import { MiDriveFile } from './DriveFile.js';
|
||||
import { MiChatRoom } from './ChatRoom.js';
|
||||
|
||||
@Entity('chat_message')
|
||||
export class MiChatMessage {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public fromUserId: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public fromUser: MiUser | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(), nullable: true,
|
||||
})
|
||||
public toUserId: MiUser['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public toUser: MiUser | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(), nullable: true,
|
||||
})
|
||||
public toRoomId: MiChatRoom['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiChatRoom, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public toRoom: MiChatRoom | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 4096, nullable: true,
|
||||
})
|
||||
public text: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512, nullable: true,
|
||||
})
|
||||
public uri: string | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
array: true, default: '{}',
|
||||
})
|
||||
public reads: MiUser['id'][];
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public fileId: MiDriveFile['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiDriveFile, {
|
||||
onDelete: 'SET NULL',
|
||||
})
|
||||
@JoinColumn()
|
||||
public file: MiDriveFile | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024, array: true, default: '{}',
|
||||
})
|
||||
public reactions: string[];
|
||||
}
|
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
@Entity('chat_room')
|
||||
export class MiChatRoom {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public ownerId: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public owner: MiUser | null;
|
||||
}
|
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
import { MiChatRoom } from './ChatRoom.js';
|
||||
|
||||
@Entity('chat_room_invitation')
|
||||
@Index(['userId', 'roomId'], { unique: true })
|
||||
export class MiChatRoomInvitation {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public roomId: MiChatRoom['id'];
|
||||
|
||||
@ManyToOne(type => MiChatRoom, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public room: MiChatRoom | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public ignored: boolean;
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
import { MiChatRoom } from './ChatRoom.js';
|
||||
|
||||
@Entity('chat_room_membership')
|
||||
@Index(['userId', 'roomId'], { unique: true })
|
||||
export class MiChatRoomMembership {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public roomId: MiChatRoom['id'];
|
||||
|
||||
@ManyToOne(type => MiChatRoom, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public room: MiChatRoom | null;
|
||||
}
|
68
packages/backend/src/models/NoteUnread.ts
Normal file
68
packages/backend/src/models/NoteUnread.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
import { MiNote } from './Note.js';
|
||||
import type { MiChannel } from './Channel.js';
|
||||
|
||||
@Entity('note_unread')
|
||||
@Index(['userId', 'noteId'], { unique: true })
|
||||
export class MiNoteUnread {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public noteId: MiNote['id'];
|
||||
|
||||
@ManyToOne(type => MiNote, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public note: MiNote | null;
|
||||
|
||||
/**
|
||||
* メンションか否か
|
||||
*/
|
||||
@Index()
|
||||
@Column('boolean')
|
||||
public isMentioned: boolean;
|
||||
|
||||
/**
|
||||
* ダイレクト投稿か否か
|
||||
*/
|
||||
@Index()
|
||||
@Column('boolean')
|
||||
public isSpecified: boolean;
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: '[Denormalized]',
|
||||
})
|
||||
public noteUserId: MiUser['id'];
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
comment: '[Denormalized]',
|
||||
})
|
||||
public noteChannelId: MiChannel['id'] | null;
|
||||
//#endregion
|
||||
}
|
@@ -42,6 +42,7 @@ import {
|
||||
MiNoteFavorite,
|
||||
MiNoteReaction,
|
||||
MiNoteThreadMuting,
|
||||
MiNoteUnread,
|
||||
MiPage,
|
||||
MiPageLike,
|
||||
MiPasswordResetRequest,
|
||||
@@ -77,11 +78,6 @@ import {
|
||||
MiUserPublickey,
|
||||
MiUserSecurityKey,
|
||||
MiWebhook,
|
||||
MiChatMessage,
|
||||
MiChatRoom,
|
||||
MiChatRoomMembership,
|
||||
MiChatRoomInvitation,
|
||||
MiChatApproval,
|
||||
} from './_.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DataSource } from 'typeorm';
|
||||
@@ -140,6 +136,12 @@ const $noteReactionsRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $noteUnreadsRepository: Provider = {
|
||||
provide: DI.noteUnreadsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiNoteUnread).extend(miRepository as MiRepository<MiNoteUnread>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $pollsRepository: Provider = {
|
||||
provide: DI.pollsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository<MiPoll>),
|
||||
@@ -286,7 +288,7 @@ const $swSubscriptionsRepository: Provider = {
|
||||
|
||||
const $systemAccountsRepository: Provider = {
|
||||
provide: DI.systemAccountsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiSystemAccount).extend(miRepository as MiRepository<MiSystemAccount>),
|
||||
useFactory: (db: DataSource) => db.getRepository(MiSystemAccount),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@@ -304,7 +306,7 @@ const $abuseUserReportsRepository: Provider = {
|
||||
|
||||
const $abuseReportNotificationRecipientRepository: Provider = {
|
||||
provide: DI.abuseReportNotificationRecipientRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient).extend(miRepository as MiRepository<MiAbuseReportNotificationRecipient>),
|
||||
useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@@ -436,7 +438,7 @@ const $webhooksRepository: Provider = {
|
||||
|
||||
const $systemWebhooksRepository: Provider = {
|
||||
provide: DI.systemWebhooksRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook).extend(miRepository as MiRepository<MiSystemWebhook>),
|
||||
useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@@ -488,36 +490,6 @@ const $userMemosRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $chatMessagesRepository: Provider = {
|
||||
provide: DI.chatMessagesRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiChatMessage).extend(miRepository as MiRepository<MiChatMessage>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $chatRoomsRepository: Provider = {
|
||||
provide: DI.chatRoomsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiChatRoom).extend(miRepository as MiRepository<MiChatRoom>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $chatRoomMembershipsRepository: Provider = {
|
||||
provide: DI.chatRoomMembershipsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiChatRoomMembership).extend(miRepository as MiRepository<MiChatRoomMembership>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $chatRoomInvitationsRepository: Provider = {
|
||||
provide: DI.chatRoomInvitationsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiChatRoomInvitation).extend(miRepository as MiRepository<MiChatRoomInvitation>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $chatApprovalsRepository: Provider = {
|
||||
provide: DI.chatApprovalsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiChatApproval).extend(miRepository as MiRepository<MiChatApproval>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $bubbleGameRecordsRepository: Provider = {
|
||||
provide: DI.bubbleGameRecordsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository<MiBubbleGameRecord>),
|
||||
@@ -542,6 +514,7 @@ const $reversiGamesRepository: Provider = {
|
||||
$noteFavoritesRepository,
|
||||
$noteThreadMutingsRepository,
|
||||
$noteReactionsRepository,
|
||||
$noteUnreadsRepository,
|
||||
$pollsRepository,
|
||||
$pollVotesRepository,
|
||||
$userProfilesRepository,
|
||||
@@ -600,11 +573,6 @@ const $reversiGamesRepository: Provider = {
|
||||
$flashsRepository,
|
||||
$flashLikesRepository,
|
||||
$userMemosRepository,
|
||||
$chatMessagesRepository,
|
||||
$chatRoomsRepository,
|
||||
$chatRoomMembershipsRepository,
|
||||
$chatRoomInvitationsRepository,
|
||||
$chatApprovalsRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
],
|
||||
@@ -618,6 +586,7 @@ const $reversiGamesRepository: Provider = {
|
||||
$noteFavoritesRepository,
|
||||
$noteThreadMutingsRepository,
|
||||
$noteReactionsRepository,
|
||||
$noteUnreadsRepository,
|
||||
$pollsRepository,
|
||||
$pollVotesRepository,
|
||||
$userProfilesRepository,
|
||||
@@ -676,11 +645,6 @@ const $reversiGamesRepository: Provider = {
|
||||
$flashsRepository,
|
||||
$flashLikesRepository,
|
||||
$userMemosRepository,
|
||||
$chatMessagesRepository,
|
||||
$chatRoomsRepository,
|
||||
$chatRoomMembershipsRepository,
|
||||
$chatRoomInvitationsRepository,
|
||||
$chatApprovalsRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
],
|
||||
|
@@ -225,17 +225,6 @@ export class MiUser {
|
||||
})
|
||||
public emojis: string[];
|
||||
|
||||
// チャットを許可する相手
|
||||
// everyone: 誰からでも
|
||||
// followers: フォロワーのみ
|
||||
// following: フォローしているユーザーのみ
|
||||
// mutual: 相互フォローのみ
|
||||
// none: 誰からも受け付けない
|
||||
@Column('varchar', {
|
||||
length: 128, default: 'mutual',
|
||||
})
|
||||
public chatScope: 'everyone' | 'followers' | 'following' | 'mutual' | 'none';
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
|
@@ -3,10 +3,13 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
|
||||
import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder, TypeORMError } from 'typeorm';
|
||||
import { DriverUtils } from 'typeorm/driver/DriverUtils.js';
|
||||
import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js';
|
||||
import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js';
|
||||
import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
|
||||
import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
|
||||
import { OrmUtils } from 'typeorm/util/OrmUtils.js';
|
||||
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
|
||||
import { MiAccessToken } from '@/models/AccessToken.js';
|
||||
@@ -40,6 +43,7 @@ import { MiNote } from '@/models/Note.js';
|
||||
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
||||
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
||||
import { MiNoteUnread } from '@/models/NoteUnread.js';
|
||||
import { MiPage } from '@/models/Page.js';
|
||||
import { MiPageLike } from '@/models/PageLike.js';
|
||||
import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js';
|
||||
@@ -74,11 +78,6 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
|
||||
import { MiFlash } from '@/models/Flash.js';
|
||||
import { MiFlashLike } from '@/models/FlashLike.js';
|
||||
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
|
||||
import { MiChatMessage } from '@/models/ChatMessage.js';
|
||||
import { MiChatRoom } from '@/models/ChatRoom.js';
|
||||
import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
|
||||
import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
|
||||
@@ -160,6 +159,7 @@ export {
|
||||
MiNoteFavorite,
|
||||
MiNoteReaction,
|
||||
MiNoteThreadMuting,
|
||||
MiNoteUnread,
|
||||
MiPage,
|
||||
MiPageLike,
|
||||
MiPasswordResetRequest,
|
||||
@@ -194,11 +194,6 @@ export {
|
||||
MiFlash,
|
||||
MiFlashLike,
|
||||
MiUserMemo,
|
||||
MiChatMessage,
|
||||
MiChatRoom,
|
||||
MiChatRoomMembership,
|
||||
MiChatRoomInvitation,
|
||||
MiChatApproval,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
};
|
||||
@@ -236,6 +231,7 @@ export type NotesRepository = Repository<MiNote> & MiRepository<MiNote>;
|
||||
export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository<MiNoteFavorite>;
|
||||
export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>;
|
||||
export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>;
|
||||
export type NoteUnreadsRepository = Repository<MiNoteUnread> & MiRepository<MiNoteUnread>;
|
||||
export type PagesRepository = Repository<MiPage> & MiRepository<MiPage>;
|
||||
export type PageLikesRepository = Repository<MiPageLike> & MiRepository<MiPageLike>;
|
||||
export type PasswordResetRequestsRepository = Repository<MiPasswordResetRequest> & MiRepository<MiPasswordResetRequest>;
|
||||
@@ -270,10 +266,5 @@ export type RoleAssignmentsRepository = Repository<MiRoleAssignment> & MiReposit
|
||||
export type FlashsRepository = Repository<MiFlash> & MiRepository<MiFlash>;
|
||||
export type FlashLikesRepository = Repository<MiFlashLike> & MiRepository<MiFlashLike>;
|
||||
export type UserMemoRepository = Repository<MiUserMemo> & MiRepository<MiUserMemo>;
|
||||
export type ChatMessagesRepository = Repository<MiChatMessage> & MiRepository<MiChatMessage>;
|
||||
export type ChatRoomsRepository = Repository<MiChatRoom> & MiRepository<MiChatRoom>;
|
||||
export type ChatRoomMembershipsRepository = Repository<MiChatRoomMembership> & MiRepository<MiChatRoomMembership>;
|
||||
export type ChatRoomInvitationsRepository = Repository<MiChatRoomInvitation> & MiRepository<MiChatRoomInvitation>;
|
||||
export type ChatApprovalsRepository = Repository<MiChatApproval> & MiRepository<MiChatApproval>;
|
||||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
|
||||
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
|
||||
|
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedChatMessageSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
fromUserId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
fromUser: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
toUserId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
toUser: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
toRoomId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
toRoom: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
ref: 'ChatRoom',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
file: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
ref: 'DriveFile',
|
||||
},
|
||||
isRead: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedChatMessageLiteSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
fromUserId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
toUserId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
toRoomId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
file: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
ref: 'DriveFile',
|
||||
},
|
||||
},
|
||||
} as const;
|
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedChatRoomInvitationSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
roomId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
room: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'ChatRoom',
|
||||
},
|
||||
},
|
||||
} as const;
|
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedChatRoomMembershipSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
},
|
||||
} as const;
|
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedChatRoomSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
ownerId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
owner: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
@@ -292,10 +292,6 @@ export const packedRolePoliciesSchema = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canChat: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@@ -358,11 +358,6 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||
nullable: false, optional: false,
|
||||
enum: ['public', 'followers', 'private'],
|
||||
},
|
||||
chatScope: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
enum: ['everyone', 'following', 'followers', 'mutual', 'none'],
|
||||
},
|
||||
roles: {
|
||||
type: 'array',
|
||||
nullable: false, optional: false,
|
||||
@@ -545,10 +540,6 @@ export const packedMeDetailedOnlySchema = {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
hasUnreadChatMessages: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
hasUnreadNotification: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
|
@@ -8,9 +8,6 @@ import pg from 'pg';
|
||||
import { DataSource, Logger } from 'typeorm';
|
||||
import * as highlight from 'cli-highlight';
|
||||
import { entities as charts } from '@/core/chart/entities.js';
|
||||
import { Config } from '@/config.js';
|
||||
import MisskeyLogger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
|
||||
@@ -45,6 +42,7 @@ import { MiNote } from '@/models/Note.js';
|
||||
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
||||
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
||||
import { MiNoteUnread } from '@/models/NoteUnread.js';
|
||||
import { MiPage } from '@/models/Page.js';
|
||||
import { MiPageLike } from '@/models/PageLike.js';
|
||||
import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js';
|
||||
@@ -78,14 +76,13 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
|
||||
import { MiFlash } from '@/models/Flash.js';
|
||||
import { MiFlashLike } from '@/models/FlashLike.js';
|
||||
import { MiUserMemo } from '@/models/UserMemo.js';
|
||||
import { MiChatMessage } from '@/models/ChatMessage.js';
|
||||
import { MiChatRoom } from '@/models/ChatRoom.js';
|
||||
import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
|
||||
import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiSystemAccount } from '@/models/SystemAccount.js';
|
||||
|
||||
import { Config } from '@/config.js';
|
||||
import MisskeyLogger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MiSystemAccount } from './models/SystemAccount.js';
|
||||
|
||||
pg.types.setTypeParser(20, Number);
|
||||
|
||||
@@ -198,6 +195,7 @@ export const entities = [
|
||||
MiNoteFavorite,
|
||||
MiNoteReaction,
|
||||
MiNoteThreadMuting,
|
||||
MiNoteUnread,
|
||||
MiPage,
|
||||
MiPageLike,
|
||||
MiGalleryPost,
|
||||
@@ -238,11 +236,6 @@ export const entities = [
|
||||
MiFlash,
|
||||
MiFlashLike,
|
||||
MiUserMemo,
|
||||
MiChatMessage,
|
||||
MiChatRoom,
|
||||
MiChatRoomMembership,
|
||||
MiChatRoomInvitation,
|
||||
MiChatApproval,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
...charts,
|
||||
|
@@ -6,7 +6,6 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ZipReader } from 'slacc';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { EmojisRepository, DriveFilesRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
@@ -87,7 +86,6 @@ export class ImportCustomEmojisProcessorService {
|
||||
const emojiPath = outputPath + '/' + record.fileName;
|
||||
await this.emojisRepository.delete({
|
||||
name: emojiInfo.name,
|
||||
host: IsNull(),
|
||||
});
|
||||
|
||||
try {
|
||||
|
@@ -13,7 +13,7 @@ import accepts from 'accepts';
|
||||
import vary from 'vary';
|
||||
import secureJson from 'secure-json-parse';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
|
||||
import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js';
|
||||
import * as url from '@/misc/prelude/url.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
@@ -42,9 +42,6 @@ export class ActivityPubServerService {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -105,11 +102,6 @@ export class ActivityPubServerService {
|
||||
|
||||
@bindThis
|
||||
private inbox(request: FastifyRequest, reply: FastifyReply) {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
let signature;
|
||||
|
||||
try {
|
||||
@@ -181,11 +173,6 @@ export class ActivityPubServerService {
|
||||
request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = request.params.user;
|
||||
|
||||
const cursor = request.query.cursor;
|
||||
@@ -278,11 +265,6 @@ export class ActivityPubServerService {
|
||||
request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = request.params.user;
|
||||
|
||||
const cursor = request.query.cursor;
|
||||
@@ -372,11 +354,6 @@ export class ActivityPubServerService {
|
||||
|
||||
@bindThis
|
||||
private async featured(request: FastifyRequest<{ Params: { user: string; }; }>, reply: FastifyReply) {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = request.params.user;
|
||||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
@@ -421,11 +398,6 @@ export class ActivityPubServerService {
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = request.params.user;
|
||||
|
||||
const sinceId = request.query.since_id;
|
||||
@@ -510,11 +482,6 @@ export class ActivityPubServerService {
|
||||
|
||||
@bindThis
|
||||
private async userInfo(request: FastifyRequest, reply: FastifyReply, user: MiUser | null) {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
reply.code(404);
|
||||
return;
|
||||
@@ -597,11 +564,6 @@ export class ActivityPubServerService {
|
||||
fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
||||
vary(reply.raw, 'Accept');
|
||||
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const note = await this.notesRepository.findOneBy({
|
||||
id: request.params.note,
|
||||
visibility: In(['public', 'home']),
|
||||
@@ -632,11 +594,6 @@ export class ActivityPubServerService {
|
||||
fastify.get<{ Params: { note: string; } }>('/notes/:note/activity', async (request, reply) => {
|
||||
vary(reply.raw, 'Accept');
|
||||
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const note = await this.notesRepository.findOneBy({
|
||||
id: request.params.note,
|
||||
userHost: IsNull(),
|
||||
@@ -677,11 +634,6 @@ export class ActivityPubServerService {
|
||||
|
||||
// publickey
|
||||
fastify.get<{ Params: { user: string; } }>('/users/:user/publickey', async (request, reply) => {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = request.params.user;
|
||||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
@@ -709,11 +661,6 @@ export class ActivityPubServerService {
|
||||
fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
||||
vary(reply.raw, 'Accept');
|
||||
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = request.params.user;
|
||||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
@@ -727,11 +674,6 @@ export class ActivityPubServerService {
|
||||
fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
||||
vary(reply.raw, 'Accept');
|
||||
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const acct = Acct.parse(request.params.acct);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
@@ -746,11 +688,6 @@ export class ActivityPubServerService {
|
||||
|
||||
// emoji
|
||||
fastify.get<{ Params: { emoji: string; } }>('/emojis/:emoji', async (request, reply) => {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const emoji = await this.emojisRepository.findOneBy({
|
||||
host: IsNull(),
|
||||
name: request.params.emoji,
|
||||
@@ -768,11 +705,6 @@ export class ActivityPubServerService {
|
||||
|
||||
// like
|
||||
fastify.get<{ Params: { like: string; } }>('/likes/:like', async (request, reply) => {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const reaction = await this.noteReactionsRepository.findOneBy({ id: request.params.like });
|
||||
|
||||
if (reaction == null) {
|
||||
@@ -794,11 +726,6 @@ export class ActivityPubServerService {
|
||||
|
||||
// follow
|
||||
fastify.get<{ Params: { follower: string; followee: string; } }>('/follows/:follower/:followee', async (request, reply) => {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
// This may be used before the follow is completed, so we do not
|
||||
// check if the following exists.
|
||||
|
||||
@@ -825,11 +752,6 @@ export class ActivityPubServerService {
|
||||
|
||||
// follow
|
||||
fastify.get<{ Params: { followRequestId: string; } }>('/follows/:followRequestId', async (request, reply) => {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
// This may be used before the follow is completed, so we do not
|
||||
// check if the following exists and only check if the follow request exists.
|
||||
|
||||
|
@@ -44,8 +44,6 @@ import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
|
||||
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
|
||||
import { UserListChannelService } from './api/stream/channels/user-list.js';
|
||||
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
|
||||
import { ChatUserChannelService } from './api/stream/channels/chat-user.js';
|
||||
import { ChatRoomChannelService } from './api/stream/channels/chat-room.js';
|
||||
import { ReversiChannelService } from './api/stream/channels/reversi.js';
|
||||
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
|
||||
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
|
||||
@@ -86,8 +84,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
||||
GlobalTimelineChannelService,
|
||||
HashtagChannelService,
|
||||
RoleTimelineChannelService,
|
||||
ChatUserChannelService,
|
||||
ChatRoomChannelService,
|
||||
ReversiChannelService,
|
||||
ReversiGameChannelService,
|
||||
HomeTimelineChannelService,
|
||||
|
@@ -8,7 +8,7 @@ import { IsNull } from 'typeorm';
|
||||
import vary from 'vary';
|
||||
import fastifyAccepts from '@fastify/accepts';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiMeta, UsersRepository } from '@/models/_.js';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
@@ -26,9 +26,6 @@ export class WellKnownServerService {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -69,11 +66,6 @@ export class WellKnownServerService {
|
||||
});
|
||||
|
||||
fastify.get('/.well-known/host-meta', async (request, reply) => {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.header('Content-Type', xrd);
|
||||
return XRD({ element: 'Link', attributes: {
|
||||
rel: 'lrdd',
|
||||
@@ -83,11 +75,6 @@ export class WellKnownServerService {
|
||||
});
|
||||
|
||||
fastify.get('/.well-known/host-meta.json', async (request, reply) => {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.header('Content-Type', 'application/json');
|
||||
return {
|
||||
links: [{
|
||||
@@ -99,11 +86,6 @@ export class WellKnownServerService {
|
||||
});
|
||||
|
||||
fastify.get('/.well-known/nodeinfo', async (request, reply) => {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
return { links: this.nodeinfoServerService.getLinks() };
|
||||
});
|
||||
|
||||
@@ -117,11 +99,6 @@ fastify.get('/.well-known/change-password', async (request, reply) => {
|
||||
*/
|
||||
|
||||
fastify.get<{ Querystring: { resource: string } }>(webFingerPath, async (request, reply) => {
|
||||
if (this.meta.federation === 'none') {
|
||||
reply.code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const fromId = (id: MiUser['id']): FindOptionsWhere<MiUser> => ({
|
||||
id,
|
||||
host: IsNull(),
|
||||
|
@@ -391,10 +391,10 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
if (ep.meta.requiredRolePolicy != null && (this.meta.rootUserId !== user!.id)) {
|
||||
if (ep.meta.requireRolePolicy != null && (this.meta.rootUserId !== user!.id)) {
|
||||
const myRoles = await this.roleService.getUserRoles(user!.id);
|
||||
const policies = await this.roleService.getUserPolicies(user!.id);
|
||||
if (!policies[ep.meta.requiredRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
|
||||
if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
|
||||
throw new ApiError({
|
||||
message: 'You are not assigned to a required role.',
|
||||
code: 'ROLE_PERMISSION_DENIED',
|
||||
|
@@ -6,6 +6,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import cors from '@fastify/cors';
|
||||
import multipart from '@fastify/multipart';
|
||||
import fastifyCookie from '@fastify/cookie';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -56,6 +57,8 @@ export class ApiServerService {
|
||||
},
|
||||
});
|
||||
|
||||
fastify.register(fastifyCookie, {});
|
||||
|
||||
// Prevent cache
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
|
||||
|
@@ -9,6 +9,7 @@ import * as Redis from 'ioredis';
|
||||
import * as WebSocket from 'ws';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository, MiAccessToken } from '@/models/_.js';
|
||||
import { NoteReadService } from '@/core/NoteReadService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
@@ -34,6 +35,7 @@ export class StreamingApiServerService {
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private noteReadService: NoteReadService,
|
||||
private authenticateService: AuthenticateService,
|
||||
private channelsService: ChannelsService,
|
||||
private notificationService: NotificationService,
|
||||
@@ -94,6 +96,7 @@ export class StreamingApiServerService {
|
||||
|
||||
const stream = new MainStreamConnection(
|
||||
this.channelsService,
|
||||
this.noteReadService,
|
||||
this.notificationService,
|
||||
this.cacheService,
|
||||
this.channelFollowingService,
|
||||
|
@@ -263,6 +263,7 @@ export * as 'i/notifications-grouped' from './endpoints/i/notifications-grouped.
|
||||
export * as 'i/page-likes' from './endpoints/i/page-likes.js';
|
||||
export * as 'i/pages' from './endpoints/i/pages.js';
|
||||
export * as 'i/pin' from './endpoints/i/pin.js';
|
||||
export * as 'i/read-all-unread-notes' from './endpoints/i/read-all-unread-notes.js';
|
||||
export * as 'i/read-announcement' from './endpoints/i/read-announcement.js';
|
||||
export * as 'i/regenerate-token' from './endpoints/i/regenerate-token.js';
|
||||
export * as 'i/registry/get' from './endpoints/i/registry/get.js';
|
||||
@@ -396,22 +397,4 @@ export * as 'users/search' from './endpoints/users/search.js';
|
||||
export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js';
|
||||
export * as 'users/show' from './endpoints/users/show.js';
|
||||
export * as 'users/update-memo' from './endpoints/users/update-memo.js';
|
||||
export * as 'chat/messages/create-to-user' from './endpoints/chat/messages/create-to-user.js';
|
||||
export * as 'chat/messages/create-to-room' from './endpoints/chat/messages/create-to-room.js';
|
||||
export * as 'chat/messages/delete' from './endpoints/chat/messages/delete.js';
|
||||
export * as 'chat/messages/show' from './endpoints/chat/messages/show.js';
|
||||
export * as 'chat/messages/user-timeline' from './endpoints/chat/messages/user-timeline.js';
|
||||
export * as 'chat/messages/room-timeline' from './endpoints/chat/messages/room-timeline.js';
|
||||
export * as 'chat/rooms/create' from './endpoints/chat/rooms/create.js';
|
||||
export * as 'chat/rooms/delete' from './endpoints/chat/rooms/delete.js';
|
||||
export * as 'chat/rooms/join' from './endpoints/chat/rooms/join.js';
|
||||
export * as 'chat/rooms/leave' from './endpoints/chat/rooms/leave.js';
|
||||
export * as 'chat/rooms/show' from './endpoints/chat/rooms/show.js';
|
||||
export * as 'chat/rooms/owned' from './endpoints/chat/rooms/owned.js';
|
||||
export * as 'chat/rooms/update' from './endpoints/chat/rooms/update.js';
|
||||
export * as 'chat/rooms/members' from './endpoints/chat/rooms/members.js';
|
||||
export * as 'chat/rooms/invitations/create' from './endpoints/chat/rooms/invitations/create.js';
|
||||
export * as 'chat/rooms/invitations/ignore' from './endpoints/chat/rooms/invitations/ignore.js';
|
||||
export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitations/inbox.js';
|
||||
export * as 'chat/history' from './endpoints/chat/history.js';
|
||||
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';
|
||||
|
@@ -39,7 +39,7 @@ interface IEndpointMetaBase {
|
||||
*/
|
||||
readonly requireAdmin?: boolean;
|
||||
|
||||
readonly requiredRolePolicy?: KeyOf<'RolePolicies'>;
|
||||
readonly requireRolePolicy?: KeyOf<'RolePolicies'>;
|
||||
|
||||
/**
|
||||
* 引っ越し済みのユーザーによるリクエストを禁止するか
|
||||
|
@@ -12,7 +12,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageAvatarDecorations',
|
||||
requireRolePolicy: 'canManageAvatarDecorations',
|
||||
kind: 'write:admin:avatar-decorations',
|
||||
|
||||
res: {
|
||||
|
@@ -13,7 +13,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageAvatarDecorations',
|
||||
requireRolePolicy: 'canManageAvatarDecorations',
|
||||
kind: 'write:admin:avatar-decorations',
|
||||
errors: {
|
||||
},
|
||||
|
@@ -13,7 +13,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageAvatarDecorations',
|
||||
requireRolePolicy: 'canManageAvatarDecorations',
|
||||
kind: 'read:admin:avatar-decorations',
|
||||
|
||||
res: {
|
||||
|
@@ -13,7 +13,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageAvatarDecorations',
|
||||
requireRolePolicy: 'canManageAvatarDecorations',
|
||||
kind: 'write:admin:avatar-decorations',
|
||||
|
||||
errors: {
|
||||
|
@@ -11,7 +11,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageCustomEmojis',
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
kind: 'write:admin:emoji',
|
||||
} as const;
|
||||
|
||||
|
@@ -16,7 +16,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageCustomEmojis',
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
kind: 'write:admin:emoji',
|
||||
|
||||
errors: {
|
||||
|
@@ -17,7 +17,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageCustomEmojis',
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
kind: 'write:admin:emoji',
|
||||
|
||||
errors: {
|
||||
|
@@ -11,7 +11,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageCustomEmojis',
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
kind: 'write:admin:emoji',
|
||||
} as const;
|
||||
|
||||
|
@@ -11,7 +11,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageCustomEmojis',
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
kind: 'write:admin:emoji',
|
||||
|
||||
errors: {
|
||||
|
@@ -10,7 +10,7 @@ import { QueueService } from '@/core/QueueService.js';
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageCustomEmojis',
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@@ -16,7 +16,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageCustomEmojis',
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
kind: 'read:admin:emoji',
|
||||
|
||||
res: {
|
||||
|
@@ -16,7 +16,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageCustomEmojis',
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
kind: 'read:admin:emoji',
|
||||
|
||||
res: {
|
||||
|
@@ -11,7 +11,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageCustomEmojis',
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
kind: 'write:admin:emoji',
|
||||
} as const;
|
||||
|
||||
|
@@ -11,7 +11,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageCustomEmojis',
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
kind: 'write:admin:emoji',
|
||||
} as const;
|
||||
|
||||
|
@@ -11,7 +11,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageCustomEmojis',
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
kind: 'write:admin:emoji',
|
||||
} as const;
|
||||
|
||||
|
@@ -11,7 +11,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageCustomEmojis',
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
kind: 'write:admin:emoji',
|
||||
} as const;
|
||||
|
||||
|
@@ -14,7 +14,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canManageCustomEmojis',
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
kind: 'write:admin:emoji',
|
||||
|
||||
errors: {
|
||||
|
@@ -8,6 +8,7 @@ import * as Redis from 'ioredis';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { NotesRepository, AntennasRepository } from '@/models/_.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';
|
||||
@@ -58,6 +59,9 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.redisForTimelines)
|
||||
private redisForTimelines: Redis.Redis,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@@ -67,6 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private idService: IdService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private noteReadService: NoteReadService,
|
||||
private fanoutTimelineService: FanoutTimelineService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
@@ -119,6 +124,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
this.noteReadService.read(me.id, notes);
|
||||
|
||||
return await this.noteEntityService.packMany(notes, me);
|
||||
});
|
||||
}
|
||||
|
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ChatService } from '@/core/ChatService.js';
|
||||
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:chat',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'ChatMessage',
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
room: { type: 'boolean', default: false },
|
||||
},
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private chatEntityService: ChatEntityService,
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const history = ps.room ? await this.chatService.roomHistory(me.id, ps.limit) : await this.chatService.userHistory(me.id, ps.limit);
|
||||
|
||||
const packedMessages = await this.chatEntityService.packMessagesDetailed(history, me);
|
||||
|
||||
if (ps.room) {
|
||||
const roomIds = history.map(m => m.toRoomId!);
|
||||
const readStateMap = await this.chatService.getRoomReadStateMap(me.id, roomIds);
|
||||
|
||||
for (const message of packedMessages) {
|
||||
message.isRead = readStateMap[message.toRoomId!] ?? false;
|
||||
}
|
||||
} else {
|
||||
const otherIds = history.map(m => m.fromUserId === me.id ? m.toUserId! : m.fromUserId!);
|
||||
const readStateMap = await this.chatService.getUserReadStateMap(me.id, otherIds);
|
||||
|
||||
for (const message of packedMessages) {
|
||||
const otherId = message.fromUserId === me.id ? message.toUserId! : message.fromUserId!;
|
||||
message.isRead = readStateMap[otherId] ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
return packedMessages;
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { ChatService } from '@/core/ChatService.js';
|
||||
import type { DriveFilesRepository, MiUser } from '@/models/_.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canChat',
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:chat',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 500,
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'ChatMessageLite',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: '8098520d-2da5-4e8f-8ee1-df78b55a4ec6',
|
||||
},
|
||||
|
||||
noSuchFile: {
|
||||
message: 'No such file.',
|
||||
code: 'NO_SUCH_FILE',
|
||||
id: 'b6accbd3-1d7b-4d9f-bdb7-eb185bac06db',
|
||||
},
|
||||
|
||||
contentRequired: {
|
||||
message: 'Content required. You need to set text or fileId.',
|
||||
code: 'CONTENT_REQUIRED',
|
||||
id: '340517b7-6d04-42c0-bac1-37ee804e3594',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: 'string', nullable: true, maxLength: 2000 },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
toRoomId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['toRoomId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private getterService: GetterService,
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.chatService.findRoomById(ps.toRoomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
let file = null;
|
||||
if (ps.fileId != null) {
|
||||
file = await this.driveFilesRepository.findOneBy({
|
||||
id: ps.fileId,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (file == null) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
}
|
||||
|
||||
// テキストが無いかつ添付ファイルも無かったらエラー
|
||||
if (ps.text == null && file == null) {
|
||||
throw new ApiError(meta.errors.contentRequired);
|
||||
}
|
||||
|
||||
return await this.chatService.createMessageToRoom(me, room, {
|
||||
text: ps.text,
|
||||
file: file,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user