Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 243361cc12 | |||
| 0a604285a0 | |||
| c99723b59e | |||
| 96bb36f77e | |||
| 388ef3de08 | |||
| ff85eff209 | |||
| a0b9f456e7 | |||
| 7b38806413 | |||
| 978ab2f848 | |||
| 74d9cc4e38 | |||
| 7a29aed17c | |||
| fd61ac5221 | |||
| 96de18b096 | |||
| eda2f587a3 | |||
| f2aeafaddb | |||
| 5a9f54a07a | |||
| 0257d54add | |||
| d18bb1dda0 | |||
| 81c24ddcc5 | |||
| 17e7340933 | |||
| 29f475b17a | |||
| 6d90e09a58 | |||
| 35d4b43c95 | |||
| ef477ce1b5 | |||
| 4bd23c4c8c | |||
| 46a5aa9ede | |||
| 75267f87d5 | |||
| c5f1ce60fa | |||
| 86774ad753 | |||
| de19d9a4d4 | |||
| 165830d6c8 | |||
| ee29f31324 | |||
| f5a89c2533 | |||
| 45d65ffbba | |||
| fc6037af46 | |||
| fff2783c1b | |||
| b2e3e65896 | |||
| f454e820bd | |||
| d5fe6e36ae | |||
| 6bd45a27b1 | |||
| 525b6a7677 | |||
| 7c0806f208 | |||
| 0d4feed6d3 | |||
| 1f0621b085 | |||
| 4c473eb76d | |||
| f7195d54d0 | |||
| 8edf1bc208 | |||
| dd5dd6184a | |||
| 81bf139e3e | |||
| a0e9895764 | |||
| 413324a278 | |||
| e2b68aabae | |||
| 0aaeb6d136 | |||
| fb277501a0 | |||
| 718138509f | |||
| a1ef38a606 | |||
| 4a8b7ef23e | |||
| 1f17161dfb | |||
| 2488708934 | |||
| 628a0c71a9 | |||
| b40f5b9021 | |||
| 0f79db6a4d | |||
| 910e097480 | |||
| a8acbd6e68 | |||
| c19f51a2ab | |||
| 1cf370882e | |||
| 58cb7cf179 | |||
| a51192bac5 | |||
| c500e4392a | |||
| 799226412f | |||
| 0d18c0169f | |||
| 863fb5dd7f | |||
| 177943b06a | |||
| 2aa7630466 | |||
| c96f345f01 | |||
| baf04a1e25 | |||
| 40097d4aac | |||
| 01cd398799 | |||
| 3d206d2a0f | |||
| e8758bf77d | |||
| 278629046a | |||
| 3374a587a1 | |||
| ce721a8066 | |||
| 547d6361b7 | |||
| f765b374c7 | |||
| b0c0d1b294 | |||
| 7f1cd614db | |||
| 33e6ebb2ee | |||
| 65b4458474 | |||
| 9d3f3264fd | |||
| 6c27ab12eb |
@@ -18,8 +18,8 @@ jobs:
|
||||
# Neither Dependabot nor Renovate will change the actual behavior for components.
|
||||
if: >-
|
||||
github.repository == 'misskey-dev/misskey' &&
|
||||
startsWith(github.ref, 'refs/heads/dependabot/') != true &&
|
||||
startsWith(github.ref, 'refs/heads/renovate/') != true
|
||||
startsWith(github.head_ref, 'refs/heads/dependabot/') != true &&
|
||||
startsWith(github.head_ref, 'refs/heads/renovate/') != true
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
|
||||
+37
-5
@@ -1,7 +1,35 @@
|
||||
## 2025.4.1
|
||||
|
||||
### General
|
||||
- Feat: bull-boardに代わるジョブキューの管理ツールが実装されました
|
||||
- Enhance: チャットの新規メッセージをプッシュ通知するように
|
||||
|
||||
### Client
|
||||
- Feat: チャットウィジェットを追加
|
||||
- Feat: デッキにチャットカラムを追加
|
||||
- Enhance: Unicode絵文字をslugから入力する際に`:ok:`のように最後の`:`を入力したあとにUnicode絵文字に変換できるように
|
||||
- Enhance: コントロールパネルでジョブキューをクリアできるように
|
||||
- Enhance: テーマでページヘッダーの色を変更できるように
|
||||
- Enhance: デザインのブラッシュアップ
|
||||
- Fix: ログアウトした際に処理が終了しない問題を修正
|
||||
- Fix: 自動バックアップが設定されている環境でログアウト直前に設定をバックアップするように
|
||||
- Fix: フォルダを開いた状態でメニューからアップロードしてもルートフォルダにアップロードされる問題を修正 #15836
|
||||
- Fix: タイムラインのスクロール位置を記憶するように修正
|
||||
- Fix: ノートの直後のノートを表示する機能で表示が逆順になっていた問題を修正 #15841
|
||||
- Fix: アカウントの移行時にアンテナのフィルターのユーザが更新されない問題を修正 #15843
|
||||
|
||||
### Server
|
||||
- Enhance: ジョブキューの成功/失敗したジョブも一定数・一定期間保存するようにし、後から問題を調査することを容易に
|
||||
- Enhance: フォローしているユーザーならフォロワー限定投稿のノートでもアンテナで検知できるように
|
||||
(Cherry-picked from https://github.com/yojo-art/cherrypick/pull/568 and https://github.com/team-shahu/misskey/pull/38)
|
||||
- Fix: システムアカウントの名前がサーバー名と同期されない問題を修正
|
||||
- Fix: 大文字を含むユーザの URL で紹介された場合に 404 エラーを返す問題 #15813
|
||||
- Fix: リードレプリカ設定時にレコードの追加・更新・削除を伴うクエリを発行した際はmasterノードで実行されるように調整( #10897 )
|
||||
|
||||
## 2025.4.0
|
||||
|
||||
### General
|
||||
- Feat: チャットがリニューアルして復活しました(beta)
|
||||
- Feat: チャット(ダイレクトメッセージ)がリニューアルして復活しました
|
||||
- 既存のDM機能よりも便利で効率的な実装になっています
|
||||
- チャットを受け付ける相手を制限可能です
|
||||
- 誰でも / フォローユーザーのみ / フォロワーのみ / 相互のみ / 受け付けない から選択できます
|
||||
@@ -11,7 +39,8 @@
|
||||
- 過去自分が送ったメッセージ・自分に送られたメッセージの検索が可能です
|
||||
- 参加中のルームをミュートして通知が来ないように設定可能です
|
||||
- メッセージにはリアクションも可能です
|
||||
- Feat: アカウントのマイグレーション時に古いアカウントからロールをコピーできるようになりました。
|
||||
- 現在、リモートユーザーがチャットを受け付ける設定になっているかどうかを取得する術がないため、ローカルユーザー間でのみ利用可能です
|
||||
- Feat: アカウントの移行時に古いアカウントからあたらしいアカウントにロールをコピーできるようになりました。
|
||||
- 管理者がロールの設定でマイグレーション時にコピーするかを指定できるようになります。
|
||||
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
|
||||
- Misskeyネイティブでダッシュボードを実装予定です
|
||||
@@ -29,7 +58,7 @@
|
||||
- プラグイン、テーマ、クライアントに追加されたすべてのアカウント情報も含まれるようになりました
|
||||
- 自動で設定データをサーバーにバックアップできるように
|
||||
- 設定→設定のプロファイル→自動バックアップ で有効にできます
|
||||
- 新しいデバイスからログインしたり、ブラウザから設定データが消えてしまったときに自動で復元されます(復元をスキップすることも可能)
|
||||
- ログインしたとき、ブラウザから設定データが消えてしまったときに自動で復元されます(復元をスキップすることも可能)
|
||||
- 任意の設定項目をデバイス間で同期できるように
|
||||
- 設定項目の「...」メニュー→「デバイス間で同期」
|
||||
- 同期をオンにした際にサーバーに保存された値とローカルの値が競合する場合はどちらを優先するか選択できます
|
||||
@@ -38,9 +67,10 @@
|
||||
- アカウントごとに設定値が分離される設定とそうでないクライアント設定が混在していた(かつ分離するかどうかを設定不可だった)のを、基本的に一律でクライアント全体に適用されるようにし、個別でアカウントごとに異なる設定を行えるように
|
||||
- 設定項目の「...」メニュー→「アカウントで上書き」をオンにすることで、設定値をそのアカウントでだけ適用するようにできます
|
||||
- ログアウトすると設定データもブラウザから消去されるようになりプライバシーが向上しました
|
||||
- 再度ログインすればサーバーのバックアップから設定データを復元可能です
|
||||
- バックアップを有効にしている場合、ログインした後にバックアップから設定データを復元可能です
|
||||
- エクスポートした設定データを他のサーバーでインポートして適用すること(設定の持ち運び)が可能になりました
|
||||
- 設定情報の移行は自動で行われますが、何らかの理由で失敗した場合、設定→その他→旧設定情報を移行 で再試行可能です
|
||||
- 過去に作成されたバックアップデータとは現在互換性がありませんのでご注意ください
|
||||
- Feat: 画面を重ねて表示するオプションを実装(実験的)
|
||||
- 設定 → その他 → 実験的機能 → Enable stacking router view
|
||||
- Enhance: プラグインの管理が強化されました
|
||||
@@ -59,11 +89,13 @@
|
||||
- Enhance: 2段階認証時のリカバリーコードのファイル名にサーバーURLを含めるように
|
||||
- Enhance: 全体的なブラッシュアップ
|
||||
- Enhance 全体的なパフォーマンス向上
|
||||
- Enhance: ファイルのアップロードでデフォルトで圧縮するかどうかのオプションが廃止され、アップロード時に圧縮するかどうかを選択するようになりました
|
||||
- 画像データの貼り付け、ドロップ時は圧縮されるようになりました
|
||||
- Fix: 読み込み直後にスクロールしようとすると途中で止まる場合があるのを修正
|
||||
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
|
||||
- Fix: iPadOSでdeck uiをマウスカーソルによってスクロールできない問題を修正
|
||||
- NOTE: 構造上クラシックUIを新しいデザインシステムに移行することが困難なため、クラシックUIが削除されました
|
||||
- デッキUIでカラムを中央寄せにし、メインカラムの左右にウィジェットカラムを配置し、ナビゲーションバーを上部に表示することである程度クラシックUIを再現できます
|
||||
- Fix: iPadOSでdeck uiをマウスカーソルによってスクロールできない問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance 全体的なパフォーマンス向上
|
||||
|
||||
+22
-16
@@ -356,7 +356,7 @@ banner: "Bàner"
|
||||
displayOfSensitiveMedia: "Visualització de contingut sensible"
|
||||
whenServerDisconnected: "Quan es perdi la connexió al servidor"
|
||||
disconnectedFromServer: "Desconnectat pel servidor"
|
||||
reload: "Actualitza"
|
||||
reload: "Actualitzar"
|
||||
doNothing: "Ignora"
|
||||
reloadConfirm: "Vols recarregar?"
|
||||
watch: "Veure"
|
||||
@@ -424,7 +424,7 @@ antennaExcludeBots: "Exclou els bots"
|
||||
antennaKeywordsDescription: "Separar amb espais per la condició AND o amb salts de línia per la condició OR."
|
||||
notifyAntenna: "Notifica'm les publicacions noves"
|
||||
withFileAntenna: "Només les publicacions amb fitxers"
|
||||
hideNotesInSensitiveChannel: "Amaga les notes a canals sensibles "
|
||||
excludeNotesInSensitiveChannel: "Excloure notes a canals sensibles"
|
||||
enableServiceworker: "Activar les notificacions al navegador"
|
||||
antennaUsersDescription: "Llistar un nom d'usuari per línia"
|
||||
caseSensitive: "Sensible a majúscules i minúscules "
|
||||
@@ -537,7 +537,7 @@ mediaListWithOneImageAppearance: "Altura de la llista de fitxers amb una única
|
||||
limitTo: "Limita a {x}"
|
||||
noFollowRequests: "No tens sol·licituds de seguiment"
|
||||
openImageInNewTab: "Obre imatges a una nova pestanya"
|
||||
dashboard: "Taulell de control"
|
||||
dashboard: "Tauler de control"
|
||||
local: "Local"
|
||||
remote: "Remot"
|
||||
total: "Total"
|
||||
@@ -652,7 +652,7 @@ manage: "Administració"
|
||||
plugins: "Extensions"
|
||||
preferencesBackups: "Configuracions de les Còpies de seguretat"
|
||||
deck: "Escriptori"
|
||||
undeck: "Tanca l'escriptori"
|
||||
undeck: "Tanca el tauler"
|
||||
useBlurEffectForModal: "Utilitzar l'efecte de difuminació a modals"
|
||||
useFullReactionPicker: "Utilitza el cercador de reaccions d'escala sencera"
|
||||
width: "Amplada"
|
||||
@@ -708,7 +708,7 @@ notificationSetting: "Paràmetres de notificacions"
|
||||
notificationSettingDesc: "Selecciona els tipus de notificacions que es mostraran"
|
||||
useGlobalSetting: "Fer servir la configuració global"
|
||||
useGlobalSettingDesc: "Si s'activa, es farà servir la configuració de notificacions del teu comte. Si no s'activa es poden fer configuracions individuals."
|
||||
other: "Altre"
|
||||
other: "Altres"
|
||||
regenerateLoginToken: "Regenerar clau de seguretat d'inici de sessió"
|
||||
regenerateLoginTokenDescription: "Regenera la clau de seguretat que es fa servir internament durant l'inici de sessió. Normalment aquesta acció no és necessària. Si es regenera es tancarà la sessió a tots els dispositius amb una sessió activa."
|
||||
theKeywordWhenSearchingForCustomEmoji: "Cercar un emoji personalitzat "
|
||||
@@ -915,7 +915,7 @@ off: "Desactivar"
|
||||
emailRequiredForSignup: "Demanar correu electrònic per registrar-se "
|
||||
unread: "Sense llegir"
|
||||
filter: "Filtrar"
|
||||
controlPanel: "Taulell de control"
|
||||
controlPanel: "Tauler de control"
|
||||
manageAccounts: "Gestionar comptes"
|
||||
makeReactionsPublic: "Reaccions públiques "
|
||||
makeReactionsPublicDescription: "Això fa que totes les teves reaccions siguin visibles públicament "
|
||||
@@ -979,6 +979,7 @@ document: "Documentació"
|
||||
numberOfPageCache: "Nombre de pàgines a la memòria cau"
|
||||
numberOfPageCacheDescription: "Incrementant aquest nombre farà que millori l'experiència de l'usuari, però es farà servir més memòria al dispositiu de l'usuari."
|
||||
logoutConfirm: "Vols sortir?"
|
||||
logoutWillClearClientData: "En tancar la sessió, la informació del client al navegador s'esborrarà. Per garantir que la informació de configuració es pugui restaurar en tornar a iniciar sessió activa la còpia de seguretat automàtica de la configuració."
|
||||
lastActiveDate: "Fet servir per última vegada"
|
||||
statusbar: "Barra d'estat"
|
||||
pleaseSelect: "Selecciona una opció"
|
||||
@@ -1334,14 +1335,16 @@ postForm: "Formulari de publicació"
|
||||
textCount: "Nombre de caràcters "
|
||||
information: "Informació"
|
||||
chat: "Xat"
|
||||
migrateOldSettings: "Migració de la configuració antiga "
|
||||
migrateOldSettings: "Migrar la configuració anterior"
|
||||
migrateOldSettings_description: "Normalment això es fa automàticament, però si la transició no es fa, el procés es pot iniciar manualment. S'esborrarà la configuració actual."
|
||||
compress: "Comprimir "
|
||||
right: "Dreta"
|
||||
bottom: "A baix "
|
||||
top: "A dalt "
|
||||
embed: "Incrustar"
|
||||
settingsMigrating: "Estem fent la migració de la teva configuració. Si us plau espera un moment... (També pots fer la migració més tard i manualment anant a Preferències → Altres configuracions → Migrar configuració antiga)"
|
||||
settingsMigrating: "Estem migrant la teva configuració. Si us plau espera un moment... (També pots fer la migració més tard, manualment, anant a Preferències → Altres → Migrar configuració antiga)"
|
||||
readonly: "Només lectura"
|
||||
goToDeck: "Tornar al tauler"
|
||||
_chat:
|
||||
noMessagesYet: "Encara no tens missatges "
|
||||
newMessage: "Missatge nou"
|
||||
@@ -1357,7 +1360,7 @@ _chat:
|
||||
noInvitations: "No tens cap invitació "
|
||||
history: "Historial de converses "
|
||||
noHistory: "No hi ha un registre previ"
|
||||
noRooms: "No hi ha habitacions"
|
||||
noRooms: "No hi ha cap sala"
|
||||
inviteUser: "Invitar usuaris"
|
||||
sentInvitations: "Enviar invitacions"
|
||||
join: "Afegir-se "
|
||||
@@ -1371,6 +1374,7 @@ _chat:
|
||||
muteThisRoom: "Silenciar aquesta sala"
|
||||
deleteRoom: "Esborrar la sala"
|
||||
chatNotAvailableForThisAccountOrServer: "El xat no està disponible per aquest servidor o aquest compte."
|
||||
chatIsReadOnlyForThisAccountOrServer: "El xat és només de lectura en aquest servidor o compte. No es poden escriure nous missatges ni crear o unir-se a sales de xat."
|
||||
chatNotAvailableInOtherAccount: "La funció de xat es troba desactivada al compte de l'altre usuari."
|
||||
cannotChatWithTheUser: "No pots xatejar amb aquest usuari"
|
||||
cannotChatWithTheUser_description: "El xat està desactivat o l'altra part encara no l'ha obert."
|
||||
@@ -1414,8 +1418,8 @@ _settings:
|
||||
makeEveryTextElementsSelectable_description: "L'activació pot reduir la usabilitat en determinades ocasions."
|
||||
useStickyIcons: "Utilitza icones fixes"
|
||||
showNavbarSubButtons: "Mostrar sub botons a la barra de navegació "
|
||||
ifOn: "Quan s'encén "
|
||||
ifOff: "Quan s'apaga "
|
||||
ifOn: "Quan s'activa"
|
||||
ifOff: "Quan es desactiva"
|
||||
enableSyncThemesBetweenDevices: "Sincronitzar els temes instal·lats entre dispositius"
|
||||
_chat:
|
||||
showSenderName: "Mostrar el nom del remitent"
|
||||
@@ -1601,7 +1605,7 @@ _accountMigration:
|
||||
moveTo: "Migrar aquest compte a un altre"
|
||||
moveToLabel: "Compte al qual es vol migrar:"
|
||||
moveCannotBeUndone: "Les migracions dels comptes no es poden desfer."
|
||||
moveAccountDescription: "Això migrarà la teva compte a un altre diferent.\n ・Els seguidors d'aquest compte és passaran al compte nou de forma automàtica\n ・Es deixaran de seguir a tots els usuaris que es segueixen actualment en aquest compte\n ・No es poden crear notes noves, etc. en aquest compte\n\nSi bé la migració de seguidors es automàtica, has de preparar alguns pasos manualment per migrar la llista d'usuaris que segueixes. Per fer això has d'exportar els seguidors que després importaraes al compte nou mitjançant el menú de configuració. El mateix procediment s'ha de seguir per less teves llistes i els teus usuaris silenciats i bloquejats.\n\n(Aquesta explicació s'aplica a Misskey v13.12.0 i posteriors. Altres aplicacions, com Mastodon, poden funcionar diferent.)"
|
||||
moveAccountDescription: "Això migrarà el teu compte a un altre diferent.\n ・Els seguidors d'aquest compte és passaran al compte nou de forma automàtica\n ・Es deixaran de seguir a tots els usuaris que es segueixen actualment en aquest compte\n ・No es poden crear notes noves, etc. en aquest compte\n\nSi bé la migració de seguidors es automàtica, has de preparar alguns pasos manualment per migrar la llista d'usuaris que segueixes. Per fer això has d'exportar els seguidors que després importaraes al compte nou mitjançant el menú de configuració. El mateix procediment s'ha de seguir per less teves llistes i els teus usuaris silenciats i bloquejats.\n\n(Aquesta explicació s'aplica a Misskey v13.12.0 i posteriors. Altres aplicacions, com Mastodon, poden funcionar diferent.)"
|
||||
moveAccountHowTo: "Per fer la migració, primer has de crear un àlies per aquest compte al compte al qual vols migrar.\nDesprés de crear l'àlies, introdueix el compte al qual vols migrar amb el format següent: @nomusuari@servidor.exemple.com"
|
||||
startMigration: "Migrar"
|
||||
migrationConfirm: "Vols migrar aquest compte a {account}? Una vegada comenci la migració no es podrà parar O fer marxa enrere i no podràs tornar a fer servir aquest compte mai més."
|
||||
@@ -1931,7 +1935,7 @@ _role:
|
||||
canImportFollowing: "Autoritza la importació de seguidors"
|
||||
canImportMuting: "Autoritza la importació de silenciats"
|
||||
canImportUserLists: "Autoritza la importació de llistes d'usuaris "
|
||||
canChat: "Pot xatejar"
|
||||
chatAvailability: "Es permet xatejar"
|
||||
_condition:
|
||||
roleAssignedTo: "Assignat a rols manuals"
|
||||
isLocal: "Usuari local"
|
||||
@@ -2123,7 +2127,7 @@ _theme:
|
||||
fg: "Text"
|
||||
focus: "Enfocament"
|
||||
indicator: "Indicador"
|
||||
panel: "Taulell "
|
||||
panel: "Tauler"
|
||||
shadow: "Ombra"
|
||||
header: "Capçalera"
|
||||
navBg: "Fons de la barra lateral"
|
||||
@@ -2365,6 +2369,7 @@ _widgets:
|
||||
chooseList: "Tria una llista"
|
||||
clicker: "Clicker"
|
||||
birthdayFollowings: "Usuaris que fan l'aniversari avui"
|
||||
chat: "Xat"
|
||||
_cw:
|
||||
hide: "Amagar"
|
||||
show: "Carregar més"
|
||||
@@ -2601,7 +2606,7 @@ _deck:
|
||||
columnGap: "Espai entre columnes"
|
||||
deckMenuPosition: "Posició del menú del tauler"
|
||||
navbarPosition: "Posició de la barra de navegació "
|
||||
addColumn: "Afig una columna"
|
||||
addColumn: "Afegeix una columna"
|
||||
newNoteNotificationSettings: "Configuració de notificacions per a notes noves"
|
||||
configureColumn: "Configuració de columnes"
|
||||
swapLeft: "Mou a l’esquerra"
|
||||
@@ -2631,6 +2636,7 @@ _deck:
|
||||
mentions: "Mencions"
|
||||
direct: "Publicacions directes"
|
||||
roleTimeline: "Línia de temps dels rols"
|
||||
chat: "Xat"
|
||||
_dialog:
|
||||
charactersExceeded: "Has arribat al màxim de caràcters! Actualment és {current} de {max}"
|
||||
charactersBelow: "Ets per sota del mínim de caràcters! Actualment és {current} de {min}"
|
||||
@@ -2796,7 +2802,7 @@ _hemisphere:
|
||||
_reversi:
|
||||
reversi: "Reversi"
|
||||
gameSettings: "Opcions del joc"
|
||||
chooseBoard: "Escull un taulell"
|
||||
chooseBoard: "Escull un tauler"
|
||||
blackOrWhite: "Negres/Blanques"
|
||||
blackIs: "{name} juga amb negres "
|
||||
rules: "Regles"
|
||||
|
||||
+8
-2
@@ -424,7 +424,7 @@ antennaExcludeBots: "Bot-Accounts ausschließen"
|
||||
antennaKeywordsDescription: "Zum Nutzen einer \"UND\"-Verknüpfung Einträge mit Leerzeichen trennen, zum Nutzen einer \"ODER\"-Verknüpfung Einträge mit einem Zeilenumbruch trennen"
|
||||
notifyAntenna: "Über neue Notizen benachrichtigen"
|
||||
withFileAntenna: "Nur Notizen mit Dateien"
|
||||
hideNotesInSensitiveChannel: "Verstecke Notizen von sensitive Kanäle"
|
||||
excludeNotesInSensitiveChannel: "Schließe Notizen von sensitive Kanäle aus"
|
||||
enableServiceworker: "Push-Benachrichtigungen im Browser aktivieren"
|
||||
antennaUsersDescription: "Benutzernamen getrennt durch Zeilenumbrüche angeben"
|
||||
caseSensitive: "Groß-/Kleinschreibung unterscheiden"
|
||||
@@ -1341,6 +1341,9 @@ right: "Rechts"
|
||||
bottom: "Unten"
|
||||
top: "Oben"
|
||||
embed: "Einbetten"
|
||||
settingsMigrating: "Ihre Einstellungen werden gerade migriert, Bitte warten Sie einen Moment... (Sie können die Einstellungen später auch manuell migrieren, indem Sie zu Einstellungen → Sonstiges → Alte Einstellungen migrieren gehen)"
|
||||
readonly: "Nur Lesezugriff"
|
||||
goToDeck: "Zurück zum Deck"
|
||||
_chat:
|
||||
noMessagesYet: "Noch keine Nachrichten"
|
||||
newMessage: "Neue Nachricht"
|
||||
@@ -1370,6 +1373,7 @@ _chat:
|
||||
muteThisRoom: "Raum stummschalten"
|
||||
deleteRoom: "Raum löschen"
|
||||
chatNotAvailableForThisAccountOrServer: "Der Chat ist auf diesem Server oder für dieses Konto nicht aktiviert."
|
||||
chatIsReadOnlyForThisAccountOrServer: "Der Chat ist auf dieser Instanz oder diesem Konto nur zum Lesen freigegeben. Es ist nicht möglich, neue Nachrichten zu schreiben oder Chaträume zu erstellen oder zu betreten."
|
||||
chatNotAvailableInOtherAccount: "Die Chatfunktion wurde vom anderen Benutzer deaktiviert."
|
||||
cannotChatWithTheUser: "Starten eines Chats mit diesem Benutzer nicht möglich"
|
||||
cannotChatWithTheUser_description: "Der Chat ist entweder nicht verfügbar oder die andere Seite hat den Chat nicht aktiviert."
|
||||
@@ -1930,7 +1934,7 @@ _role:
|
||||
canImportFollowing: "Importieren von Gefolgten zulassen"
|
||||
canImportMuting: "Importieren von Stummgeschalteten zulassen"
|
||||
canImportUserLists: "Importieren von Listen erlauben"
|
||||
canChat: "Chatten erlauben"
|
||||
chatAvailability: "Chatten erlauben"
|
||||
_condition:
|
||||
roleAssignedTo: "Manuellen Rollen zugewiesen"
|
||||
isLocal: "Lokaler Benutzer"
|
||||
@@ -2364,6 +2368,7 @@ _widgets:
|
||||
chooseList: "Liste auswählen"
|
||||
clicker: "Klickzähler"
|
||||
birthdayFollowings: "Nutzer, die heute Geburtstag haben"
|
||||
chat: "Chat"
|
||||
_cw:
|
||||
hide: "Inhalt verbergen"
|
||||
show: "Inhalt anzeigen"
|
||||
@@ -2630,6 +2635,7 @@ _deck:
|
||||
mentions: "Erwähnungen"
|
||||
direct: "Direktnachrichten"
|
||||
roleTimeline: "Rollenchronik"
|
||||
chat: "Chat"
|
||||
_dialog:
|
||||
charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}"
|
||||
charactersBelow: "Minimallänge unterschritten! Momentan {current} von {min}"
|
||||
|
||||
+8
-2
@@ -424,7 +424,7 @@ antennaExcludeBots: "Exclude bot accounts"
|
||||
antennaKeywordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition."
|
||||
notifyAntenna: "Notify about new notes"
|
||||
withFileAntenna: "Only notes with files"
|
||||
hideNotesInSensitiveChannel: "Hide notes from sensitive channels"
|
||||
excludeNotesInSensitiveChannel: "Exclude notes from sensitive channels"
|
||||
enableServiceworker: "Enable Push-Notifications for your Browser"
|
||||
antennaUsersDescription: "List one username per line"
|
||||
caseSensitive: "Case sensitive"
|
||||
@@ -1341,6 +1341,9 @@ right: "Right"
|
||||
bottom: "Bottom"
|
||||
top: "Top"
|
||||
embed: "Embed"
|
||||
settingsMigrating: "Settings are being migrated, please wait a moment... (You can also migrate manually later by going to Settings→Others→Migrate old settings)"
|
||||
readonly: "Read only"
|
||||
goToDeck: "Return to Deck"
|
||||
_chat:
|
||||
noMessagesYet: "No messages yet"
|
||||
newMessage: "New message"
|
||||
@@ -1370,6 +1373,7 @@ _chat:
|
||||
muteThisRoom: "Mute room"
|
||||
deleteRoom: "Delete room"
|
||||
chatNotAvailableForThisAccountOrServer: "Chat is not enabled on this server or for this account."
|
||||
chatIsReadOnlyForThisAccountOrServer: "Chat is read-only on this instance or this account. You cannot write new messages or create/join chat rooms."
|
||||
chatNotAvailableInOtherAccount: "The chat function is disabled for the other user."
|
||||
cannotChatWithTheUser: "Cannot start a chat with this user"
|
||||
cannotChatWithTheUser_description: "Chat is either unavailable or the other party has not enabled chat."
|
||||
@@ -1930,7 +1934,7 @@ _role:
|
||||
canImportFollowing: "Allow importing following"
|
||||
canImportMuting: "Allow importing muting"
|
||||
canImportUserLists: "Allow importing lists"
|
||||
canChat: "Allow Chat"
|
||||
chatAvailability: "Allow Chat"
|
||||
_condition:
|
||||
roleAssignedTo: "Assigned to manual roles"
|
||||
isLocal: "Local user"
|
||||
@@ -2364,6 +2368,7 @@ _widgets:
|
||||
chooseList: "Select a list"
|
||||
clicker: "Clicker"
|
||||
birthdayFollowings: "Today's Birthdays"
|
||||
chat: "Chat"
|
||||
_cw:
|
||||
hide: "Hide"
|
||||
show: "Show content"
|
||||
@@ -2630,6 +2635,7 @@ _deck:
|
||||
mentions: "Mentions"
|
||||
direct: "Direct notes"
|
||||
roleTimeline: "Role Timeline"
|
||||
chat: "Chat"
|
||||
_dialog:
|
||||
charactersExceeded: "You've exceeded the maximum character limit! Currently at {current} of {max}."
|
||||
charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}."
|
||||
|
||||
@@ -301,6 +301,7 @@ uploadFromUrlMayTakeTime: "Subir el fichero puede tardar un tiempo."
|
||||
explore: "Explorar"
|
||||
messageRead: "Ya leído"
|
||||
noMoreHistory: "El historial se ha acabado"
|
||||
startChat: "Nuevo Chat"
|
||||
nUsersRead: "Leído por {n} personas"
|
||||
agreeTo: "De acuerdo con {0}"
|
||||
agree: "De acuerdo."
|
||||
@@ -694,6 +695,7 @@ userSaysSomethingAbout: "{name} dijo algo sobre {word}"
|
||||
makeActive: "Activar"
|
||||
display: "Apariencia"
|
||||
copy: "Copiar"
|
||||
copiedToClipboard: "Texto copiado al portapapeles"
|
||||
metrics: "Métricas"
|
||||
overview: "Resumen"
|
||||
logs: "Registros"
|
||||
@@ -1293,18 +1295,55 @@ passkeyVerificationFailed: "La verificación de la clave de acceso ha fallado."
|
||||
passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificación de la clave de acceso ha sido satisfactoria pero se ha deshabilitado el inicio de sesión sin contraseña."
|
||||
messageToFollower: "Mensaje a seguidores"
|
||||
target: "Para"
|
||||
prohibitedWordsForNameOfUser: "Palabras prohibidas para nombres de usuario"
|
||||
prohibitedWordsForNameOfUserDescription: "Si alguna de las cadenas de esta lista está incluida en el nombre del usuario, el nombre será denegado. Los usuarios con privilegios de moderador no se ven afectados por esta restricción."
|
||||
yourNameContainsProhibitedWords: "Tu nombre contiene palabras prohibidas"
|
||||
yourNameContainsProhibitedWordsDescription: "Si deseas usar este nombre, por favor contacta con tu administrador/a de tu servidor"
|
||||
lockdown: "Bloqueo"
|
||||
pleaseSelectAccount: "Seleccione una cuenta, por favor."
|
||||
availableRoles: "Roles disponibles "
|
||||
acknowledgeNotesAndEnable: "Activar después de comprender las precauciones"
|
||||
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"
|
||||
preferences: "Preferencias"
|
||||
postForm: "Formulario"
|
||||
information: "Información"
|
||||
right: "Derecha"
|
||||
bottom: "Abajo"
|
||||
top: "Arriba"
|
||||
embed: "Insertar"
|
||||
settingsMigrating: "La configuración está siendo migrada, por favor espera un momento... (También puedes migrar manualmente más tarde yendo a Ajustes otros migrar configuración antigua"
|
||||
readonly: "Solo Lectura"
|
||||
_chat:
|
||||
noMessagesYet: "Aún no hay mensajes"
|
||||
newMessage: "Mensajes nuevos"
|
||||
individualChat: "Chat individual"
|
||||
individualChat_description: "Mantén una conversación privada con otra persona."
|
||||
invitations: "Invitar"
|
||||
noHistory: "No hay datos en el historial"
|
||||
members: "Miembros"
|
||||
home: "Inicio"
|
||||
send: "Enviar"
|
||||
chatNotAvailableInOtherAccount: "La función de chat está desactivada para el otro usuario."
|
||||
cannotChatWithTheUser: "No se puede iniciar un chat con este usuario"
|
||||
cannotChatWithTheUser_description: "El chat no está disponible o la otra parte no ha habilitado el chat."
|
||||
chatWithThisUser: "Chatear"
|
||||
thisUserAllowsChatOnlyFromFollowers: "Este usuario sólo acepta chats de seguidores."
|
||||
thisUserAllowsChatOnlyFromFollowing: "Este usuario sólo acepta chats de los usuarios a los que sigue."
|
||||
thisUserAllowsChatOnlyFromMutualFollowing: "Este usuario sólo acepta chats de usuarios que son seguidores mutuos."
|
||||
thisUserNotAllowedChatAnyone: "Este usuario no acepta chats de nadie."
|
||||
chatAllowedUsers: "A quién permitir chatear."
|
||||
chatAllowedUsers_note: "Puedes chatear con cualquier persona a la que hayas enviado un mensaje de chat, independientemente de esta configuración."
|
||||
_chatAllowedUsers:
|
||||
everyone: "Todos"
|
||||
followers: "Sólo sus propios seguidores."
|
||||
following: "Solo usuarios que sigues"
|
||||
mutual: "Solo seguidores mutuos"
|
||||
none: "Nadie"
|
||||
_emojiPalette:
|
||||
palettes: "Paleta\n"
|
||||
_settings:
|
||||
api: "API"
|
||||
webhook: "Webhook"
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "Se requiere iniciar sesión para ver el contenido"
|
||||
|
||||
Vendored
+31
-3
@@ -1715,9 +1715,9 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"withFileAntenna": string;
|
||||
/**
|
||||
* センシティブなチャンネルのノートを非表示
|
||||
* センシティブなチャンネルのノートを除外
|
||||
*/
|
||||
"hideNotesInSensitiveChannel": string;
|
||||
"excludeNotesInSensitiveChannel": string;
|
||||
/**
|
||||
* ブラウザへのプッシュ通知を有効にする
|
||||
*/
|
||||
@@ -3934,6 +3934,10 @@ export interface Locale extends ILocale {
|
||||
* ログアウトしますか?
|
||||
*/
|
||||
"logoutConfirm": string;
|
||||
/**
|
||||
* ログアウトするとクライアントの設定情報がブラウザから消去されます。再ログイン時に設定情報を復元できるようにするためには、設定の自動バックアップを有効にしてください。
|
||||
*/
|
||||
"logoutWillClearClientData": string;
|
||||
/**
|
||||
* 最終利用日時
|
||||
*/
|
||||
@@ -5386,6 +5390,18 @@ export interface Locale extends ILocale {
|
||||
* 設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます)
|
||||
*/
|
||||
"settingsMigrating": string;
|
||||
/**
|
||||
* 読み取り専用
|
||||
*/
|
||||
"readonly": string;
|
||||
/**
|
||||
* デッキへ戻る
|
||||
*/
|
||||
"goToDeck": string;
|
||||
/**
|
||||
* 連合ジョブ
|
||||
*/
|
||||
"federationJobs": string;
|
||||
"_chat": {
|
||||
/**
|
||||
* まだメッセージはありません
|
||||
@@ -5500,6 +5516,10 @@ export interface Locale extends ILocale {
|
||||
* このサーバー、またはこのアカウントでチャットは有効化されていません。
|
||||
*/
|
||||
"chatNotAvailableForThisAccountOrServer": string;
|
||||
/**
|
||||
* このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。
|
||||
*/
|
||||
"chatIsReadOnlyForThisAccountOrServer": string;
|
||||
/**
|
||||
* 相手のアカウントでチャット機能が使えない状態になっています。
|
||||
*/
|
||||
@@ -7531,7 +7551,7 @@ export interface Locale extends ILocale {
|
||||
/**
|
||||
* チャットを許可
|
||||
*/
|
||||
"canChat": string;
|
||||
"chatAvailability": string;
|
||||
};
|
||||
"_condition": {
|
||||
/**
|
||||
@@ -9191,6 +9211,10 @@ export interface Locale extends ILocale {
|
||||
* 今日誕生日のユーザー
|
||||
*/
|
||||
"birthdayFollowings": string;
|
||||
/**
|
||||
* チャット
|
||||
*/
|
||||
"chat": string;
|
||||
};
|
||||
"_cw": {
|
||||
/**
|
||||
@@ -10214,6 +10238,10 @@ export interface Locale extends ILocale {
|
||||
* ロールタイムライン
|
||||
*/
|
||||
"roleTimeline": string;
|
||||
/**
|
||||
* チャット
|
||||
*/
|
||||
"chat": string;
|
||||
};
|
||||
};
|
||||
"_dialog": {
|
||||
|
||||
+14
-7
@@ -424,7 +424,7 @@ antennaExcludeBots: "Escludere i Bot"
|
||||
antennaKeywordsDescription: "Sparando con uno spazio indichi la condizione E (and). Separando con un a capo, indichi la condizione O (or)."
|
||||
notifyAntenna: "Invia notifiche delle nuove note"
|
||||
withFileAntenna: "Solo note con file in allegato"
|
||||
hideNotesInSensitiveChannel: "Nascondere le Note dai canali espliciti"
|
||||
excludeNotesInSensitiveChannel: "Escludere le Note dai canali espliciti"
|
||||
enableServiceworker: "Abilita ServiceWorker"
|
||||
antennaUsersDescription: "Elenca un nome utente per riga"
|
||||
caseSensitive: "Sensibile alla distinzione tra maiuscole e minuscole"
|
||||
@@ -728,7 +728,7 @@ reporterOrigin: "Segnalazione da"
|
||||
send: "Inviare"
|
||||
openInNewTab: "Apri in una nuova scheda"
|
||||
openInSideView: "Apri in vista laterale"
|
||||
defaultNavigationBehaviour: "Navigazione preimpostata"
|
||||
defaultNavigationBehaviour: "Tipo di navigazione predefinita"
|
||||
editTheseSettingsMayBreakAccount: "Modificare queste impostazioni può danneggiare il profilo"
|
||||
instanceTicker: "Informazioni sull'istanza da cui vengono le note"
|
||||
waitingFor: "Aspettando {x}"
|
||||
@@ -867,7 +867,7 @@ noBotProtectionWarning: "Non è stata impostata alcuna protezione dai Bot"
|
||||
configure: "Imposta"
|
||||
postToGallery: "Pubblicare nella galleria"
|
||||
postToHashtag: "Pubblica a questo hashtag"
|
||||
gallery: "Galleria"
|
||||
gallery: "Gallerie"
|
||||
recentPosts: "Pubblicazioni recenti"
|
||||
popularPosts: "Le più visualizzate"
|
||||
shareWithNote: "Condividere in nota"
|
||||
@@ -979,6 +979,7 @@ document: "Documentazione"
|
||||
numberOfPageCache: "Numero di pagine cache"
|
||||
numberOfPageCacheDescription: "Aumenta l'usabilità, ma aumenta anche il carico e l'utilizzo della memoria."
|
||||
logoutConfirm: "Vuoi davvero uscire da Misskey? "
|
||||
logoutWillClearClientData: "All'uscita, la configurazione del client viene rimossa dal browser. Per ripristinarla quando si effettua nuovamente l'accesso, abilitare il backup automatico."
|
||||
lastActiveDate: "Data dell'ultimo utilizzo"
|
||||
statusbar: "Barra di stato"
|
||||
pleaseSelect: "Scegli un'opzione"
|
||||
@@ -1341,6 +1342,9 @@ right: "Destra"
|
||||
bottom: "Sotto"
|
||||
top: "Sopra"
|
||||
embed: "Incorporare"
|
||||
settingsMigrating: "Migrazione delle impostazioni. Attendere prego ... (Puoi anche migrare manualmente in un secondo momento, nel menu: Impostazioni → Altro → Migrazione delle impostazioni)"
|
||||
readonly: "Sola lettura"
|
||||
goToDeck: "Torna al Deck"
|
||||
_chat:
|
||||
noMessagesYet: "Ancora nessun messaggio"
|
||||
newMessage: "Nuovo messaggio"
|
||||
@@ -1370,6 +1374,7 @@ _chat:
|
||||
muteThisRoom: "Silenzia stanza"
|
||||
deleteRoom: "Elimina stanza"
|
||||
chatNotAvailableForThisAccountOrServer: "Questo server, o questo profilo ha disabilitato la chat."
|
||||
chatIsReadOnlyForThisAccountOrServer: "Le chat, su questo server o su questo profilo, sono di sola lettura. Impossibile scrivere in chat o creare e partecipare a stanze."
|
||||
chatNotAvailableInOtherAccount: "La chat non è disponibile nel profilo dell'altra persona."
|
||||
cannotChatWithTheUser: "Impossibile chattare con questa persona"
|
||||
cannotChatWithTheUser_description: "La chat potrebbe non essere disponibile, oppure l'altra persona potrebbe non esserlo."
|
||||
@@ -1930,7 +1935,7 @@ _role:
|
||||
canImportFollowing: "Può importare Following"
|
||||
canImportMuting: "Può importare Silenziati"
|
||||
canImportUserLists: "Può importare liste di Profili"
|
||||
canChat: "Chat consentita"
|
||||
chatAvailability: "Chat consentita"
|
||||
_condition:
|
||||
roleAssignedTo: "Assegnato a ruoli manualmente"
|
||||
isLocal: "Profilo locale"
|
||||
@@ -2364,6 +2369,7 @@ _widgets:
|
||||
chooseList: "Seleziona una lista"
|
||||
clicker: "Cliccheria"
|
||||
birthdayFollowings: "Compleanni del giorno"
|
||||
chat: "Chat"
|
||||
_cw:
|
||||
hide: "Nascondere"
|
||||
show: "Continua la lettura..."
|
||||
@@ -2596,8 +2602,8 @@ _notification:
|
||||
renote: "Rinota"
|
||||
_deck:
|
||||
alwaysShowMainColumn: "Mostra sempre la colonna principale"
|
||||
columnAlign: "Allineare colonne"
|
||||
columnGap: "Margine tra le colonne"
|
||||
columnAlign: "Allineamento delle colonne"
|
||||
columnGap: "Spessore del margine tra colonne"
|
||||
deckMenuPosition: "Posizione del menu Deck"
|
||||
navbarPosition: "Posizione barra di navigazione"
|
||||
addColumn: "Aggiungi colonna"
|
||||
@@ -2626,10 +2632,11 @@ _deck:
|
||||
tl: "Timeline"
|
||||
antenna: "Antenne"
|
||||
list: "Liste"
|
||||
channel: "Canale"
|
||||
channel: "Canali"
|
||||
mentions: "Menzioni"
|
||||
direct: "Note Dirette"
|
||||
roleTimeline: "Timeline Ruolo"
|
||||
chat: "Chat"
|
||||
_dialog:
|
||||
charactersExceeded: "Hai superato il limite di {max} caratteri! ({current})"
|
||||
charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({current})"
|
||||
|
||||
+9
-2
@@ -424,7 +424,7 @@ antennaExcludeBots: "Botアカウントを除外"
|
||||
antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
|
||||
notifyAntenna: "新しいノートを通知する"
|
||||
withFileAntenna: "ファイルが添付されたノートのみ"
|
||||
hideNotesInSensitiveChannel: "センシティブなチャンネルのノートを非表示"
|
||||
excludeNotesInSensitiveChannel: "センシティブなチャンネルのノートを除外"
|
||||
enableServiceworker: "ブラウザへのプッシュ通知を有効にする"
|
||||
antennaUsersDescription: "ユーザー名を改行で区切って指定します"
|
||||
caseSensitive: "大文字小文字を区別する"
|
||||
@@ -979,6 +979,7 @@ document: "ドキュメント"
|
||||
numberOfPageCache: "ページキャッシュ数"
|
||||
numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。"
|
||||
logoutConfirm: "ログアウトしますか?"
|
||||
logoutWillClearClientData: "ログアウトするとクライアントの設定情報がブラウザから消去されます。再ログイン時に設定情報を復元できるようにするためには、設定の自動バックアップを有効にしてください。"
|
||||
lastActiveDate: "最終利用日時"
|
||||
statusbar: "ステータスバー"
|
||||
pleaseSelect: "選択してください"
|
||||
@@ -1342,6 +1343,9 @@ bottom: "下"
|
||||
top: "上"
|
||||
embed: "埋め込み"
|
||||
settingsMigrating: "設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます)"
|
||||
readonly: "読み取り専用"
|
||||
goToDeck: "デッキへ戻る"
|
||||
federationJobs: "連合ジョブ"
|
||||
|
||||
_chat:
|
||||
noMessagesYet: "まだメッセージはありません"
|
||||
@@ -1372,6 +1376,7 @@ _chat:
|
||||
muteThisRoom: "このルームをミュート"
|
||||
deleteRoom: "ルームを削除"
|
||||
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。"
|
||||
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。"
|
||||
chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。"
|
||||
cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
|
||||
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
|
||||
@@ -1950,7 +1955,7 @@ _role:
|
||||
canImportFollowing: "フォローのインポートを許可"
|
||||
canImportMuting: "ミュートのインポートを許可"
|
||||
canImportUserLists: "リストのインポートを許可"
|
||||
canChat: "チャットを許可"
|
||||
chatAvailability: "チャットを許可"
|
||||
_condition:
|
||||
roleAssignedTo: "マニュアルロールにアサイン済み"
|
||||
isLocal: "ローカルユーザー"
|
||||
@@ -2417,6 +2422,7 @@ _widgets:
|
||||
chooseList: "リストを選択"
|
||||
clicker: "クリッカー"
|
||||
birthdayFollowings: "今日誕生日のユーザー"
|
||||
chat: "チャット"
|
||||
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
@@ -2701,6 +2707,7 @@ _deck:
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト"
|
||||
roleTimeline: "ロールタイムライン"
|
||||
chat: "チャット"
|
||||
|
||||
_dialog:
|
||||
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
|
||||
|
||||
+196
-190
@@ -64,8 +64,8 @@ copyNoteId: "노트 ID 복사"
|
||||
copyFileId: "파일 ID 복사"
|
||||
copyFolderId: "폴더 ID 복사"
|
||||
copyProfileUrl: "프로필 URL 복사"
|
||||
searchUser: "사용자 검색"
|
||||
searchThisUsersNotes: "사용자의 노트 검색"
|
||||
searchUser: "유저 검색"
|
||||
searchThisUsersNotes: "유저의 노트를 검색"
|
||||
reply: "답글"
|
||||
loadMore: "더 보기"
|
||||
showMore: "더 보기"
|
||||
@@ -267,7 +267,7 @@ publishing: "배포 중"
|
||||
notResponding: "응답 없음"
|
||||
instanceFollowing: "서버의 팔로잉"
|
||||
instanceFollowers: "서버의 팔로워"
|
||||
instanceUsers: "서버의 사용자"
|
||||
instanceUsers: "서버의 유저"
|
||||
changePassword: "비밀번호 변경"
|
||||
security: "보안"
|
||||
retypedNotMatch: "입력이 일치하지 않습니다."
|
||||
@@ -385,12 +385,12 @@ disablingTimelinesInfo: "특정 타임라인을 비활성화하더라도 관리
|
||||
registration: "등록"
|
||||
invite: "초대"
|
||||
driveCapacityPerLocalAccount: "로컬 유저 한 명당 드라이브 용량"
|
||||
driveCapacityPerRemoteAccount: "원격 사용자별 드라이브 용량"
|
||||
driveCapacityPerRemoteAccount: "리모트 유저별 드라이브 용량"
|
||||
inMb: "메가바이트 단위"
|
||||
bannerUrl: "배너 이미지 URL"
|
||||
backgroundImageUrl: "배경 이미지 URL"
|
||||
basicInfo: "기본 정보"
|
||||
pinnedUsers: "고정한 사용자"
|
||||
pinnedUsers: "고정한 유저"
|
||||
pinnedUsersDescription: "\"발견하기\" 페이지 등에 고정하고 싶은 유저를 한 줄에 한 명씩 적습니다."
|
||||
pinnedPages: "고정한 페이지"
|
||||
pinnedPagesDescription: "서버의 대문에 고정하고 싶은 페이지의 경로를 한 줄에 하나씩 적습니다."
|
||||
@@ -418,13 +418,13 @@ antennas: "안테나"
|
||||
manageAntennas: "안테나 관리"
|
||||
name: "이름"
|
||||
antennaSource: "받을 소스"
|
||||
antennaKeywords: "받을 검색어"
|
||||
antennaExcludeKeywords: "제외할 검색어"
|
||||
antennaKeywords: "받을 키워드"
|
||||
antennaExcludeKeywords: "제외할 키워드"
|
||||
antennaExcludeBots: "봇 계정 제외"
|
||||
antennaKeywordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다"
|
||||
notifyAntenna: "새로운 노트를 알림"
|
||||
withFileAntenna: "파일이 첨부된 노트만"
|
||||
hideNotesInSensitiveChannel: "민감한 채널의 노트 숨기기"
|
||||
excludeNotesInSensitiveChannel: "민감한 채널의 노트 제외"
|
||||
enableServiceworker: "ServiceWorker 사용"
|
||||
antennaUsersDescription: "유저명을 한 줄에 한 명씩 적습니다"
|
||||
caseSensitive: "대소문자를 구분"
|
||||
@@ -436,11 +436,11 @@ silence: "사일런스"
|
||||
silenceConfirm: "이 계정을 사일런스로 설정하시겠습니까?"
|
||||
unsilence: "사일런스 해제"
|
||||
unsilenceConfirm: "이 계정의 사일런스를 해제하시겠습니까?"
|
||||
popularUsers: "인기 사용자"
|
||||
recentlyUpdatedUsers: "최근에 활동한 사용자"
|
||||
recentlyRegisteredUsers: "최근에 가입한 사용자"
|
||||
recentlyDiscoveredUsers: "최근에 발견한 사용자"
|
||||
exploreUsersCount: "{count}명의 사용자가 있습니다"
|
||||
popularUsers: "인기 유저"
|
||||
recentlyUpdatedUsers: "최근에 활동한 유저"
|
||||
recentlyRegisteredUsers: "최근에 가입한 유저"
|
||||
recentlyDiscoveredUsers: "최근에 발견한 유저"
|
||||
exploreUsersCount: "{count}명의 유저가 있습니다"
|
||||
exploreFediverse: "연합우주를 탐색"
|
||||
popularTags: "인기 태그"
|
||||
userList: "리스트"
|
||||
@@ -489,7 +489,7 @@ next: "다음"
|
||||
retype: "다시 입력"
|
||||
noteOf: "{user}의 노트"
|
||||
quoteAttached: "인용함"
|
||||
quoteQuestion: "인용해서 작성하시겠습니까?"
|
||||
quoteQuestion: "인용해서 첨부하시겠습니까?"
|
||||
attachAsFileQuestion: "붙여넣으려는 글이 너무 깁니다. 텍스트 파일로 첨부하시겠습니까?"
|
||||
onlyOneFileCanBeAttached: "메시지에 첨부할 수 있는 파일은 하나까지입니다"
|
||||
signinRequired: "진행하기 전에 로그인을 해 주세요"
|
||||
@@ -508,7 +508,7 @@ strongPassword: "강한 비밀번호"
|
||||
passwordMatched: "일치합니다"
|
||||
passwordNotMatched: "일치하지 않습니다"
|
||||
signinWith: "{x}로 로그인"
|
||||
signinFailed: "로그인할 수 없습니다. 사용자 이름과 비밀번호를 확인해 주십시오."
|
||||
signinFailed: "로그인할 수 없습니다. 유저 이름과 비밀번호를 확인해 주십시오."
|
||||
or: "혹은"
|
||||
language: "언어"
|
||||
uiLanguage: "UI 표시 언어"
|
||||
@@ -520,7 +520,7 @@ style: "스타일"
|
||||
drawer: "서랍"
|
||||
popup: "팝업"
|
||||
showNoteActionsOnlyHover: "마우스가 올라간 때에만 노트 동작 버튼을 표시하기"
|
||||
showReactionsCount: "노트의 반응 수를 표시하기"
|
||||
showReactionsCount: "노트의 리액션 수를 표시하기"
|
||||
noHistory: "기록이 없습니다"
|
||||
signinHistory: "로그인 기록"
|
||||
enableAdvancedMfm: "고급 MFM을 활성화"
|
||||
@@ -571,8 +571,8 @@ objectStorageSetPublicRead: "업로드할 때 'public-read'를 설정하기"
|
||||
s3ForcePathStyleDesc: "s3ForcePathStyle을 활성화하면, 버킷 이름을 URL의 호스트명이 아닌 경로의 일부로써 취급합니다. 셀프 호스트 Minio와 같은 서비스를 사용할 경우 활성화해야 할 수 있습니다."
|
||||
serverLogs: "서버 로그"
|
||||
deleteAll: "모두 삭제"
|
||||
showFixedPostForm: "타임라인 상단에 글 작성란을 표시"
|
||||
showFixedPostFormInChannel: "채널 타임라인 상단에 글 작성란을 표시"
|
||||
showFixedPostForm: "타임라인 상단에 글 입력란을 표시"
|
||||
showFixedPostFormInChannel: "채널 타임라인 상단에 글 입력란을 표시"
|
||||
withRepliesByDefaultForNewlyFollowed: "팔로우 할 때 기본적으로 답글을 타임라인에 나오게 하기"
|
||||
newNoteRecived: "새 노트가 있습니다"
|
||||
sounds: "소리"
|
||||
@@ -607,7 +607,7 @@ uiInspectorDescription: "메모리에 있는 UI 컴포넌트의 인스턴트 목
|
||||
output: "출력"
|
||||
script: "스크립트"
|
||||
disablePagesScript: "Pages 에서 AiScript 를 사용하지 않음"
|
||||
updateRemoteUser: "원격 사용자 정보 갱신"
|
||||
updateRemoteUser: "리모트 유저 정보 갱신"
|
||||
unsetUserAvatar: "아바타 제거"
|
||||
unsetUserAvatarConfirm: "아바타를 제거할까요?"
|
||||
unsetUserBanner: "배너 제거"
|
||||
@@ -616,7 +616,7 @@ deleteAllFiles: "모든 파일 삭제"
|
||||
deleteAllFilesConfirm: "모든 파일을 삭제하시겠습니까?"
|
||||
removeAllFollowing: "모든 팔로잉 해제"
|
||||
removeAllFollowingDescription: "{host} 서버의 모든 팔로잉을 해제합니다. 해당 서버가 더 이상 존재하지 않는 경우 등에 실행해 주세요."
|
||||
userSuspended: "이 사용자는 정지되었습니다."
|
||||
userSuspended: "이 유저는 정지되었습니다."
|
||||
userSilenced: "이 계정은 사일런스된 상태입니다."
|
||||
yourAccountSuspendedTitle: "계정이 정지되었습니다"
|
||||
yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반하거나, 기타 다른 이유로 인해 정지되었습니다. 자세한 사항은 관리자에게 문의해 주십시오. 계정을 새로 생성하지 마십시오."
|
||||
@@ -677,7 +677,7 @@ emailAddress: "메일 주소"
|
||||
smtpConfig: "SMTP 서버 설정"
|
||||
smtpHost: "호스트"
|
||||
smtpPort: "포트"
|
||||
smtpUser: "사용자 이름"
|
||||
smtpUser: "유저 이름"
|
||||
smtpPass: "비밀번호"
|
||||
emptyToDisableSmtpAuth: "SMTP 인증을 사용하지 않으려면 공란으로 비워둡니다."
|
||||
smtpSecure: "SMTP 연결에 Implicit SSL/TTS 사용"
|
||||
@@ -720,7 +720,7 @@ abuseReports: "신고"
|
||||
reportAbuse: "신고"
|
||||
reportAbuseRenote: "리노트 신고하기"
|
||||
reportAbuseOf: "{name} 신고하기"
|
||||
fillAbuseReportDescription: "신고하려는 이유를 자세히 알려주세요. 특정 게시물을 신고할 때에는 게시물의 URL도 포함해 주세요."
|
||||
fillAbuseReportDescription: "신고 사유를 자세히 기재해 주세요. 대상 노트나 페이지 등이 있는 경우에는 해당 URL도 기재해 주세요."
|
||||
abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다."
|
||||
reporter: "신고자"
|
||||
reporteeOrigin: "피신고자"
|
||||
@@ -754,8 +754,8 @@ repliedCount: "받은 답글 수"
|
||||
renotedCount: "받은 리노트 수"
|
||||
followingCount: "팔로우 수"
|
||||
followersCount: "팔로워 수"
|
||||
sentReactionsCount: "반응 수"
|
||||
receivedReactionsCount: "받은 반응 수"
|
||||
sentReactionsCount: "리액션 수"
|
||||
receivedReactionsCount: "받은 리액션 수"
|
||||
pollVotesCount: "투표 수"
|
||||
pollVotedCount: "받은 투표 수"
|
||||
yes: "예"
|
||||
@@ -763,7 +763,7 @@ no: "아니오"
|
||||
driveFilesCount: "드라이브에 있는 파일 수"
|
||||
driveUsage: "드라이브 사용량"
|
||||
noCrawle: "검색엔진의 인덱싱 거부"
|
||||
noCrawleDescription: "검색엔진에 사용자 페이지, 노트, 페이지 등의 콘텐츠를 인덱싱되지 않게 합니다."
|
||||
noCrawleDescription: "검색엔진에 유저 페이지, 노트, 페이지 등의 콘텐츠를 인덱싱되지 않게 합니다."
|
||||
lockedAccountInfo: "팔로우를 승인으로 승인받더라도 노트의 공개 범위를 '팔로워'로 하지 않는 한 누구나 당신의 노트를 볼 수 있습니다."
|
||||
alwaysMarkSensitive: "미디어를 항상 열람 주의로 설정"
|
||||
loadRawImages: "첨부한 이미지의 썸네일을 원본화질로 표시"
|
||||
@@ -795,7 +795,7 @@ needReloadToApply: "변경 사항은 새로고침하면 적용됩니다."
|
||||
showTitlebar: "타이틀 바를 표시하기"
|
||||
clearCache: "캐시 비우기"
|
||||
onlineUsersCount: "{n}명이 접속 중"
|
||||
nUsers: "{n} 사용자"
|
||||
nUsers: "{n} 유저"
|
||||
nNotes: "{n} 노트"
|
||||
sendErrorReports: "오류 보고서 보내기"
|
||||
sendErrorReportsDescription: "이 설정을 활성화하면, 문제가 발생했을 때 오류에 대한 상세 정보를 Misskey에 보내어 더 나은 소프트웨어를 만드는 데에 도움을 줄 수 있습니다."
|
||||
@@ -825,7 +825,7 @@ editCode: "코드 수정"
|
||||
apply: "적용"
|
||||
receiveAnnouncementFromInstance: "이 서버의 알림을 이메일로 수신할게요"
|
||||
emailNotification: "메일 알림"
|
||||
publish: "게시"
|
||||
publish: "공개"
|
||||
inChannelSearch: "채널에서 검색"
|
||||
useReactionPickerForContextMenu: "우클릭하여 리액션 선택기 열기"
|
||||
typingUsers: "{users}님이 입력 중"
|
||||
@@ -841,7 +841,7 @@ addDescription: "설명 추가"
|
||||
userPagePinTip: "각 노트의 메뉴에서 「프로필에 고정」을 선택하는 것으로, 여기에 노트를 표시해 둘 수 있어요."
|
||||
notSpecifiedMentionWarning: "수신자가 선택되지 않은 멘션이 있어요"
|
||||
info: "정보"
|
||||
userInfo: "사용자 정보"
|
||||
userInfo: "유저 정보"
|
||||
unknown: "알 수 없음"
|
||||
onlineStatus: "온라인 상태"
|
||||
hideOnlineStatus: "온라인 상태 숨기기"
|
||||
@@ -857,7 +857,7 @@ switchAccount: "계정 바꾸기"
|
||||
enabled: "활성화"
|
||||
disabled: "비활성화"
|
||||
quickAction: "빠른 동작"
|
||||
user: "사용자"
|
||||
user: "유저"
|
||||
administration: "관리"
|
||||
accounts: "계정"
|
||||
switch: "전환"
|
||||
@@ -868,8 +868,8 @@ configure: "설정하기"
|
||||
postToGallery: "갤러리에 업로드"
|
||||
postToHashtag: "이 해시태그에 게시"
|
||||
gallery: "갤러리"
|
||||
recentPosts: "최근 포스트"
|
||||
popularPosts: "인기 포스트"
|
||||
recentPosts: "최근 게시물"
|
||||
popularPosts: "인기 게시물"
|
||||
shareWithNote: "노트로 공유"
|
||||
ads: "광고"
|
||||
expiration: "기한"
|
||||
@@ -898,7 +898,7 @@ whatIsNew: "패치 정보 보기"
|
||||
translate: "번역"
|
||||
translatedFrom: "{x}에서 번역"
|
||||
accountDeletionInProgress: "계정 삭제 작업을 진행하고 있습니다"
|
||||
usernameInfo: "서버상에서 계정을 식별하기 위한 이름. 알파벳(a~z, A~Z), 숫자(0~9) 및 언더바(_)를 사용할 수 있습니다. 사용자명은 나중에 변경할 수 없습니다."
|
||||
usernameInfo: "서버상에서 계정을 식별하기 위한 이름. 알파벳(a~z, A~Z), 숫자(0~9) 및 언더바(_)를 사용할 수 있습니다. 유저명은 나중에 변경할 수 없습니다."
|
||||
aiChanMode: "아이 모드"
|
||||
devMode: "개발자 모드"
|
||||
keepCw: "CW 유지하기"
|
||||
@@ -1032,7 +1032,7 @@ correspondingSourceIsAvailable: "소스 코드는 {anchor}에서 받아보실
|
||||
roles: "역할"
|
||||
role: "역할"
|
||||
noRole: "역할이 없습니다"
|
||||
normalUser: "일반 사용자"
|
||||
normalUser: "일반 유저"
|
||||
undefined: "정의되지 않음"
|
||||
assign: "할당"
|
||||
unassign: "할당 취소"
|
||||
@@ -1056,7 +1056,7 @@ thisPostMayBeAnnoyingHome: "홈에 게시"
|
||||
thisPostMayBeAnnoyingCancel: "그만두기"
|
||||
thisPostMayBeAnnoyingIgnore: "이대로 게시"
|
||||
collapseRenotes: "이미 본 리노트를 간략화하기"
|
||||
collapseRenotesDescription: "반응이나 리노트를 한 노트를 접어서 표시합니다."
|
||||
collapseRenotesDescription: "리액션이나 리노트를 한 노트를 접어서 표시합니다."
|
||||
internalServerError: "내부 서버 오류"
|
||||
internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했습니다."
|
||||
copyErrorInfo: "오류 정보 복사"
|
||||
@@ -1080,8 +1080,8 @@ resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?"
|
||||
sensitiveWords: "민감한 단어"
|
||||
sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제합니다. 개행으로 구분하여 여러 개를 지정할 수 있습니다."
|
||||
sensitiveWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다."
|
||||
prohibitedWords: "금지 워드"
|
||||
prohibitedWordsDescription: "설정된 워드가 포함되는 노트를 작성하려고 하면, 에러가 발생하도록 합니다. 줄바꿈으로 구분지어 복수 설정할 수 있습니다."
|
||||
prohibitedWords: "금지 단어"
|
||||
prohibitedWordsDescription: "설정된 단어가 포함되는 노트를 게시하려고 하면, 오류가 발생하도록 합니다. 줄바꿈으로 구분지어 복수 설정할 수 있습니다."
|
||||
prohibitedWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다."
|
||||
hiddenTags: "숨긴 해시태그"
|
||||
hiddenTagsDescription: "설정한 태그를 트렌드에 표시하지 않도록 합니다. 줄 바꿈으로 하나씩 나눠서 설정할 수 있습니다."
|
||||
@@ -1106,7 +1106,7 @@ audio: "소리"
|
||||
audioFiles: "소리"
|
||||
dataSaver: "데이터 절약 모드"
|
||||
accountMigration: "계정 이동"
|
||||
accountMoved: "이 사용자는 다음 계정으로 이사했습니다:"
|
||||
accountMoved: "이 유저는 다음 계정으로 이사했습니다:"
|
||||
accountMovedShort: "이사한 계정입니다"
|
||||
operationForbidden: "사용할 수 없습니다"
|
||||
forceShowAds: "광고를 항상 표시"
|
||||
@@ -1127,8 +1127,8 @@ serverRules: "서버 규칙"
|
||||
pleaseConfirmBelowBeforeSignup: "이 서버에 가입하기 전에 아래 사항을 확인하여 주십시오."
|
||||
pleaseAgreeAllToContinue: "계속하시려면 모든 항목에 동의하십시오."
|
||||
continue: "계속"
|
||||
preservedUsernames: "예약한 사용자 이름"
|
||||
preservedUsernamesDescription: "예약할 사용자명을 한 줄에 하나씩 입력합니다. 여기에서 지정한 사용자명으로는 계정을 생성할 수 없게 됩니다. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않습니다."
|
||||
preservedUsernames: "예약한 유저명"
|
||||
preservedUsernamesDescription: "예약할 유저명을 한 줄에 하나씩 입력합니다. 여기에서 지정한 유저명으로는 계정을 생성할 수 없게 됩니다. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않습니다."
|
||||
createNoteFromTheFile: "이 파일로 노트를 작성"
|
||||
archive: "아카이브"
|
||||
archived: "아카이브 됨"
|
||||
@@ -1142,7 +1142,7 @@ youFollowing: "팔로잉"
|
||||
preventAiLearning: "기계학습(생성형 AI)으로의 사용을 거부"
|
||||
preventAiLearningDescription: "외부의 문장 생성 AI나 이미지 생성 AI에 대해 제출한 노트나 이미지 등의 콘텐츠를 학습의 대상으로 사용하지 않도록 요구합니다. 다만, 이 요구사항을 지킬 의무는 없기 때문에 학습을 완전히 방지하는 것은 아닙니다."
|
||||
options: "옵션"
|
||||
specifyUser: "사용자 지정"
|
||||
specifyUser: "유저 지정"
|
||||
lookupConfirm: "조회 할까요?"
|
||||
openTagPageConfirm: "해시태그의 페이지를 열까요?"
|
||||
specifyHost: "호스트 지정"
|
||||
@@ -1297,8 +1297,8 @@ passkeyVerificationSucceededButPasswordlessLoginDisabled: "입력된 패스키
|
||||
messageToFollower: "팔로워에게 보낼 메시지"
|
||||
target: "대상"
|
||||
testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. <strong>실제 환경에서는 사용하지 마세요.</strong>"
|
||||
prohibitedWordsForNameOfUser: "금지 단어 (사용자 이름)"
|
||||
prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 사용자 이름에 있는 경우, 일반 사용자는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 사용자는 제한 대상에서 제외됩니다."
|
||||
prohibitedWordsForNameOfUser: "금지 단어 (유저명)"
|
||||
prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 유저명에 있는 경우, 일반 유저는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 유저는 제한 대상에서 제외됩니다."
|
||||
yourNameContainsProhibitedWords: "바꾸려는 이름에 금지된 키워드가 포함되어 있습니다."
|
||||
yourNameContainsProhibitedWordsDescription: "이름에 금지된 키워드가 있습니다. 이름을 사용해야 하는 경우, 서버 관리자에 문의하세요."
|
||||
thisContentsAreMarkedAsSigninRequiredByAuthor: "게시자에 의해 로그인해야 볼 수 있도록 설정되어 있습니다."
|
||||
@@ -1341,15 +1341,18 @@ right: "오른쪽"
|
||||
bottom: "아래"
|
||||
top: "위"
|
||||
embed: "임베드"
|
||||
settingsMigrating: "설정을 이전하는 중입니다. 잠시 기다려주십시오... (나중에 '환경설정 → 기타 → 기존 설정 정보를 이전'에서 수동으로 이전할 수도 있습니다)"
|
||||
readonly: "읽기 전용"
|
||||
goToDeck: "덱으로 돌아가기"
|
||||
_chat:
|
||||
noMessagesYet: "아직 메시지가 없습니다"
|
||||
newMessage: "새로운 메시지"
|
||||
individualChat: "개인 대화"
|
||||
individualChat_description: "특정 사용자와 일대일 채팅을 할 수 있습니다."
|
||||
individualChat_description: "특정 유저와 일대일 채팅을 할 수 있습니다."
|
||||
roomChat: "룸 채팅"
|
||||
roomChat_description: "여러 명이 함께 채팅할 수 있습니다.\n또한, 개인 채팅을 허용하지 않은 사용자와도 상대방이 수락하면 채팅을 할 수 있습니다."
|
||||
roomChat_description: "여러 명이 함께 채팅할 수 있습니다.\n또한, 개인 채팅을 허용하지 않은 유저와도 상대방이 수락하면 채팅을 할 수 있습니다."
|
||||
createRoom: "룸을 생성"
|
||||
inviteUserToChat: "사용자를 초대하여 채팅을 시작하세요"
|
||||
inviteUserToChat: "유저를 초대하여 채팅을 시작하세요"
|
||||
yourRooms: "생성한 룸"
|
||||
joiningRooms: "참가 중인 룸"
|
||||
invitations: "초대"
|
||||
@@ -1357,7 +1360,7 @@ _chat:
|
||||
history: "이력"
|
||||
noHistory: "기록이 없습니다"
|
||||
noRooms: "룸이 없습니다"
|
||||
inviteUser: "사용자를 초대"
|
||||
inviteUser: "유저를 초대"
|
||||
sentInvitations: "초대를 보내기"
|
||||
join: "참여"
|
||||
ignore: "무시"
|
||||
@@ -1370,21 +1373,22 @@ _chat:
|
||||
muteThisRoom: "이 룸을 뮤트"
|
||||
deleteRoom: "룸을 삭제"
|
||||
chatNotAvailableForThisAccountOrServer: "이 서버 또는 이 계정에서 채팅이 활성화되어 있지 않습니다."
|
||||
chatIsReadOnlyForThisAccountOrServer: "이 서버 또는 이 계정에서 채팅은 읽기 전용입니다. 새로 쓰거나 채팅 룸을 만들거나 참가할 수 없습니다."
|
||||
chatNotAvailableInOtherAccount: "상대방 계정에서 채팅 기능을 사용할 수 없는 상태입니다."
|
||||
cannotChatWithTheUser: "이 사용자와 채팅을 시작할 수 없습니다"
|
||||
cannotChatWithTheUser: "이 유저와 채팅을 시작할 수 없습니다"
|
||||
cannotChatWithTheUser_description: "채팅을 사용할 수 없는 상태이거나 상대방이 채팅을 열지 않은 상태입니다."
|
||||
chatWithThisUser: "채팅하기"
|
||||
thisUserAllowsChatOnlyFromFollowers: "이 사용자는 팔로워만 채팅을 할 수 있습니다."
|
||||
thisUserAllowsChatOnlyFromFollowing: "이 사용자는 이 사용자가 팔로우하는 사용자만 채팅을 허용합니다."
|
||||
thisUserAllowsChatOnlyFromMutualFollowing: "이 사용자는 상호 팔로우하는 사용자만 채팅을 허용합니다."
|
||||
thisUserNotAllowedChatAnyone: "이 사용자는 다른 사람의 채팅을 받지 않습니다."
|
||||
thisUserAllowsChatOnlyFromFollowers: "이 유저는 팔로워만 채팅을 할 수 있습니다."
|
||||
thisUserAllowsChatOnlyFromFollowing: "이 유저는 이 유저가 팔로우하는 유저만 채팅을 허용합니다."
|
||||
thisUserAllowsChatOnlyFromMutualFollowing: "이 유저는 상호 팔로우하는 유저만 채팅을 허용합니다."
|
||||
thisUserNotAllowedChatAnyone: "이 유저는 다른 사람의 채팅을 받지 않습니다."
|
||||
chatAllowedUsers: "채팅을 허용한 상대"
|
||||
chatAllowedUsers_note: "내가 채팅 메시지를 보낸 상대와는 이 설정과 상관없이 채팅이 가능합니다."
|
||||
_chatAllowedUsers:
|
||||
everyone: "누구나"
|
||||
followers: "자신의 팔로워만"
|
||||
following: "자신이 팔로우한 사용자만"
|
||||
mutual: "상호 팔로우한 사용자만"
|
||||
following: "자신이 팔로우한 유저만"
|
||||
mutual: "상호 팔로우한 유저만"
|
||||
none: "아무도 허락하지 않기"
|
||||
_emojiPalette:
|
||||
palettes: "팔레트"
|
||||
@@ -1410,7 +1414,7 @@ _settings:
|
||||
soundsBanner: "클라이언트에서 재생할 소리에 대한 설정을 합니다."
|
||||
timelineAndNote: "타임라인과 노트"
|
||||
makeEveryTextElementsSelectable: "모든 텍스트 요소를 선택할 수 있도록 함"
|
||||
makeEveryTextElementsSelectable_description: "활성화 시, 일부 동작에서 사용자의 접근성이 나빠질 수도 있습니다."
|
||||
makeEveryTextElementsSelectable_description: "활성화 시, 일부 동작에서 유저의 접근성이 나빠질 수도 있습니다."
|
||||
useStickyIcons: "아이콘이 스크롤을 따라가도록 하기"
|
||||
showNavbarSubButtons: "내비게이션 바에 보조 버튼 표시"
|
||||
ifOn: "켜져 있을 때"
|
||||
@@ -1436,12 +1440,12 @@ _accountSettings:
|
||||
requireSigninToViewContents: "콘텐츠 열람을 위해 로그인을 필수로 설정하기"
|
||||
requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다."
|
||||
requireSigninToViewContentsDescription2: "URL 미리보기(OGP), 웹페이지에 삽입, 노트 인용을 지원하지 않는 서버에서 볼 수 없게 됩니다."
|
||||
requireSigninToViewContentsDescription3: "원격 서버에 연합된 콘텐츠에는 이러한 제한이 적용되지 않을 수 있습니다."
|
||||
requireSigninToViewContentsDescription3: "리모트 서버에 연합된 콘텐츠에는 이러한 제한이 적용되지 않을 수 있습니다."
|
||||
makeNotesFollowersOnlyBefore: "과거 노트는 팔로워만 볼 수 있도록 설정하기"
|
||||
makeNotesFollowersOnlyBeforeDescription: "이 기능이 활성화되어 있는 동안, 설정된 날짜 및 시간보다 과거 또는 설정된 시간이 지난 노트는 팔로워만 볼 수 있게 됩니다. 비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다."
|
||||
makeNotesHiddenBefore: "과거 노트 비공개로 전환하기"
|
||||
makeNotesHiddenBeforeDescription: "이 기능이 활성화되어 있는 동안 설정한 날짜 및 시간보다 과거 또는 설정한 시간이 지난 노트는 본인만 볼 수 있게(비공개로 전환) 됩니다. 비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다."
|
||||
mayNotEffectForFederatedNotes: "원격 서버에 연합된 노트에는 효과가 없을 수도 있습니다."
|
||||
mayNotEffectForFederatedNotes: "리모트 서버에 연합된 노트에는 효과가 없을 수도 있습니다."
|
||||
mayNotEffectSomeSituations: "여기서 설정하는 제한은 모더레이션이나 리모트 서버에서 볼 때 등 일부 환경에서는 적용되지 않을 수도 있습니다."
|
||||
notesHavePassedSpecifiedPeriod: "지정한 시간이 경과된 노트"
|
||||
notesOlderThanSpecifiedDateAndTime: "지정된 날짜 및 시간 이전의 노트"
|
||||
@@ -1482,11 +1486,11 @@ _announcement:
|
||||
needConfirmationToRead: "읽음으로 표시하기 전에 확인하기"
|
||||
needConfirmationToReadDescription: "활성화하면 이 공지사항을 읽음으로 표시하기 전에 확인 알림창을 띄웁니다. '모두 읽음'의 대상에서도 제외됩니다."
|
||||
end: "공지에서 내리기"
|
||||
tooManyActiveAnnouncementDescription: "공지사항이 너무 많을 경우, 사용자 경험에 영향을 끼칠 가능성이 있습니다. 오래된 공지사항은 아카이브하시는 것을 권장드립니다."
|
||||
tooManyActiveAnnouncementDescription: "공지사항이 너무 많을 경우, 유저 경험에 영향을 끼칠 가능성이 있습니다. 오래된 공지사항은 아카이브하시는 것을 권장드립니다."
|
||||
readConfirmTitle: "읽음으로 표시합니까?"
|
||||
readConfirmText: "〈{title}〉의 내용을 읽음으로 표시합니다."
|
||||
shouldNotBeUsedToPresentPermanentInfo: "신규 유저의 이용 경험에 악영향을 끼칠 수 있으므로, 일시적인 알림 수단으로만 사용하고 고정된 정보에는 사용을 지양하는 것을 추천합니다."
|
||||
dialogAnnouncementUxWarn: "다이얼로그 형태의 알림이 동시에 2개 이상 존재하는 경우, 사용자 경험에 악영향을 끼칠 수 있으므로 신중히 결정하십시오."
|
||||
dialogAnnouncementUxWarn: "다이얼로그 형태의 알림이 동시에 2개 이상 존재하는 경우, 유저 경험에 악영향을 끼칠 수 있으므로 신중히 결정하십시오."
|
||||
silence: "조용히 알림"
|
||||
silenceDescription: "활성화하면 공지사항에 대한 알림이 가지 않게 되며, 확인 버튼을 누를 필요가 없게 됩니다."
|
||||
_initialAccountSetting:
|
||||
@@ -1538,7 +1542,7 @@ _initialTutorial:
|
||||
description3: "이 외에도, '리스트 타임라인'이나 '채널 타임라인' 등이 있습니다. 자세한 사항은 {link}에서 확인하실 수 있습니다."
|
||||
_postNote:
|
||||
title: "노트 게시 설정"
|
||||
description1: "Misskey에 노트를 쓸 때에는 다양한 옵션을 설정할 수 있습니다. 노트를 작성하는 화면은 이렇게 생겼습니다."
|
||||
description1: "Misskey에 노트를 게시할 때에는 다양한 옵션 설정이 가능합니다. 노트를 게시할 때 쓰이는 '글 입력란'은 이렇게 생겼습니다."
|
||||
_visibility:
|
||||
description: "노트를 볼 수 있는 사람을 제한할 수 있습니다."
|
||||
public: "모든 유저에게 공개합니다."
|
||||
@@ -1558,7 +1562,7 @@ _initialTutorial:
|
||||
_howToMakeAttachmentsSensitive:
|
||||
title: "첨부 파일을 열람주의로 설정하려면?"
|
||||
description: "서버의 가이드라인에 따라 필요한 이미지, 또는 그대로 노출되기에 부적절한 미디어는 '열람 주의'를 설정해 주세요."
|
||||
tryThisFile: "이 작성 창에 첨부된 이미지를 열람 주의로 설정해 보세요!"
|
||||
tryThisFile: "이 입력란에 첨부된 이미지를 열람 주의로 설정해 보세요!"
|
||||
_exampleNote:
|
||||
note: "낫또 뚜껑 뜯다가 실수했다…"
|
||||
method: "첨부 파일을 열람 주의로 설정하려면, 해당 파일을 클릭하여 메뉴를 열고, '열람주의로 설정'을 클릭합니다."
|
||||
@@ -1612,53 +1616,53 @@ _achievements:
|
||||
_types:
|
||||
_notes1:
|
||||
title: "미스키 계정 만들었어요"
|
||||
description: "첫 노트를 작성했습니다"
|
||||
description: "첫 노트를 게시했다"
|
||||
flavor: "Misskey에 어서 오세요!"
|
||||
_notes10:
|
||||
title: "몇 가지 노트"
|
||||
description: "10개의 노트를 작성했습니다"
|
||||
description: "10개의 노트를 게시했다"
|
||||
_notes100:
|
||||
title: "많은 노트"
|
||||
description: "100개의 노트를 작성했습니다"
|
||||
description: "100개의 노트를 게시했다"
|
||||
_notes500:
|
||||
title: "노트 범벅"
|
||||
description: "500개의 노트를 작성했습니다"
|
||||
description: "500개의 노트를 게시했다"
|
||||
_notes1000:
|
||||
title: "노트가 산더미"
|
||||
description: "1,000개의 노트를 작성했습니다"
|
||||
description: "1,000개의 노트를 게시했다"
|
||||
_notes5000:
|
||||
title: "솟아나는 노트"
|
||||
description: "5,000개의 노트를 작성했습니다"
|
||||
description: "5,000개의 노트를 게시했다"
|
||||
_notes10000:
|
||||
title: "슈퍼 노트"
|
||||
description: "10,000개의 노트를 작성했습니다"
|
||||
description: "10,000개의 노트를 게시했다"
|
||||
_notes20000:
|
||||
title: "노트가 필요해요"
|
||||
description: "20,000개의 노트를 작성했습니다"
|
||||
title: "노트가 더 필요해요"
|
||||
description: "20,000개의 노트를 게시했다"
|
||||
_notes30000:
|
||||
title: "노트노트노트"
|
||||
description: "30,000개의 노트를 작성했습니다"
|
||||
description: "30,000개의 노트를 게시했다"
|
||||
_notes40000:
|
||||
title: "노트 공장"
|
||||
description: "40,000개의 노트를 작성했습니다"
|
||||
description: "40,000개의 노트를 게시했다"
|
||||
_notes50000:
|
||||
title: "노트 행성"
|
||||
description: "50,000개의 노트를 작성했습니다"
|
||||
description: "50,000개의 노트를 게시했다"
|
||||
_notes60000:
|
||||
title: "노트 퀘이사"
|
||||
description: "60,000개의 노트를 작성했습니다"
|
||||
description: "60,000개의 노트를 게시했다"
|
||||
_notes70000:
|
||||
title: "노트 블랙홀"
|
||||
description: "70,000개의 노트를 작성했습니다"
|
||||
description: "70,000개의 노트를 게시했다"
|
||||
_notes80000:
|
||||
title: "노트 은하"
|
||||
description: "80,000개의 노트를 작성했습니다"
|
||||
description: "80,000개의 노트를 게시했다"
|
||||
_notes90000:
|
||||
title: "노트 우주"
|
||||
description: "90,000개의 노트를 작성했습니다"
|
||||
description: "90,000개의 노트를 게시했다"
|
||||
_notes100000:
|
||||
title: "ALL YOUR NOTE ARE BELONG TO US"
|
||||
description: "100,000개의 노트를 작성했습니다"
|
||||
description: "100,000개의 노트를 게시했다"
|
||||
flavor: "이렇게나 쓸 게 있어요?"
|
||||
_login3:
|
||||
title: "초보자 I"
|
||||
@@ -1683,181 +1687,181 @@ _achievements:
|
||||
flavor: "그 유저, 미스키스트이다"
|
||||
_login200:
|
||||
title: "단골 I"
|
||||
description: "총 200일간 로그인했습니다"
|
||||
description: "총 로그인한 날이 200일"
|
||||
_login300:
|
||||
title: "단골 II"
|
||||
description: "총 300일간 로그인했습니다"
|
||||
description: "총 로그인한 날이 300일"
|
||||
_login400:
|
||||
title: "단골 III"
|
||||
description: "총 400일간 로그인했습니다"
|
||||
description: "총 로그인한 날이 400일"
|
||||
_login500:
|
||||
title: "베테랑 I"
|
||||
description: "총 500일간 로그인했습니다"
|
||||
description: "총 로그인한 날이 500일"
|
||||
flavor: "제군, 나는 노트가 좋다"
|
||||
_login600:
|
||||
title: "베테랑 II"
|
||||
description: "총 600일간 로그인했습니다"
|
||||
description: "총 로그인한 날이 600일"
|
||||
_login700:
|
||||
title: "베테랑 III"
|
||||
description: "총 700일간 로그인했습니다"
|
||||
description: "총 로그인한 날이 700일"
|
||||
_login800:
|
||||
title: "노트 마스터 I"
|
||||
description: "총 800일간 로그인했습니다"
|
||||
description: "총 로그인한 날이 800일"
|
||||
_login900:
|
||||
title: "노트 마스터 II"
|
||||
description: "총 900일간 로그인했습니다"
|
||||
description: "총 로그인한 날이 900일"
|
||||
_login1000:
|
||||
title: "노트 마스터 III"
|
||||
description: "총 1,000일간 로그인했습니다"
|
||||
description: "총 로그인한 날이 1,000일"
|
||||
flavor: "Misskey를 사용해 주셔서 감사합니다!"
|
||||
_noteClipped1:
|
||||
title: "클립할 수밖에 없었어"
|
||||
description: "처음으로 노트를 클립했습니다"
|
||||
description: "처음으로 노트를 클립했다"
|
||||
_noteFavorited1:
|
||||
title: "별을 바라보는 자"
|
||||
description: "처음으로 노트를 즐겨찾기했습니다"
|
||||
description: "처음으로 노트를 즐겨찾기했다"
|
||||
_myNoteFavorited1:
|
||||
title: "별을 원하는 자"
|
||||
description: "다른 사람이 당신의 노트를 즐겨찾기했습니다"
|
||||
description: "다른 사람이 당신의 노트를 즐겨찾기했다"
|
||||
_profileFilled:
|
||||
title: "준비 완료"
|
||||
description: "프로필 설정을 완료했습니다"
|
||||
description: "프로필 설정을 완료했다"
|
||||
_markedAsCat:
|
||||
title: "나는 고양이다냥!"
|
||||
description: "계정을 고양이로 설정했습니다냥"
|
||||
description: "계정을 고양이로 설정했다냥"
|
||||
flavor: "냐냐냐냐냐냐아아아아앙!"
|
||||
_following1:
|
||||
title: "첫 팔로우"
|
||||
description: "사용자를 처음으로 팔로우했습니다"
|
||||
description: "유저를 처음으로 팔로우했다"
|
||||
_following10:
|
||||
title: "팔로우, 팔로우"
|
||||
description: "10명의 사용자를 팔로우했습니다"
|
||||
description: "10명의 유저를 팔로우했다"
|
||||
_following50:
|
||||
title: "친구 잔뜩"
|
||||
description: "50명의 사용자를 팔로우했습니다"
|
||||
description: "50명의 유저를 팔로우했다"
|
||||
_following100:
|
||||
title: "주소록 한 권으론 부족해"
|
||||
description: "100명의 사용자를 팔로우했습니다"
|
||||
description: "100명의 유저를 팔로우했다"
|
||||
_following300:
|
||||
title: "친구가 넘쳐나"
|
||||
description: "300명의 사용자를 팔로우했습니다"
|
||||
description: "300명의 유저를 팔로우했다"
|
||||
_followers1:
|
||||
title: "첫 팔로워"
|
||||
description: "사용자가 처음으로 팔로잉했습니다"
|
||||
description: "유저가 처음으로 팔로잉했다"
|
||||
_followers10:
|
||||
title: "팔로우 미!"
|
||||
description: "10명의 사용자가 팔로우했습니다"
|
||||
description: "10명의 유저가 팔로우했다"
|
||||
_followers50:
|
||||
title: "이곳저곳"
|
||||
description: "50명의 사용자가 팔로우했습니다"
|
||||
description: "50명의 유저가 팔로우했다"
|
||||
_followers100:
|
||||
title: "인기왕"
|
||||
description: "100명의 사용자가 팔로우했습니다"
|
||||
description: "100명의 유저가 팔로우했다"
|
||||
_followers300:
|
||||
title: "줄 좀 서봐요"
|
||||
description: "100명의 사용자가 팔로우했습니다"
|
||||
description: "100명의 유저가 팔로우했다"
|
||||
_followers500:
|
||||
title: "기지국"
|
||||
description: "500명의 사용자가 팔로우했습니다"
|
||||
description: "500명의 유저가 팔로우했다"
|
||||
_followers1000:
|
||||
title: "유명인사"
|
||||
description: "1,000명의 사용자가 팔로우했습니다"
|
||||
description: "1,000명의 유저가 팔로우했다"
|
||||
_collectAchievements30:
|
||||
title: "도전 과제 콜렉터"
|
||||
description: "30개의 도전과제를 획득했습니다"
|
||||
description: "30개의 도전과제를 획득했다"
|
||||
_viewAchievements3min:
|
||||
title: "저 도전과제 좋아해요"
|
||||
description: "도전 과제 목록을 3분 이상 쳐다봤습니다"
|
||||
description: "도전 과제 목록을 3분 이상 쳐다봤다"
|
||||
_iLoveMisskey:
|
||||
title: "I Love Misskey"
|
||||
description: "\"I ❤ #Misskey\"를 포스트했습니다"
|
||||
description: "\"I ❤ #Misskey\"를 게시했다"
|
||||
flavor: "Misskey를 이용해 주셔서 감사합니다! ― 개발 팀"
|
||||
_foundTreasure:
|
||||
title: "보물찾기"
|
||||
description: "숨겨진 보물을 발견했습니다"
|
||||
description: "숨겨진 보물을 발견했다"
|
||||
_client30min:
|
||||
title: "잠시 쉬어요"
|
||||
description: "클라이언트를 시작하고 30분이 경과하였습니다"
|
||||
description: "클라이언트를 시작하고 30분이 경과했다"
|
||||
_client60min:
|
||||
title: "No \"Miss\" in Misskey"
|
||||
description: "클라이언트를 시작하고 60분이 경과하였습니다"
|
||||
description: "클라이언트를 시작하고 60분이 경과했다"
|
||||
_noteDeletedWithin1min:
|
||||
title: "있었는데요 없었습니다"
|
||||
description: "노트를 포스트한 후 1분 이내에 삭제했습니다"
|
||||
description: "노트를 게시한 후 1분 이내에 삭제했다"
|
||||
_postedAtLateNight:
|
||||
title: "올빼미"
|
||||
description: "한밤중에 노트를 포스트했습니다"
|
||||
description: "한밤중에 노트를 게시했다"
|
||||
flavor: "잠 좀 자세요. 걱정돼요."
|
||||
_postedAt0min0sec:
|
||||
title: "정각"
|
||||
description: "0분 0초 정각에 노트를 작성했습니다"
|
||||
description: "0분 0초 정각에 노트를 게시했다"
|
||||
flavor: "째깍 째깍 째깍 땡!"
|
||||
_selfQuote:
|
||||
title: "혼잣말"
|
||||
description: "자기 노트를 인용했습니다"
|
||||
description: "자기 노트를 인용했다"
|
||||
_htl20npm:
|
||||
title: "타임라인 폭주 중"
|
||||
description: "1분 사이에 홈 타임라인에 노트가 20개 넘게 생성되었습니다"
|
||||
description: "1분 사이에 홈 타임라인에 노트가 20개 넘게 생성되었다"
|
||||
_viewInstanceChart:
|
||||
title: "애널리스트"
|
||||
description: "서버의 차트를 열었습니다"
|
||||
description: "서버의 차트를 열었다"
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "Hello, world!"
|
||||
description: "스크래치패드에서 hello world를 출력했습니다"
|
||||
description: "스크래치패드에서 hello world를 출력했다"
|
||||
_open3windows:
|
||||
title: "멀티 윈도우"
|
||||
description: "3개 이상의 창을 열었습니다"
|
||||
description: "3개 이상의 창을 열었다"
|
||||
_driveFolderCircularReference:
|
||||
title: "순환 참조"
|
||||
description: "드라이브 폴더에 스스로를 넣게 했습니다"
|
||||
description: "드라이브 폴더에 스스로를 넣게 했다"
|
||||
_reactWithoutRead:
|
||||
title: "읽고 답하긴 하시는 건가요?"
|
||||
description: "100자가 넘는 노트를 작성한 지 3초 안에 반응했어요"
|
||||
description: "100자가 넘는 노트를 게시한 지 3초 안에 리액션했다"
|
||||
_clickedClickHere:
|
||||
title: "여기를 누르세요"
|
||||
description: "여기를 눌렀습니다"
|
||||
title: "여길 눌러보세요"
|
||||
description: "여기를 눌렀다"
|
||||
_justPlainLucky:
|
||||
title: "그냥 운이 좋았어"
|
||||
description: "매 10초마다 0.01%의 확률로 달성됩니다"
|
||||
description: "매 10초마다 0.01%의 확률로 달성된다"
|
||||
_setNameToSyuilo:
|
||||
title: "신 콤플렉스"
|
||||
description: "이름을 syuilo로 설정했습니다"
|
||||
description: "이름을 syuilo로 설정했다"
|
||||
_passedSinceAccountCreated1:
|
||||
title: "1주년"
|
||||
description: "계정을 생성하고 1년이 지났습니다"
|
||||
description: "계정을 생성하고 1년이 지났다"
|
||||
_passedSinceAccountCreated2:
|
||||
title: "2주년"
|
||||
description: "계정을 생성하고 2년이 지났습니다"
|
||||
description: "계정을 생성하고 2년이 지났다"
|
||||
_passedSinceAccountCreated3:
|
||||
title: "3주년"
|
||||
description: "계정을 생성하고 3년이 지났습니다"
|
||||
description: "계정을 생성하고 3년이 지났다"
|
||||
_loggedInOnBirthday:
|
||||
title: "생일 축하합니다!"
|
||||
description: "생일에 로그인했습니다"
|
||||
description: "생일에 로그인했다"
|
||||
_loggedInOnNewYearsDay:
|
||||
title: "새해 복 많이 받으세요"
|
||||
description: "새해 첫 날에 로그인했습니다"
|
||||
description: "새해 첫 날에 로그인했다"
|
||||
flavor: "올해에도 저희 서버에 관심을 가져 주셔서 감사합니다"
|
||||
_cookieClicked:
|
||||
title: "쿠키를 클릭하는 게임"
|
||||
description: "쿠키를 클릭했습니다"
|
||||
description: "쿠키를 클릭했다"
|
||||
flavor: "소프트웨어 착각하지 않으셨나요?"
|
||||
_brainDiver:
|
||||
title: "Brain Diver"
|
||||
description: "Brain Diver로의 링크를 첨부했습니다"
|
||||
description: "Brain Diver로의 링크를 첨부했다"
|
||||
flavor: "Misskey-Misskey La-Tu-Ma"
|
||||
_smashTestNotificationButton:
|
||||
title: "테스트 과잉"
|
||||
description: "매우 짧은 시간 안에 알림 테스트를 여러 번 수행했습니다"
|
||||
description: "매우 짧은 시간 안에 알림 테스트를 여러 번 수행했다"
|
||||
_tutorialCompleted:
|
||||
title: "Misskey 입문자 과정 수료증"
|
||||
description: "튜토리얼을 완료했습니다"
|
||||
description: "튜토리얼을 완료했다"
|
||||
_bubbleGameExplodingHead:
|
||||
title: "🤯"
|
||||
description: "버블 게임에서 가장 큰 물건을 내놓았다"
|
||||
_bubbleGameDoubleExplodingHead:
|
||||
title: "더블 🤯"
|
||||
description: "버블게임에서 가장 큰 물건 2개를 동시에 내놓았다."
|
||||
description: "버블게임에서 가장 큰 물건 2개를 동시에 내놓았다"
|
||||
flavor: "이 정도만 도시락통에 🤯 🤯 조금만 더"
|
||||
_role:
|
||||
new: "새 역할 생성"
|
||||
@@ -1867,7 +1871,7 @@ _role:
|
||||
permission: "역할 권한"
|
||||
descriptionOfPermission: "<b>조정자</b>는 기본적인 조정 작업을 진행할 수 있습니다.\n<b>관리자</b>는 서버의 모든 설정을 변경할 수 있습니다."
|
||||
assignTarget: "할당 대상"
|
||||
descriptionOfAssignTarget: "<b>수동</b>을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n<b>조건부</b>를 선택하면 조건을 설정해 일치하는 사용자를 자동으로 포함되게 할 수 있습니다."
|
||||
descriptionOfAssignTarget: "<b>수동</b>을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n<b>조건부</b>를 선택하면 조건을 설정해 일치하는 유저를 자동으로 포함되게 할 수 있습니다."
|
||||
manual: "수동"
|
||||
manualRoles: "수동 역할"
|
||||
conditional: "조건부"
|
||||
@@ -1875,7 +1879,7 @@ _role:
|
||||
condition: "조건"
|
||||
isConditionalRole: "조건부 역할입니다."
|
||||
isPublic: "역할 공개"
|
||||
descriptionOfIsPublic: "역할에 할당된 사용자를 누구나 볼 수 있습니다. 또한 사용자 프로필에 이 역할이 표시됩니다."
|
||||
descriptionOfIsPublic: "역할에 할당된 유저를 누구나 볼 수 있습니다. 또한 유저 프로필에 이 역할이 표시됩니다."
|
||||
options: "옵션"
|
||||
policies: "정책"
|
||||
baseRole: "기본 역할"
|
||||
@@ -1888,10 +1892,10 @@ _role:
|
||||
descriptionOfIsExplorable: "활성화하면 역할 타임라인을 공개합니다. 비활성화 시 타임라인이 공개되지 않습니다."
|
||||
displayOrder: "표시 순서"
|
||||
descriptionOfDisplayOrder: "값이 클 수록 UI에서 먼저 표시됩니다."
|
||||
preserveAssignmentOnMoveAccount: "마이그레이션 대상 계정에도 할당 상태 전달"
|
||||
preserveAssignmentOnMoveAccount_description: "켜면 이 역할이 부여된 계정이 마이그레이션될 때 마이그레이션 대상 계정에도 이 역할이 승계됩니다."
|
||||
preserveAssignmentOnMoveAccount: "이전 대상 계정에도 할당 상태 전달"
|
||||
preserveAssignmentOnMoveAccount_description: "켜면 이 역할이 부여된 계정이 이전될 때 마이그레이션 대상 계정에도 이 역할이 승계됩니다."
|
||||
canEditMembersByModerator: "모더레이터의 역할 수정 허용"
|
||||
descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 할당하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 할당이 가능합니다."
|
||||
descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 유저를 할당하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 할당이 가능합니다."
|
||||
priority: "우선순위"
|
||||
_priority:
|
||||
low: "낮음"
|
||||
@@ -1917,8 +1921,8 @@ _role:
|
||||
webhookMax: "만들 수 있는 Webhook 수"
|
||||
clipMax: "만들 수 있는 클립 수"
|
||||
noteEachClipsMax: "클립에 넣을 수 있는 노트 수"
|
||||
userListMax: "만들 수 있는 사용자 리스트 수"
|
||||
userEachUserListsMax: "사용자 리스트에 넣을 수 있는 사용자 수"
|
||||
userListMax: "만들 수 있는 유저 리스트 수"
|
||||
userEachUserListsMax: "유저 리스트에 넣을 수 있는 유저 수"
|
||||
rateLimitFactor: "요청 빈도 제한"
|
||||
descriptionOfRateLimitFactor: "작을수록 제한이 완화되고, 클수록 제한이 강화됩니다."
|
||||
canHideAds: "광고 숨기기"
|
||||
@@ -1930,24 +1934,24 @@ _role:
|
||||
canImportFollowing: "팔로우 가져오기 허용"
|
||||
canImportMuting: "뮤트 목록 가져오기 허용"
|
||||
canImportUserLists: "리스트 목록 가져오기 허용"
|
||||
canChat: "채팅을 허락"
|
||||
chatAvailability: "채팅을 허락"
|
||||
_condition:
|
||||
roleAssignedTo: "수동 역할에 이미 할당됨"
|
||||
isLocal: "로컬 사용자"
|
||||
isRemote: "원격 사용자"
|
||||
isCat: "고양이 사용자"
|
||||
isBot: "봇 사용자"
|
||||
isSuspended: "정지된 사용자"
|
||||
isLocked: "잠금 계정 사용자"
|
||||
isExplorable: "‘계정을 쉽게 발견하도록 하기’를 활성화한 사용자"
|
||||
isLocal: "로컬 유저"
|
||||
isRemote: "리모트 유저"
|
||||
isCat: "고양이 유저"
|
||||
isBot: "봇 유저"
|
||||
isSuspended: "정지된 유저"
|
||||
isLocked: "잠금 계정 유저"
|
||||
isExplorable: "‘계정을 쉽게 발견하도록 하기’를 활성화한 유저"
|
||||
createdLessThan: "가입한 지 다음 일수 이내인 유저"
|
||||
createdMoreThan: "가입한 지 다음 일수 이상인 유저"
|
||||
followersLessThanOrEq: "팔로워 수가 다음 이하인 유저"
|
||||
followersMoreThanOrEq: "팔로워 수가 다음보다 많은 사용자"
|
||||
followersMoreThanOrEq: "팔로워 수가 다음보다 많은 유저"
|
||||
followingLessThanOrEq: "팔로잉 수가 다음 이하인 유저"
|
||||
followingMoreThanOrEq: "팔로잉 수가 다음보다 많은 사용자"
|
||||
followingMoreThanOrEq: "팔로잉 수가 다음보다 많은 유저"
|
||||
notesLessThanOrEq: "노트 수가 다음 이하인 유저"
|
||||
notesMoreThanOrEq: "노트 수가 다음보다 많은 사용자"
|
||||
notesMoreThanOrEq: "노트 수가 다음보다 많은 유저"
|
||||
and: "다음을 모두 만족"
|
||||
or: "다음을 하나라도 만족"
|
||||
not: "다음을 만족하지 않음"
|
||||
@@ -1989,7 +1993,7 @@ _ad:
|
||||
adsSettings: "광고 표시 설정"
|
||||
notesPerOneAd: "실시간으로 갱신되는 타임라인에서 광고를 노출시키는 간격 (노트 당)"
|
||||
setZeroToDisable: "0으로 지정하면 실시간 타임라인에서의 광고를 비활성화합니다"
|
||||
adsTooClose: "광고의 표시 간격이 매우 작아, 사용자 경험에 부정적인 영향을 미칠 수 있습니다."
|
||||
adsTooClose: "광고의 표시 간격이 매우 작아, 유저 경험에 부정적인 영향을 미칠 수 있습니다."
|
||||
_forgotPassword:
|
||||
enterEmail: "여기에 계정에 등록한 메일 주소를 입력해 주세요. 입력한 메일 주소로 비밀번호 재설정 링크를 발송합니다."
|
||||
ifNoEmail: "메일 주소를 등록하지 않은 경우, 관리자에 문의해 주십시오."
|
||||
@@ -2244,7 +2248,7 @@ _permissions:
|
||||
"write:pages": "페이지를 수정합니다"
|
||||
"read:page-likes": "페이지의 좋아요를 확인합니다"
|
||||
"write:page-likes": "페이지에 좋아요를 추가하거나 취소합니다"
|
||||
"read:user-groups": "사용자 그룹 보기"
|
||||
"read:user-groups": "유저 그룹 보기"
|
||||
"write:user-groups": "유저 그룹을 만들거나, 초대하거나, 이름을 변경하거나, 양도하거나, 삭제합니다"
|
||||
"read:channels": "채널을 보기"
|
||||
"write:channels": "채널을 추가하거나 삭제합니다"
|
||||
@@ -2256,23 +2260,23 @@ _permissions:
|
||||
"write:flash": "Play를 조작합니다"
|
||||
"read:flash-likes": "Play의 좋아요를 봅니다"
|
||||
"write:flash-likes": "Play의 좋아요를 조작합니다"
|
||||
"read:admin:abuse-user-reports": "사용자 신고 보기"
|
||||
"write:admin:delete-account": "사용자 계정 삭제하기"
|
||||
"write:admin:delete-all-files-of-a-user": "모든 사용자 파일 삭제하기"
|
||||
"read:admin:abuse-user-reports": "유저 신고 보기"
|
||||
"write:admin:delete-account": "유저 계정 삭제하기"
|
||||
"write:admin:delete-all-files-of-a-user": "모든 유저 파일 삭제하기"
|
||||
"read:admin:index-stats": "데이터베이스 색인 정보 보기"
|
||||
"read:admin:table-stats": "데이터베이스 테이블 정보 보기"
|
||||
"read:admin:user-ips": "사용자 IP 주소 보기"
|
||||
"read:admin:user-ips": "유저 IP 주소 보기"
|
||||
"read:admin:meta": "인스턴스 메타데이터 보기"
|
||||
"write:admin:reset-password": "사용자 비밀번호 재설정하기"
|
||||
"write:admin:resolve-abuse-user-report": "사용자 신고 처리하기"
|
||||
"write:admin:reset-password": "유저 비밀번호 재설정하기"
|
||||
"write:admin:resolve-abuse-user-report": "유저 신고 처리하기"
|
||||
"write:admin:send-email": "이메일 보내기"
|
||||
"read:admin:server-info": "서버 정보 보기"
|
||||
"read:admin:show-moderation-log": "조정 기록 보기"
|
||||
"read:admin:show-user": "사용자 개인정보 보기"
|
||||
"write:admin:suspend-user": "사용자 정지하기"
|
||||
"write:admin:unset-user-avatar": "사용자 아바타 삭제하기"
|
||||
"write:admin:unset-user-banner": "사용자 배너 삭제하기"
|
||||
"write:admin:unsuspend-user": "사용자 정지 해제하기"
|
||||
"read:admin:show-user": "유저 개인정보 보기"
|
||||
"write:admin:suspend-user": "유저 정지하기"
|
||||
"write:admin:unset-user-avatar": "유저 아바타 삭제하기"
|
||||
"write:admin:unset-user-banner": "유저 배너 삭제하기"
|
||||
"write:admin:unsuspend-user": "유저 정지 해제하기"
|
||||
"write:admin:meta": "인스턴스 메타데이터 수정하기"
|
||||
"write:admin:user-note": "조정 기록 수정하기"
|
||||
"write:admin:roles": "역할 수정하기"
|
||||
@@ -2286,15 +2290,15 @@ _permissions:
|
||||
"write:admin:avatar-decorations": "아바타 꾸미기 수정하기"
|
||||
"read:admin:avatar-decorations": "아바타 꾸미기 보기"
|
||||
"write:admin:federation": "연합 정보 수정하기"
|
||||
"write:admin:account": "사용자 계정 수정하기"
|
||||
"read:admin:account": "사용자 정보 보기"
|
||||
"write:admin:account": "유저 계정 수정하기"
|
||||
"read:admin:account": "유저 정보 보기"
|
||||
"write:admin:emoji": "이모지 수정하기"
|
||||
"read:admin:emoji": "이모지 보기"
|
||||
"write:admin:queue": "작업 대기열 수정하기"
|
||||
"read:admin:queue": "작업 대기열 정보 보기"
|
||||
"write:admin:promo": "홍보 기록 수정하기"
|
||||
"write:admin:drive": "사용자 드라이브 수정하기"
|
||||
"read:admin:drive": "사용자 드라이브 정보 보기"
|
||||
"write:admin:drive": "유저 드라이브 수정하기"
|
||||
"read:admin:drive": "유저 드라이브 정보 보기"
|
||||
"read:admin:stream": "관리자용 Websocket API 사용하기"
|
||||
"write:admin:ad": "광고 수정하기"
|
||||
"read:admin:ad": "광고 보기"
|
||||
@@ -2316,7 +2320,7 @@ _auth:
|
||||
callback: "앱으로 돌아갑니다"
|
||||
accepted: "접근 권한이 부여되었습니다."
|
||||
denied: "접근이 거부되었습니다"
|
||||
scopeUser: "다음 사용자로 활동하고 있습니다."
|
||||
scopeUser: "다음 유저로 활동하고 있습니다."
|
||||
pleaseLogin: "어플리케이션의 접근을 허가하려면 로그인하십시오."
|
||||
byClickingYouWillBeRedirectedToThisUrl: "접근을 허용하면 자동으로 다음 URL로 이동합니다."
|
||||
_antennaSources:
|
||||
@@ -2353,7 +2357,7 @@ _widgets:
|
||||
postForm: "글 입력란"
|
||||
slideshow: "슬라이드 쇼"
|
||||
button: "버튼"
|
||||
onlineUsers: "온라인 사용자"
|
||||
onlineUsers: "온라인 유저"
|
||||
jobQueue: "작업 대기열"
|
||||
serverMetric: "서버 통계"
|
||||
aiscript: "AiScript 콘솔"
|
||||
@@ -2363,7 +2367,8 @@ _widgets:
|
||||
_userList:
|
||||
chooseList: "리스트 선택"
|
||||
clicker: "클리커"
|
||||
birthdayFollowings: "오늘이 생일인 사용자"
|
||||
birthdayFollowings: "오늘이 생일인 유저"
|
||||
chat: "채팅"
|
||||
_cw:
|
||||
hide: "숨기기"
|
||||
show: "더 보기"
|
||||
@@ -2415,7 +2420,7 @@ _postForm:
|
||||
f: "작성해주시길 기다리고 있어요..."
|
||||
_profile:
|
||||
name: "이름"
|
||||
username: "사용자 이름"
|
||||
username: "유저명"
|
||||
description: "자기소개"
|
||||
youCanIncludeHashtags: "해시 태그를 포함할 수 있습니다."
|
||||
metadata: "추가 정보"
|
||||
@@ -2446,7 +2451,7 @@ _charts:
|
||||
apRequest: "요청"
|
||||
usersIncDec: "유저 수 증감"
|
||||
usersTotal: "유저 수 합계"
|
||||
activeUsers: "활동 사용자 수"
|
||||
activeUsers: "활동 유저 수"
|
||||
notesIncDec: "노트 수 증감"
|
||||
localNotesIncDec: "로컬 노트 수 증감"
|
||||
remoteNotesIncDec: "리모트 노트 수 증감"
|
||||
@@ -2457,8 +2462,8 @@ _charts:
|
||||
storageUsageTotal: "스토리지 사용량 합계"
|
||||
_instanceCharts:
|
||||
requests: "요청"
|
||||
users: "사용자 수 차이"
|
||||
usersTotal: "누적 사용자 수"
|
||||
users: "유저 수 차이"
|
||||
usersTotal: "누적 유저 수"
|
||||
notes: "노트 수 증감"
|
||||
notesTotal: "누적 노트 수"
|
||||
ff: "팔로잉/팔로워 증감"
|
||||
@@ -2561,7 +2566,7 @@ _notification:
|
||||
checkNotificationBehavior: "알림 표시를 체크하기"
|
||||
sendTestNotification: "테스트 알림 보내기"
|
||||
notificationWillBeDisplayedLikeThis: "알림이 이렇게 표시됩니다"
|
||||
reactedBySomeUsers: "{n}명이 반응했습니다"
|
||||
reactedBySomeUsers: "{n}명이 리액션했습니다"
|
||||
likedBySomeUsers: "{n}명이 좋아요를 했습니다"
|
||||
renotedBySomeUsers: "{n}명이 리노트했습니다"
|
||||
followedBySomeUsers: "{n}명에게 팔로우됨"
|
||||
@@ -2572,18 +2577,18 @@ _notification:
|
||||
createTokenDescription: "만약 기억이 나지 않는다면 '{text}'를 통해 액세스 토큰을 삭제해 주세요."
|
||||
_types:
|
||||
all: "전부"
|
||||
note: "사용자의 새 글"
|
||||
note: "유저의 새 글"
|
||||
follow: "팔로잉"
|
||||
mention: "멘션"
|
||||
reply: "답글"
|
||||
renote: "리노트"
|
||||
quote: "인용"
|
||||
reaction: "반응"
|
||||
reaction: "리액션"
|
||||
pollEnded: "투표가 종료됨"
|
||||
receiveFollowRequest: "팔로우 요청을 받았을 때"
|
||||
followRequestAccepted: "팔로우 요청이 승인되었을 때"
|
||||
roleAssigned: "역할이 부여 됨"
|
||||
chatRoomInvitationReceived: "채팅 룸에 초대받았습니다"
|
||||
roleAssigned: "역할이 부여됨"
|
||||
chatRoomInvitationReceived: "채팅 룸에 초대받음"
|
||||
achievementEarned: "도전 과제 획득"
|
||||
exportCompleted: "추출을 성공함"
|
||||
login: "로그인"
|
||||
@@ -2630,6 +2635,7 @@ _deck:
|
||||
mentions: "받은 멘션"
|
||||
direct: "다이렉트"
|
||||
roleTimeline: "역할 타임라인"
|
||||
chat: "채팅"
|
||||
_dialog:
|
||||
charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {max}"
|
||||
charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}"
|
||||
@@ -2671,10 +2677,10 @@ _abuseReport:
|
||||
mail: "이메일"
|
||||
webhook: "Webhook"
|
||||
_captions:
|
||||
mail: "모더레이터 권한을 가진 사용자의 이메일 주소에 알림을 보냅니다 (신고를 받은 때에만)"
|
||||
mail: "모더레이터 권한을 가진 유저의 이메일 주소에 알림을 보냅니다 (신고를 받은 때에만)"
|
||||
webhook: "지정한 SystemWebhook에 알림을 보냅니다 (신고를 받은 때와 해결했을 때에 송신)"
|
||||
keywords: "키워드"
|
||||
notifiedUser: "알릴 사용자"
|
||||
notifiedUser: "알릴 유저"
|
||||
notifiedWebhook: "사용할 Webhook"
|
||||
deleteConfirm: "수신자를 삭제하시겠습니까?"
|
||||
_moderationLogTypes:
|
||||
@@ -2693,11 +2699,11 @@ _moderationLogTypes:
|
||||
deleteDriveFile: "파일 삭제"
|
||||
deleteNote: "노트 삭제"
|
||||
createGlobalAnnouncement: "전역 공지사항 생성"
|
||||
createUserAnnouncement: "사용자 공지사항 만들기"
|
||||
createUserAnnouncement: "유저에게 공지사항 만들기"
|
||||
updateGlobalAnnouncement: "모든 공지사항 수정"
|
||||
updateUserAnnouncement: "사용자 공지사항 수정"
|
||||
updateUserAnnouncement: "유저의 공지사항 수정"
|
||||
deleteGlobalAnnouncement: "모든 공지사항 삭제"
|
||||
deleteUserAnnouncement: "사용자 공지사항 삭제"
|
||||
deleteUserAnnouncement: "유저의 공지사항 삭제"
|
||||
resetPassword: "비밀번호 재설정"
|
||||
suspendRemoteInstance: "리모트 서버를 정지"
|
||||
unsuspendRemoteInstance: "리모트 서버의 정지를 해제"
|
||||
@@ -2725,7 +2731,7 @@ _moderationLogTypes:
|
||||
deleteAccount: "계정을 삭제"
|
||||
deletePage: "페이지를 삭제"
|
||||
deleteFlash: "Play를 삭제"
|
||||
deleteGalleryPost: "갤러리 포스트를 삭제"
|
||||
deleteGalleryPost: "갤러리 게시물을 삭제"
|
||||
deleteChatRoom: "채팅 룸 삭제"
|
||||
updateProxyAccountDescription: "프록시 계정의 설명 업데이트"
|
||||
_fileViewer:
|
||||
@@ -2939,7 +2945,7 @@ _embedCodeGen:
|
||||
_selfXssPrevention:
|
||||
warning: "경고"
|
||||
title: "“이 화면에 뭔가를 붙여넣어라\"는 것은 모두 사기입니다."
|
||||
description1: "여기에 무언가를 붙여넣으면 악의적인 사용자에게 계정을 탈취당하거나 개인정보를 도용당할 수 있습니다."
|
||||
description1: "여기에 무언가를 붙여넣으면 악의적인 유저에게 계정을 탈취당하거나 개인정보를 도용당할 수 있습니다."
|
||||
description2: "붙여 넣으려는 항목이 무엇인지 정확히 이해하지 못하는 경우, %c지금 바로 작업을 중단하고 이 창을 닫으십시오."
|
||||
description3: "자세한 내용은 여기를 확인해 주세요. {link}"
|
||||
_followRequest:
|
||||
@@ -2972,8 +2978,8 @@ _captcha:
|
||||
title: "CAPTCHA 검증을 실패했습니다."
|
||||
text: "설정이 올바른지 다시 한 번 확인해보세요."
|
||||
_unknown:
|
||||
title: "CAPTCHA 에러"
|
||||
text: "알 수 없는 에러가 발생했습니다."
|
||||
title: "CAPTCHA 오류"
|
||||
text: "알 수 없는 오류가 발생했습니다."
|
||||
_bootErrors:
|
||||
title: "로딩이 실패함"
|
||||
serverError: "잠시 기다렸다가 다시 로드해도 여전히 문제가 해결되지 않으면 아래 Error ID와 함께 서버 관리자에게 연락해 주세요."
|
||||
@@ -2990,7 +2996,7 @@ _search:
|
||||
searchScopeAll: "전체"
|
||||
searchScopeLocal: "로컬"
|
||||
searchScopeServer: "서버 지정"
|
||||
searchScopeUser: "사용자 지정"
|
||||
searchScopeUser: "유저 지정"
|
||||
pleaseEnterServerHost: "서버의 호스트를 입력해 주세요."
|
||||
pleaseSelectUser: "유저를 선택해주세요"
|
||||
serverHostPlaceholder: "예: misskey.example.com"
|
||||
|
||||
+9
-2
@@ -424,7 +424,7 @@ antennaExcludeBots: "排除机器人账户"
|
||||
antennaKeywordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。"
|
||||
notifyAntenna: "开启通知"
|
||||
withFileAntenna: "仅带有附件的帖子"
|
||||
hideNotesInSensitiveChannel: "隐藏敏感频道内的帖子"
|
||||
excludeNotesInSensitiveChannel: "排除敏感频道内的帖子"
|
||||
enableServiceworker: "启用 ServiceWorker"
|
||||
antennaUsersDescription: "指定用户名,一行一个"
|
||||
caseSensitive: "区分大小写"
|
||||
@@ -979,6 +979,7 @@ document: "文档"
|
||||
numberOfPageCache: "缓存页数"
|
||||
numberOfPageCacheDescription: "设置较高的值会更方便用户,但设备的负载和内存使用量会增加。"
|
||||
logoutConfirm: "是否确认登出?"
|
||||
logoutWillClearClientData: "登出时将会从浏览器中删除客户端的设置信息。如果想要在再次登入时恢复设置信息,请在设置里打开自动备份。"
|
||||
lastActiveDate: "最后活跃时间"
|
||||
statusbar: "状态栏"
|
||||
pleaseSelect: "请选择"
|
||||
@@ -1341,6 +1342,9 @@ right: "右"
|
||||
bottom: "下"
|
||||
top: "上"
|
||||
embed: "嵌入"
|
||||
settingsMigrating: "正在迁移设置,请稍候。(之后也可以在设置 → 其它 → 迁移旧设置来手动迁移)"
|
||||
readonly: "只读"
|
||||
goToDeck: "返回至 Deck"
|
||||
_chat:
|
||||
noMessagesYet: "还没有消息"
|
||||
newMessage: "新消息"
|
||||
@@ -1370,6 +1374,7 @@ _chat:
|
||||
muteThisRoom: "静音此房间"
|
||||
deleteRoom: "删除房间"
|
||||
chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。"
|
||||
chatIsReadOnlyForThisAccountOrServer: "此服务器或者账户内的聊天为只读。无法发布新信息或创建及加入群聊。"
|
||||
chatNotAvailableInOtherAccount: "对方账户目前处于无法使用聊天的状态。"
|
||||
cannotChatWithTheUser: "无法与此用户聊天"
|
||||
cannotChatWithTheUser_description: "可能现在无法使用聊天,或者对方未开启聊天。"
|
||||
@@ -1929,7 +1934,7 @@ _role:
|
||||
canImportFollowing: "允许导入关注列表"
|
||||
canImportMuting: "允许导入隐藏列表"
|
||||
canImportUserLists: "允许导入用户列表"
|
||||
canChat: "允许聊天"
|
||||
chatAvailability: "允许聊天"
|
||||
_condition:
|
||||
roleAssignedTo: "已分配给手动角色"
|
||||
isLocal: "是本地用户"
|
||||
@@ -2363,6 +2368,7 @@ _widgets:
|
||||
chooseList: "选择列表"
|
||||
clicker: "点击器"
|
||||
birthdayFollowings: "今天是他们的生日"
|
||||
chat: "聊天"
|
||||
_cw:
|
||||
hide: "隐藏"
|
||||
show: "查看更多"
|
||||
@@ -2629,6 +2635,7 @@ _deck:
|
||||
mentions: "提及"
|
||||
direct: "指定用户"
|
||||
roleTimeline: "角色时间线"
|
||||
chat: "聊天"
|
||||
_dialog:
|
||||
charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}"
|
||||
charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}"
|
||||
|
||||
+10
-5
@@ -424,7 +424,7 @@ antennaExcludeBots: "排除機器人帳戶"
|
||||
antennaKeywordsDescription: "空格代表「以及」(AND),換行代表「或者」(OR)"
|
||||
notifyAntenna: "通知有新貼文"
|
||||
withFileAntenna: "僅帶有附件的貼文"
|
||||
hideNotesInSensitiveChannel: "隱藏敏感頻道的貼文"
|
||||
excludeNotesInSensitiveChannel: "排除敏感頻道的貼文"
|
||||
enableServiceworker: "啟用瀏覽器的推播通知"
|
||||
antennaUsersDescription: "填寫使用者名稱,以換行分隔"
|
||||
caseSensitive: "區分大小寫"
|
||||
@@ -979,6 +979,7 @@ document: "文件"
|
||||
numberOfPageCache: "快取頁面數"
|
||||
numberOfPageCacheDescription: "增加數量會提高便利性,但也會增加負荷與記憶體使用量。"
|
||||
logoutConfirm: "確定要登出嗎?"
|
||||
logoutWillClearClientData: "當您登出時,客戶端的設定資訊將從瀏覽器中清除。為了能夠在重新登入時恢復您的設定資訊,請啟用設定內的自動備份選項。"
|
||||
lastActiveDate: "上次使用日期及時間"
|
||||
statusbar: "狀態列"
|
||||
pleaseSelect: "請選擇"
|
||||
@@ -1308,7 +1309,7 @@ availableRoles: "可用角色"
|
||||
acknowledgeNotesAndEnable: "了解注意事項後再開啟。"
|
||||
federationSpecified: "此伺服器以白名單聯邦的方式運作。除了管理員指定的伺服器外,它無法與其他伺服器互動。"
|
||||
federationDisabled: "此伺服器未開啟站台聯邦。無法與其他伺服器上的使用者互動。"
|
||||
confirmOnReact: "反應時確認"
|
||||
confirmOnReact: "在做出反應前先確認"
|
||||
reactAreYouSure: "用「 {emoji} 」反應嗎?"
|
||||
markAsSensitiveConfirm: "要將這個媒體設定為敏感嗎?"
|
||||
unmarkAsSensitiveConfirm: "要解除這個媒體的敏感設定嗎?"
|
||||
@@ -1342,6 +1343,7 @@ bottom: "下"
|
||||
top: "上"
|
||||
embed: "嵌入"
|
||||
settingsMigrating: "正在移轉設定。請稍候……(之後也可以到「設定 → 其他 → 舊設定資訊移轉」中手動進行移轉)"
|
||||
readonly: "唯讀"
|
||||
_chat:
|
||||
noMessagesYet: "尚無訊息"
|
||||
newMessage: "新訊息"
|
||||
@@ -1371,6 +1373,7 @@ _chat:
|
||||
muteThisRoom: "此聊天室已靜音"
|
||||
deleteRoom: "刪除聊天室"
|
||||
chatNotAvailableForThisAccountOrServer: "這個伺服器或這個帳號的聊天功能尚未啟用。"
|
||||
chatIsReadOnlyForThisAccountOrServer: "在此伺服器或此帳戶上的聊天是唯讀的。您無法發布新訊息、建立或加入聊天室。"
|
||||
chatNotAvailableInOtherAccount: "對方的帳號無法使用聊天功能。"
|
||||
cannotChatWithTheUser: "無法與此使用者聊天"
|
||||
cannotChatWithTheUser_description: "聊天功能目前無法使用,或對方尚未開放聊天功能。"
|
||||
@@ -1436,14 +1439,14 @@ _preferencesBackup:
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "須登入以顯示內容"
|
||||
requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。"
|
||||
requireSigninToViewContentsDescription2: "來自不支援 URL 預覽 (OGP)、 網頁嵌入和引用貼文的伺服器,也將停止顯示。"
|
||||
requireSigninToViewContentsDescription2: "針對您貼文的 URL 預覽 (OGP) 與網頁嵌入功能將會無法使用。而不支援引用貼文的伺服器,也將停止顯示。"
|
||||
requireSigninToViewContentsDescription3: "這些限制可能不適用於被聯邦發送至遠端伺服器的內容。"
|
||||
makeNotesFollowersOnlyBefore: "讓過去的貼文僅對追隨者顯示"
|
||||
makeNotesFollowersOnlyBeforeDescription: "啟用此功能後,超過設定的日期和時間或超過設定時間的貼文將僅對追隨者顯示。 如果您再次停用它,貼文的公開狀態也會恢復原狀。"
|
||||
makeNotesHiddenBefore: "隱藏過去的貼文"
|
||||
makeNotesHiddenBeforeDescription: "啟用此功能後,超過設定的日期和時間或超過設定時間的貼文將僅對自己顯示(私密化)。 如果您再次停用它,貼文的公開狀態也會恢復原狀。"
|
||||
mayNotEffectForFederatedNotes: "聯邦發送至遠端伺服器的貼文可能會不受影響。"
|
||||
mayNotEffectSomeSituations: "這些限制已經簡化。它們可能不適用於某些情況,例如在遠端伺服器上檢視或管理時。"
|
||||
mayNotEffectSomeSituations: "這些限制僅是簡化版本。在某些情況下,例如在遠端伺服器上瀏覽或進行審核時,可能不會套用這些限制。"
|
||||
notesHavePassedSpecifiedPeriod: "早於指定時間的貼文"
|
||||
notesOlderThanSpecifiedDateAndTime: "指定時間和日期之前的貼文"
|
||||
_abuseUserReport:
|
||||
@@ -1931,7 +1934,7 @@ _role:
|
||||
canImportFollowing: "允許匯入追隨名單"
|
||||
canImportMuting: "允許匯入靜音名單"
|
||||
canImportUserLists: "允許匯入清單"
|
||||
canChat: "允許聊天"
|
||||
chatAvailability: "允許聊天"
|
||||
_condition:
|
||||
roleAssignedTo: "手動指派角色完成"
|
||||
isLocal: "本地使用者"
|
||||
@@ -2365,6 +2368,7 @@ _widgets:
|
||||
chooseList: "選擇清單"
|
||||
clicker: "點擊器"
|
||||
birthdayFollowings: "今天生日的使用者"
|
||||
chat: "聊天"
|
||||
_cw:
|
||||
hide: "隱藏"
|
||||
show: "顯示內容"
|
||||
@@ -2631,6 +2635,7 @@ _deck:
|
||||
mentions: "提及"
|
||||
direct: "指定使用者"
|
||||
roleTimeline: "角色時間軸"
|
||||
chat: "聊天"
|
||||
_dialog:
|
||||
charactersExceeded: "您的貼文太長了!現時字數 {current}/限制字數 {max}"
|
||||
charactersBelow: "您的貼文太短了!現時字數 {current}/限制字數 {min}"
|
||||
|
||||
+2
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.4.0-rc.2",
|
||||
"version": "2025.4.1-alpha.2",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -84,8 +84,7 @@
|
||||
"@aiscript-dev/aiscript-languageserver": "-"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"re2": "scripts/dependency-patches/re2.patch",
|
||||
"vite": "scripts/dependency-patches/vite.patch"
|
||||
"re2": "scripts/dependency-patches/re2.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class ExcludeNotesInSensitiveChannel1744075766000 {
|
||||
name = 'ExcludeNotesInSensitiveChannel1744075766000'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "antenna" RENAME COLUMN "hideNotesInSensitiveChannel" TO "excludeNotesInSensitiveChannel"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "antenna" RENAME COLUMN "excludeNotesInSensitiveChannel" TO "hideNotesInSensitiveChannel"`);
|
||||
}
|
||||
}
|
||||
@@ -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.11.18",
|
||||
"@swc/core-darwin-x64": "1.11.18",
|
||||
"@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.11.18",
|
||||
"@swc/core-linux-arm64-gnu": "1.11.18",
|
||||
"@swc/core-linux-arm64-musl": "1.11.18",
|
||||
"@swc/core-linux-x64-gnu": "1.11.18",
|
||||
"@swc/core-linux-x64-musl": "1.11.18",
|
||||
"@swc/core-win32-arm64-msvc": "1.11.18",
|
||||
"@swc/core-win32-ia32-msvc": "1.11.18",
|
||||
"@swc/core-win32-x64-msvc": "1.11.18",
|
||||
"@tensorflow/tfjs": "4.22.0",
|
||||
"@tensorflow/tfjs-node": "4.22.0",
|
||||
"bufferutil": "4.0.9",
|
||||
@@ -67,8 +67,8 @@
|
||||
"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.782.0",
|
||||
"@aws-sdk/lib-storage": "3.782.0",
|
||||
"@discordapp/twemoji": "15.1.0",
|
||||
"@fastify/accepts": "5.0.2",
|
||||
"@fastify/cookie": "11.0.2",
|
||||
@@ -78,12 +78,12 @@
|
||||
"@fastify/multipart": "9.0.3",
|
||||
"@fastify/static": "8.1.1",
|
||||
"@fastify/view": "10.0.2",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/sharp-read-bmp": "1.3.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.69",
|
||||
"@nestjs/common": "11.0.16",
|
||||
"@nestjs/core": "11.0.15",
|
||||
"@nestjs/testing": "11.0.15",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "8.55.0",
|
||||
"@sentry/profiling-node": "8.55.0",
|
||||
@@ -91,8 +91,9 @@
|
||||
"@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.11.18",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
"@types/redis-info": "3.0.3",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.17.1",
|
||||
"archiver": "7.0.1",
|
||||
@@ -100,7 +101,7 @@
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.3",
|
||||
"bullmq": "5.44.1",
|
||||
"bullmq": "5.48.1",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.2",
|
||||
"chalk": "5.4.1",
|
||||
@@ -111,13 +112,13 @@
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "2.30.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"fastify": "5.2.1",
|
||||
"fastify": "5.3.2",
|
||||
"fastify-raw-body": "5.0.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "19.6.0",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.2",
|
||||
"got": "14.4.6",
|
||||
"got": "14.4.7",
|
||||
"happy-dom": "16.8.1",
|
||||
"hpagent": "1.2.0",
|
||||
"htmlescape": "1.1.1",
|
||||
@@ -148,7 +149,7 @@
|
||||
"oauth2orize": "1.12.0",
|
||||
"oauth2orize-pkce": "0.1.2",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.3.6",
|
||||
"otpauth": "9.4.0",
|
||||
"parse5": "7.2.1",
|
||||
"pg": "8.14.1",
|
||||
"pkce-challenge": "4.1.0",
|
||||
@@ -159,6 +160,7 @@
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.21.4",
|
||||
"redis-info": "3.1.0",
|
||||
"redis-lock": "0.1.4",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rename": "1.0.4",
|
||||
@@ -166,17 +168,17 @@
|
||||
"rxjs": "7.8.2",
|
||||
"sanitize-html": "2.15.0",
|
||||
"secure-json-parse": "3.0.2",
|
||||
"sharp": "0.33.5",
|
||||
"sharp": "0.34.1",
|
||||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.25.11",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.3",
|
||||
"tsc-alias": "1.8.11",
|
||||
"tsc-alias": "1.8.15",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.21",
|
||||
"typescript": "5.8.2",
|
||||
"typeorm": "0.3.22",
|
||||
"typescript": "5.8.3",
|
||||
"ulid": "2.4.0",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.7",
|
||||
@@ -186,7 +188,7 @@
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@nestjs/platform-express": "10.4.15",
|
||||
"@sentry/vue": "9.8.0",
|
||||
"@sentry/vue": "9.12.0",
|
||||
"@simplewebauthn/types": "12.0.0",
|
||||
"@swc/jest": "0.2.37",
|
||||
"@types/accepts": "1.3.7",
|
||||
@@ -205,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.14.0",
|
||||
"@types/nodemailer": "6.4.17",
|
||||
"@types/oauth": "0.9.6",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
@@ -216,17 +218,17 @@
|
||||
"@types/random-seed": "0.3.5",
|
||||
"@types/ratelimiter": "3.4.6",
|
||||
"@types/rename": "1.0.7",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/sanitize-html": "2.15.0",
|
||||
"@types/semver": "7.7.0",
|
||||
"@types/simple-oauth2": "5.0.7",
|
||||
"@types/sinonjs__fake-timers": "8.1.5",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@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.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.29.1",
|
||||
"@typescript-eslint/parser": "8.29.1",
|
||||
"aws-sdk-client-mock": "4.1.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
|
||||
@@ -25,6 +25,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
|
||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { AntennaService } from '@/core/AntennaService.js';
|
||||
|
||||
@Injectable()
|
||||
export class AccountMoveService {
|
||||
@@ -63,6 +64,7 @@ export class AccountMoveService {
|
||||
private queueService: QueueService,
|
||||
private systemAccountService: SystemAccountService,
|
||||
private roleService: RoleService,
|
||||
private antennaService: AntennaService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -123,6 +125,7 @@ export class AccountMoveService {
|
||||
this.copyMutings(src, dst),
|
||||
this.copyRoles(src, dst),
|
||||
this.updateLists(src, dst),
|
||||
this.antennaService.onMoveAccount(src, dst),
|
||||
]);
|
||||
} catch {
|
||||
/* skip if any error happens */
|
||||
|
||||
@@ -5,18 +5,20 @@
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { In } from 'typeorm';
|
||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { AntennasRepository, UserListMembershipsRepository } from '@/models/_.js';
|
||||
import type { MiAntenna } from '@/models/Antenna.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AntennasRepository, UserListMembershipsRepository } from '@/models/_.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import { CacheService } from './CacheService.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
@@ -37,6 +39,7 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private utilityService: UtilityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private fanoutTimelineService: FanoutTimelineService,
|
||||
@@ -111,10 +114,7 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise<boolean> {
|
||||
if (note.visibility === 'specified') return false;
|
||||
if (note.visibility === 'followers') return false;
|
||||
|
||||
if (antenna.hideNotesInSensitiveChannel && note.channel?.isSensitive) return false;
|
||||
if (antenna.excludeNotesInSensitiveChannel && note.channel?.isSensitive) return false;
|
||||
|
||||
if (antenna.excludeBots && noteUser.isBot) return false;
|
||||
|
||||
@@ -122,6 +122,18 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
|
||||
if (!antenna.withReplies && note.replyId != null) return false;
|
||||
|
||||
if (note.visibility === 'specified') {
|
||||
if (note.userId !== antenna.userId) {
|
||||
if (note.visibleUserIds == null) return false;
|
||||
if (!note.visibleUserIds.includes(antenna.userId)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (note.visibility === 'followers') {
|
||||
const isFollowing = Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(antenna.userId), note.userId);
|
||||
if (!isFollowing && antenna.userId !== note.userId) return false;
|
||||
}
|
||||
|
||||
if (antenna.src === 'home') {
|
||||
// TODO
|
||||
} else if (antenna.src === 'list') {
|
||||
@@ -208,6 +220,41 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
return this.antennas;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async onMoveAccount(src: MiUser, dst: MiUser): Promise<void> {
|
||||
// There is a possibility for users to add the srcUser to their antennas, but it's low, so we don't check it.
|
||||
|
||||
// Get MiAntenna[] from cache and filter to select antennas with the src user is in the users list
|
||||
const srcUserAcct = this.utilityService.getFullApAccount(src.username, src.host).toLowerCase();
|
||||
const antennasToMigrate = (await this.getAntennas()).filter(antenna => {
|
||||
return antenna.users.some(user => {
|
||||
const { username, host } = Acct.parse(user);
|
||||
return this.utilityService.getFullApAccount(username, host).toLowerCase() === srcUserAcct;
|
||||
});
|
||||
});
|
||||
|
||||
if (antennasToMigrate.length === 0) return;
|
||||
|
||||
const antennaIds = antennasToMigrate.map(x => x.id);
|
||||
|
||||
// Update the antennas by appending dst users acct to the users list
|
||||
const dstUserAcct = '@' + Acct.toString({ username: dst.username, host: dst.host });
|
||||
|
||||
await this.antennasRepository.createQueryBuilder('antenna')
|
||||
.update()
|
||||
.set({
|
||||
users: () => 'array_append(antenna.users, :dstUserAcct)',
|
||||
})
|
||||
.where('antenna.id IN (:...antennaIds)', { antennaIds })
|
||||
.setParameters({ dstUserAcct })
|
||||
.execute();
|
||||
|
||||
// announce update to event
|
||||
for (const newAntenna of await this.antennasRepository.findBy({ id: In(antennaIds) })) {
|
||||
this.globalEventService.publishInternalEvent('antennaUpdated', newAntenna);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onRedisMessage);
|
||||
|
||||
@@ -94,6 +94,40 @@ export class ChatService {
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getChatAvailability(userId: MiUser['id']): Promise<{ read: boolean; write: boolean; }> {
|
||||
const policies = await this.roleService.getUserPolicies(userId);
|
||||
|
||||
switch (policies.chatAvailability) {
|
||||
case 'available':
|
||||
return {
|
||||
read: true,
|
||||
write: true,
|
||||
};
|
||||
case 'readonly':
|
||||
return {
|
||||
read: true,
|
||||
write: false,
|
||||
};
|
||||
case 'unavailable':
|
||||
return {
|
||||
read: false,
|
||||
write: false,
|
||||
};
|
||||
default:
|
||||
throw new Error('invalid chat availability (unreachable)');
|
||||
}
|
||||
}
|
||||
|
||||
/** getChatAvailabilityの糖衣。主にAPI呼び出し時に走らせて、権限的に問題ない場合はそのまま続行する */
|
||||
@bindThis
|
||||
public async checkChatAvailability(userId: MiUser['id'], permission: 'read' | 'write') {
|
||||
const policy = await this.getChatAvailability(userId);
|
||||
if (policy[permission] === false) {
|
||||
throw new Error('ROLE_PERMISSION_DENIED');
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createMessageToUser(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toUser: MiUser, params: {
|
||||
text?: string | null;
|
||||
@@ -140,7 +174,7 @@ export class ChatService {
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await this.roleService.getUserPolicies(toUser.id)).canChat) {
|
||||
if (!(await this.getChatAvailability(toUser.id)).write) {
|
||||
throw new Error('recipient is cannot chat (policy)');
|
||||
}
|
||||
|
||||
@@ -198,7 +232,7 @@ export class ChatService {
|
||||
|
||||
const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted, toUser);
|
||||
this.globalEventService.publishMainStream(toUser.id, 'newChatMessage', packedMessageForTo);
|
||||
//this.pushNotificationService.pushNotification(toUser.id, 'newChatMessage', packedMessageForTo);
|
||||
this.pushNotificationService.pushNotification(toUser.id, 'newChatMessage', packedMessageForTo);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
@@ -268,7 +302,7 @@ export class ChatService {
|
||||
if (marker == null) continue;
|
||||
|
||||
this.globalEventService.publishMainStream(membershipsOtherThanMe[i].userId, 'newChatMessage', packedMessageForTo);
|
||||
//this.pushNotificationService.pushNotification(membershipsOtherThanMe[i].userId, 'newChatMessage', packedMessageForTo);
|
||||
this.pushNotificationService.pushNotification(membershipsOtherThanMe[i].userId, 'newChatMessage', packedMessageForTo);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export class FanoutTimelineEndpointService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
|
||||
async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
|
||||
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
|
||||
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { URL } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as parse5 from 'parse5';
|
||||
import { Window, XMLSerializer } from 'happy-dom';
|
||||
import { type Document, type HTMLParagraphElement, Window, XMLSerializer } from 'happy-dom';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { intersperse } from '@/misc/prelude/array.js';
|
||||
@@ -23,6 +23,8 @@ type ChildNode = DefaultTreeAdapterMap['childNode'];
|
||||
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
|
||||
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
|
||||
|
||||
export type Appender = (document: Document, body: HTMLParagraphElement) => void;
|
||||
|
||||
@Injectable()
|
||||
export class MfmService {
|
||||
constructor(
|
||||
@@ -267,7 +269,7 @@ export class MfmService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) {
|
||||
public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], additionalAppenders: Appender[] = []) {
|
||||
if (nodes == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -492,6 +494,10 @@ export class MfmService {
|
||||
|
||||
appendChildren(nodes, body);
|
||||
|
||||
for (const additionalAppender of additionalAppenders) {
|
||||
additionalAppender(doc, body);
|
||||
}
|
||||
|
||||
// Remove the unnecessary namespace
|
||||
const serialized = new XMLSerializer().serializeToString(body).replace(/^\s*<p xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">/, '<p>');
|
||||
|
||||
|
||||
@@ -576,7 +576,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
noteId: note.id,
|
||||
}, {
|
||||
delay,
|
||||
removeOnComplete: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ type PushNotificationsTypes = {
|
||||
note: Packed<'Note'>;
|
||||
};
|
||||
'readAllNotifications': undefined;
|
||||
newChatMessage: Packed<'ChatMessage'>;
|
||||
};
|
||||
|
||||
// Reduce length because push message servers have character limits
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MetricsTime, type JobType } from 'bullmq';
|
||||
import { parse as parseRedisInfo } from 'redis-info';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
|
||||
@@ -38,6 +40,18 @@ import type {
|
||||
import type httpSignature from '@peertube/http-signature';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
export const QUEUE_TYPES = [
|
||||
'system',
|
||||
'endedPollNotification',
|
||||
'deliver',
|
||||
'inbox',
|
||||
'db',
|
||||
'relationship',
|
||||
'objectStorage',
|
||||
'userWebhookDeliver',
|
||||
'systemWebhookDeliver',
|
||||
] as const;
|
||||
|
||||
@Injectable()
|
||||
export class QueueService {
|
||||
constructor(
|
||||
@@ -57,50 +71,58 @@ export class QueueService {
|
||||
this.systemQueue.add('tickCharts', {
|
||||
}, {
|
||||
repeat: { pattern: '55 * * * *' },
|
||||
removeOnComplete: true,
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
|
||||
this.systemQueue.add('resyncCharts', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
|
||||
this.systemQueue.add('cleanCharts', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
|
||||
this.systemQueue.add('aggregateRetention', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
|
||||
this.systemQueue.add('clean', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
|
||||
this.systemQueue.add('checkExpiredMutings', {
|
||||
}, {
|
||||
repeat: { pattern: '*/5 * * * *' },
|
||||
removeOnComplete: true,
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
|
||||
this.systemQueue.add('bakeBufferedReactions', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
|
||||
this.systemQueue.add('checkModeratorsActivity', {
|
||||
}, {
|
||||
// 毎時30分に起動
|
||||
repeat: { pattern: '30 * * * *' },
|
||||
removeOnComplete: true,
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -122,13 +144,21 @@ export class QueueService {
|
||||
isSharedInbox,
|
||||
};
|
||||
|
||||
return this.deliverQueue.add(to, data, {
|
||||
const label = to.replace('https://', '').replace('/inbox', '');
|
||||
|
||||
return this.deliverQueue.add(label, data, {
|
||||
attempts: this.config.deliverJobMaxAttempts ?? 12,
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -150,12 +180,18 @@ export class QueueService {
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
};
|
||||
|
||||
await this.deliverQueue.addBulk(Array.from(inboxes.entries(), d => ({
|
||||
name: d[0],
|
||||
name: d[0].replace('https://', '').replace('/inbox', ''),
|
||||
data: {
|
||||
user,
|
||||
content: contentBody,
|
||||
@@ -176,13 +212,21 @@ export class QueueService {
|
||||
signature,
|
||||
};
|
||||
|
||||
return this.inboxQueue.add('', data, {
|
||||
const label = (activity.id ?? '').replace('https://', '').replace('/activity', '');
|
||||
|
||||
return this.inboxQueue.add(label, data, {
|
||||
attempts: this.config.inboxJobMaxAttempts ?? 8,
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,8 +235,14 @@ export class QueueService {
|
||||
return this.dbQueue.add('deleteDriveFiles', {
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -201,8 +251,14 @@ export class QueueService {
|
||||
return this.dbQueue.add('exportCustomEmojis', {
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,8 +267,14 @@ export class QueueService {
|
||||
return this.dbQueue.add('exportNotes', {
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -221,8 +283,14 @@ export class QueueService {
|
||||
return this.dbQueue.add('exportClips', {
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -231,8 +299,14 @@ export class QueueService {
|
||||
return this.dbQueue.add('exportFavorites', {
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -243,8 +317,14 @@ export class QueueService {
|
||||
excludeMuting,
|
||||
excludeInactive,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -253,8 +333,14 @@ export class QueueService {
|
||||
return this.dbQueue.add('exportMuting', {
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -263,8 +349,14 @@ export class QueueService {
|
||||
return this.dbQueue.add('exportBlocking', {
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -273,8 +365,14 @@ export class QueueService {
|
||||
return this.dbQueue.add('exportUserLists', {
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -283,8 +381,14 @@ export class QueueService {
|
||||
return this.dbQueue.add('exportAntennas', {
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -295,8 +399,14 @@ export class QueueService {
|
||||
fileId: fileId,
|
||||
withReplies,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -312,8 +422,14 @@ export class QueueService {
|
||||
user: { id: user.id },
|
||||
fileId: fileId,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -323,8 +439,14 @@ export class QueueService {
|
||||
user: { id: user.id },
|
||||
fileId: fileId,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -344,8 +466,14 @@ export class QueueService {
|
||||
name,
|
||||
data,
|
||||
opts: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -356,8 +484,14 @@ export class QueueService {
|
||||
user: { id: user.id },
|
||||
fileId: fileId,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -367,8 +501,14 @@ export class QueueService {
|
||||
user: { id: user.id },
|
||||
fileId: fileId,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -378,8 +518,14 @@ export class QueueService {
|
||||
user: { id: user.id },
|
||||
antenna,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -389,8 +535,14 @@ export class QueueService {
|
||||
user: { id: user.id },
|
||||
soft: opts.soft,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -440,8 +592,14 @@ export class QueueService {
|
||||
withReplies: data.withReplies,
|
||||
},
|
||||
opts: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
...opts,
|
||||
},
|
||||
};
|
||||
@@ -452,16 +610,28 @@ export class QueueService {
|
||||
return this.objectStorageQueue.add('deleteFile', {
|
||||
key: key,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createCleanRemoteFilesJob() {
|
||||
return this.objectStorageQueue.add('cleanRemoteFiles', {}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -492,8 +662,14 @@ export class QueueService {
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -523,21 +699,201 @@ export class QueueService {
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public destroy() {
|
||||
this.deliverQueue.once('cleaned', (jobs, status) => {
|
||||
//deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||
});
|
||||
this.deliverQueue.clean(0, 0, 'delayed');
|
||||
private getQueue(type: typeof QUEUE_TYPES[number]): Bull.Queue {
|
||||
switch (type) {
|
||||
case 'system': return this.systemQueue;
|
||||
case 'endedPollNotification': return this.endedPollNotificationQueue;
|
||||
case 'deliver': return this.deliverQueue;
|
||||
case 'inbox': return this.inboxQueue;
|
||||
case 'db': return this.dbQueue;
|
||||
case 'relationship': return this.relationshipQueue;
|
||||
case 'objectStorage': return this.objectStorageQueue;
|
||||
case 'userWebhookDeliver': return this.userWebhookDeliverQueue;
|
||||
case 'systemWebhookDeliver': return this.systemWebhookDeliverQueue;
|
||||
default: throw new Error(`Unrecognized queue type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.inboxQueue.once('cleaned', (jobs, status) => {
|
||||
//inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||
@bindThis
|
||||
public async queueClear(queueType: typeof QUEUE_TYPES[number], state: '*' | 'completed' | 'wait' | 'active' | 'paused' | 'prioritized' | 'delayed' | 'failed') {
|
||||
const queue = this.getQueue(queueType);
|
||||
|
||||
if (state === '*') {
|
||||
await Promise.all([
|
||||
queue.clean(0, 0, 'completed'),
|
||||
queue.clean(0, 0, 'wait'),
|
||||
queue.clean(0, 0, 'active'),
|
||||
queue.clean(0, 0, 'paused'),
|
||||
queue.clean(0, 0, 'prioritized'),
|
||||
queue.clean(0, 0, 'delayed'),
|
||||
queue.clean(0, 0, 'failed'),
|
||||
]);
|
||||
} else {
|
||||
await queue.clean(0, 0, state);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async queuePromoteJobs(queueType: typeof QUEUE_TYPES[number]) {
|
||||
const queue = this.getQueue(queueType);
|
||||
await queue.promoteJobs();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
|
||||
const queue = this.getQueue(queueType);
|
||||
const job: Bull.Job | null = await queue.getJob(jobId);
|
||||
if (job) {
|
||||
if (job.finishedOn != null) {
|
||||
await job.retry();
|
||||
} else {
|
||||
await job.promote();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async queueRemoveJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
|
||||
const queue = this.getQueue(queueType);
|
||||
const job: Bull.Job | null = await queue.getJob(jobId);
|
||||
if (job) {
|
||||
await job.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private packJobData(job: Bull.Job) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
const stacktrace = job.stacktrace ? job.stacktrace.filter(Boolean) : [];
|
||||
stacktrace.reverse();
|
||||
|
||||
return {
|
||||
id: job.id,
|
||||
name: job.name,
|
||||
data: job.data,
|
||||
opts: job.opts,
|
||||
timestamp: job.timestamp,
|
||||
processedOn: job.processedOn,
|
||||
processedBy: job.processedBy,
|
||||
finishedOn: job.finishedOn,
|
||||
progress: job.progress,
|
||||
attempts: job.attemptsMade,
|
||||
delay: job.delay,
|
||||
failedReason: job.failedReason,
|
||||
stacktrace: stacktrace,
|
||||
returnValue: job.returnvalue,
|
||||
isFailed: !!job.failedReason || (Array.isArray(stacktrace) && stacktrace.length > 0),
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async queueGetJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
|
||||
const queue = this.getQueue(queueType);
|
||||
const job: Bull.Job | null = await queue.getJob(jobId);
|
||||
if (job) {
|
||||
return this.packJobData(job);
|
||||
} else {
|
||||
throw new Error(`Job not found: ${jobId}`);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async queueGetJobs(queueType: typeof QUEUE_TYPES[number], jobTypes: JobType[], search?: string) {
|
||||
const RETURN_LIMIT = 100;
|
||||
const queue = this.getQueue(queueType);
|
||||
let jobs: Bull.Job[];
|
||||
|
||||
if (search) {
|
||||
jobs = await queue.getJobs(jobTypes, 0, 1000);
|
||||
|
||||
jobs = jobs.filter(job => {
|
||||
const jobString = JSON.stringify(job).toLowerCase();
|
||||
return search.toLowerCase().split(' ').every(term => {
|
||||
return jobString.includes(term);
|
||||
});
|
||||
});
|
||||
|
||||
jobs = jobs.slice(0, RETURN_LIMIT);
|
||||
} else {
|
||||
jobs = await queue.getJobs(jobTypes, 0, RETURN_LIMIT);
|
||||
}
|
||||
|
||||
return jobs.map(job => this.packJobData(job));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async queueGetQueues() {
|
||||
const fetchings = QUEUE_TYPES.map(async type => {
|
||||
const queue = this.getQueue(type);
|
||||
|
||||
const counts = await queue.getJobCounts();
|
||||
const isPaused = await queue.isPaused();
|
||||
const metrics_completed = await queue.getMetrics('completed', 0, MetricsTime.ONE_WEEK);
|
||||
const metrics_failed = await queue.getMetrics('failed', 0, MetricsTime.ONE_WEEK);
|
||||
|
||||
return {
|
||||
name: type,
|
||||
counts: counts,
|
||||
isPaused,
|
||||
metrics: {
|
||||
completed: metrics_completed,
|
||||
failed: metrics_failed,
|
||||
},
|
||||
};
|
||||
});
|
||||
this.inboxQueue.clean(0, 0, 'delayed');
|
||||
|
||||
return await Promise.all(fetchings);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async queueGetQueue(queueType: typeof QUEUE_TYPES[number]) {
|
||||
const queue = this.getQueue(queueType);
|
||||
const counts = await queue.getJobCounts();
|
||||
const isPaused = await queue.isPaused();
|
||||
const metrics_completed = await queue.getMetrics('completed', 0, MetricsTime.ONE_WEEK);
|
||||
const metrics_failed = await queue.getMetrics('failed', 0, MetricsTime.ONE_WEEK);
|
||||
const db = parseRedisInfo(await (await queue.client).info());
|
||||
|
||||
return {
|
||||
name: queueType,
|
||||
qualifiedName: queue.qualifiedName,
|
||||
counts: counts,
|
||||
isPaused,
|
||||
metrics: {
|
||||
completed: metrics_completed,
|
||||
failed: metrics_failed,
|
||||
},
|
||||
db: {
|
||||
version: db.redis_version,
|
||||
mode: db.redis_mode,
|
||||
runId: db.run_id,
|
||||
processId: db.process_id,
|
||||
port: parseInt(db.tcp_port),
|
||||
os: db.os,
|
||||
uptime: parseInt(db.uptime_in_seconds),
|
||||
memory: {
|
||||
total: parseInt(db.total_system_memory) || parseInt(db.maxmemory),
|
||||
used: parseInt(db.used_memory),
|
||||
fragmentationRatio: parseInt(db.mem_fragmentation_ratio),
|
||||
peak: parseInt(db.used_memory_peak),
|
||||
},
|
||||
clients: {
|
||||
connected: parseInt(db.connected_clients),
|
||||
blocked: parseInt(db.blocked_clients),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export type RolePolicies = {
|
||||
canImportFollowing: boolean;
|
||||
canImportMuting: boolean;
|
||||
canImportUserLists: boolean;
|
||||
canChat: boolean;
|
||||
chatAvailability: 'available' | 'readonly' | 'unavailable';
|
||||
};
|
||||
|
||||
export const DEFAULT_POLICIES: RolePolicies = {
|
||||
@@ -98,7 +98,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
canImportFollowing: true,
|
||||
canImportMuting: true,
|
||||
canImportUserLists: true,
|
||||
canChat: true,
|
||||
chatAvailability: 'available',
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -370,6 +370,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
return aggregate(policies.map(policy => policy.useDefault ? basePolicies[name] : policy.value));
|
||||
}
|
||||
|
||||
function aggregateChatAvailability(vs: RolePolicies['chatAvailability'][]) {
|
||||
if (vs.some(v => v === 'available')) return 'available';
|
||||
if (vs.some(v => v === 'readonly')) return 'readonly';
|
||||
return 'unavailable';
|
||||
}
|
||||
|
||||
return {
|
||||
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
||||
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
||||
@@ -402,7 +408,7 @@ 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)),
|
||||
chatAvailability: calc('chatAvailability', aggregateChatAvailability),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
import { DataSource, IsNull } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { MiLocalUser, MiUser } from '@/models/User.js';
|
||||
import { MiSystemAccount, MiUsedUsername, MiUserKeypair, MiUserProfile, type UsersRepository, type SystemAccountsRepository } from '@/models/_.js';
|
||||
import type { MiMeta, UserProfilesRepository } from '@/models/_.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { MemoryKVCache } from '@/misc/cache.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@@ -20,10 +23,13 @@ import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
|
||||
export const SYSTEM_ACCOUNT_TYPES = ['actor', 'relay', 'proxy'] as const;
|
||||
|
||||
@Injectable()
|
||||
export class SystemAccountService {
|
||||
export class SystemAccountService implements OnApplicationShutdown {
|
||||
private cache: MemoryKVCache<MiLocalUser>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@@ -42,6 +48,31 @@ export class SystemAccountService {
|
||||
private idService: IdService,
|
||||
) {
|
||||
this.cache = new MemoryKVCache<MiLocalUser>(1000 * 60 * 10); // 10m
|
||||
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'metaUpdated': {
|
||||
if (body.before != null && body.before.name !== body.after.name) {
|
||||
for (const account of SYSTEM_ACCOUNT_TYPES) {
|
||||
await this.updateCorrespondingUserProfile(account, {
|
||||
name: body.after.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -145,7 +176,7 @@ export class SystemAccountService {
|
||||
|
||||
@bindThis
|
||||
public async updateCorrespondingUserProfile(type: typeof SYSTEM_ACCOUNT_TYPES[number], extra: {
|
||||
name?: string;
|
||||
name?: string | null;
|
||||
description?: MiUserProfile['description'];
|
||||
}): Promise<MiLocalUser> {
|
||||
const user = await this.fetch(type);
|
||||
@@ -169,4 +200,15 @@ export class SystemAccountService {
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
this.cache.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,8 +411,8 @@ export class WebhookTestService {
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
avatarUrl: user.avatarUrl,
|
||||
avatarBlurhash: user.avatarBlurhash,
|
||||
avatarUrl: user.avatarId == null ? null : user.avatarUrl,
|
||||
avatarBlurhash: user.avatarId == null ? null : user.avatarBlurhash,
|
||||
avatarDecorations: user.avatarDecorations.map(it => ({
|
||||
id: it.id,
|
||||
angle: it.angle,
|
||||
@@ -441,8 +441,8 @@ export class WebhookTestService {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: user.updatedAt?.toISOString() ?? null,
|
||||
lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
|
||||
bannerUrl: user.bannerUrl,
|
||||
bannerBlurhash: user.bannerBlurhash,
|
||||
bannerUrl: user.bannerId == null ? null : user.bannerUrl,
|
||||
bannerBlurhash: user.bannerId == null ? null : user.bannerBlurhash,
|
||||
isLocked: user.isLocked,
|
||||
isSilenced: false,
|
||||
isSuspended: user.isSuspended,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { MfmService } from '@/core/MfmService.js';
|
||||
import { MfmService, Appender } from '@/core/MfmService.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { extractApHashtagObjects } from './models/tag.js';
|
||||
@@ -25,17 +25,17 @@ export class ApMfmService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, apAppend?: string) {
|
||||
public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, additionalAppender: Appender[] = []) {
|
||||
let noMisskeyContent = false;
|
||||
const srcMfm = (note.text ?? '') + (apAppend ?? '');
|
||||
const srcMfm = (note.text ?? '');
|
||||
|
||||
const parsed = mfm.parse(srcMfm);
|
||||
|
||||
if (!apAppend && parsed?.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
|
||||
if (!additionalAppender.length && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
|
||||
noMisskeyContent = true;
|
||||
}
|
||||
|
||||
const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers));
|
||||
const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers), additionalAppender);
|
||||
|
||||
return {
|
||||
content,
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { MiEmoji } from '@/models/Emoji.js';
|
||||
import type { MiPoll } from '@/models/Poll.js';
|
||||
import type { MiPollVote } from '@/models/PollVote.js';
|
||||
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||
import { MfmService } from '@/core/MfmService.js';
|
||||
import { MfmService, type Appender } from '@/core/MfmService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import type { MiUserKeypair } from '@/models/UserKeypair.js';
|
||||
@@ -430,10 +430,24 @@ export class ApRendererService {
|
||||
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||
}
|
||||
|
||||
let apAppend = '';
|
||||
const apAppend: Appender[] = [];
|
||||
|
||||
if (quote) {
|
||||
apAppend += `\n\nRE: ${quote}`;
|
||||
// Append quote link as `<br><br><span class="quote-inline">RE: <a href="...">...</a></span>`
|
||||
// the claas name `quote-inline` is used in non-misskey clients for styling quote notes.
|
||||
// For compatibility, the span part should be kept as possible.
|
||||
apAppend.push((doc, body) => {
|
||||
body.appendChild(doc.createElement('br'));
|
||||
body.appendChild(doc.createElement('br'));
|
||||
const span = doc.createElement('span');
|
||||
span.className = 'quote-inline';
|
||||
span.appendChild(doc.createTextNode('RE: '));
|
||||
const link = doc.createElement('a');
|
||||
link.setAttribute('href', quote);
|
||||
link.textContent = quote;
|
||||
span.appendChild(link);
|
||||
body.appendChild(span);
|
||||
});
|
||||
}
|
||||
|
||||
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
||||
@@ -509,7 +523,7 @@ export class ApRendererService {
|
||||
const urlPart = match[0];
|
||||
const urlPartParsed = new URL(urlPart);
|
||||
const restPart = maybeUrl.slice(match[0].length);
|
||||
|
||||
|
||||
return `<a href="${urlPartParsed.href}" rel="me nofollow noopener" target="_blank">${urlPart}</a>${restPart}`;
|
||||
} catch (e) {
|
||||
return maybeUrl;
|
||||
|
||||
@@ -41,7 +41,7 @@ export class AntennaEntityService {
|
||||
excludeBots: antenna.excludeBots,
|
||||
withReplies: antenna.withReplies,
|
||||
withFile: antenna.withFile,
|
||||
hideNotesInSensitiveChannel: antenna.hideNotesInSensitiveChannel,
|
||||
excludeNotesInSensitiveChannel: antenna.excludeNotesInSensitiveChannel,
|
||||
isActive: antenna.isActive,
|
||||
hasUnreadNote: false, // TODO
|
||||
notify: false, // 後方互換性のため
|
||||
|
||||
@@ -486,8 +486,8 @@ export class UserEntityService implements OnModuleInit {
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
avatarUrl: user.avatarUrl ?? this.getIdenticonUrl(user),
|
||||
avatarBlurhash: user.avatarBlurhash,
|
||||
avatarUrl: (user.avatarId == null ? null : user.avatarUrl) ?? this.getIdenticonUrl(user),
|
||||
avatarBlurhash: (user.avatarId == null ? null : user.avatarBlurhash),
|
||||
avatarDecorations: user.avatarDecorations.length > 0 ? this.avatarDecorationService.getAll().then(decorations => user.avatarDecorations.filter(ud => decorations.some(d => d.id === ud.id)).map(ud => ({
|
||||
id: ud.id,
|
||||
angle: ud.angle || undefined,
|
||||
@@ -533,8 +533,8 @@ export class UserEntityService implements OnModuleInit {
|
||||
createdAt: this.idService.parse(user.id).date.toISOString(),
|
||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
|
||||
bannerUrl: user.bannerUrl,
|
||||
bannerBlurhash: user.bannerBlurhash,
|
||||
bannerUrl: user.bannerId == null ? null : user.bannerUrl,
|
||||
bannerBlurhash: user.bannerId == null ? null : user.bannerBlurhash,
|
||||
isLocked: user.isLocked,
|
||||
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
||||
isSuspended: user.isSuspended,
|
||||
@@ -557,7 +557,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
followersVisibility: profile!.followersVisibility,
|
||||
followingVisibility: profile!.followingVisibility,
|
||||
chatScope: user.chatScope,
|
||||
canChat: this.roleService.getUserPolicies(user.id).then(r => r.canChat),
|
||||
canChat: this.roleService.getUserPolicies(user.id).then(r => r.chatAvailability === 'available'),
|
||||
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,
|
||||
|
||||
@@ -104,5 +104,5 @@ export class MiAntenna {
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public hideNotesInSensitiveChannel: boolean;
|
||||
public excludeNotesInSensitiveChannel: boolean;
|
||||
}
|
||||
|
||||
@@ -118,21 +118,25 @@ export class MiUser {
|
||||
@JoinColumn()
|
||||
public banner: MiDriveFile | null;
|
||||
|
||||
// avatarId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは avatarId の non-null チェックをすること
|
||||
@Column('varchar', {
|
||||
length: 512, nullable: true,
|
||||
})
|
||||
public avatarUrl: string | null;
|
||||
|
||||
// bannerId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは bannerId の non-null チェックをすること
|
||||
@Column('varchar', {
|
||||
length: 512, nullable: true,
|
||||
})
|
||||
public bannerUrl: string | null;
|
||||
|
||||
// avatarId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは avatarId の non-null チェックをすること
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public avatarBlurhash: string | null;
|
||||
|
||||
// bannerId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは bannerId の non-null チェックをすること
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
|
||||
@@ -3,29 +3,48 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
|
||||
import {
|
||||
FindOneOptions,
|
||||
InsertQueryBuilder,
|
||||
ObjectLiteral,
|
||||
QueryRunner,
|
||||
Repository,
|
||||
SelectQueryBuilder,
|
||||
} from 'typeorm';
|
||||
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.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 { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||
import {
|
||||
RawSqlResultsToEntityTransformer,
|
||||
} from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
|
||||
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
|
||||
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||
import { MiAccessToken } from '@/models/AccessToken.js';
|
||||
import { MiAd } from '@/models/Ad.js';
|
||||
import { MiAnnouncement } from '@/models/Announcement.js';
|
||||
import { MiAnnouncementRead } from '@/models/AnnouncementRead.js';
|
||||
import { MiAntenna } from '@/models/Antenna.js';
|
||||
import { MiApp } from '@/models/App.js';
|
||||
import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
|
||||
import { MiAuthSession } from '@/models/AuthSession.js';
|
||||
import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
|
||||
import { MiBlocking } from '@/models/Blocking.js';
|
||||
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiChannel } from '@/models/Channel.js';
|
||||
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
|
||||
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiChatMessage } from '@/models/ChatMessage.js';
|
||||
import { MiChatRoom } from '@/models/ChatRoom.js';
|
||||
import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
|
||||
import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
|
||||
import { MiClip } from '@/models/Clip.js';
|
||||
import { MiClipNote } from '@/models/ClipNote.js';
|
||||
import { MiClipFavorite } from '@/models/ClipFavorite.js';
|
||||
import { MiClipNote } from '@/models/ClipNote.js';
|
||||
import { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { MiDriveFolder } from '@/models/DriveFolder.js';
|
||||
import { MiEmoji } from '@/models/Emoji.js';
|
||||
import { MiFlash } from '@/models/Flash.js';
|
||||
import { MiFlashLike } from '@/models/FlashLike.js';
|
||||
import { MiFollowing } from '@/models/Following.js';
|
||||
import { MiFollowRequest } from '@/models/FollowRequest.js';
|
||||
import { MiGalleryLike } from '@/models/GalleryLike.js';
|
||||
@@ -35,7 +54,6 @@ import { MiInstance } from '@/models/Instance.js';
|
||||
import { MiMeta } from '@/models/Meta.js';
|
||||
import { MiModerationLog } from '@/models/ModerationLog.js';
|
||||
import { MiMuting } from '@/models/Muting.js';
|
||||
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
||||
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||
@@ -50,42 +68,38 @@ import { MiPromoRead } from '@/models/PromoRead.js';
|
||||
import { MiRegistrationTicket } from '@/models/RegistrationTicket.js';
|
||||
import { MiRegistryItem } from '@/models/RegistryItem.js';
|
||||
import { MiRelay } from '@/models/Relay.js';
|
||||
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
||||
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { MiRole } from '@/models/Role.js';
|
||||
import { MiRoleAssignment } from '@/models/RoleAssignment.js';
|
||||
import { MiSignin } from '@/models/Signin.js';
|
||||
import { MiSwSubscription } from '@/models/SwSubscription.js';
|
||||
import { MiSystemAccount } from '@/models/SystemAccount.js';
|
||||
import { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
||||
import { MiUsedUsername } from '@/models/UsedUsername.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { MiUserIp } from '@/models/UserIp.js';
|
||||
import { MiUserKeypair } from '@/models/UserKeypair.js';
|
||||
import { MiUserList } from '@/models/UserList.js';
|
||||
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
|
||||
import { MiUserListMembership } from '@/models/UserListMembership.js';
|
||||
import { MiUserMemo } from '@/models/UserMemo.js';
|
||||
import { MiUserNotePining } from '@/models/UserNotePining.js';
|
||||
import { MiUserPending } from '@/models/UserPending.js';
|
||||
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
||||
import { MiUserMemo } from '@/models/UserMemo.js';
|
||||
import { MiWebhook } from '@/models/Webhook.js';
|
||||
import { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
||||
import { MiChannel } from '@/models/Channel.js';
|
||||
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
|
||||
import { MiRole } from '@/models/Role.js';
|
||||
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';
|
||||
|
||||
export interface MiRepository<T extends ObjectLiteral> {
|
||||
createTableColumnNames(this: Repository<T> & MiRepository<T>): string[];
|
||||
|
||||
insertOne(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>): Promise<T>;
|
||||
|
||||
insertOneImpl(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>, queryRunner?: QueryRunner): Promise<T>;
|
||||
|
||||
selectAliasColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>, builder: SelectQueryBuilder<T>): void;
|
||||
}
|
||||
|
||||
@@ -94,6 +108,21 @@ export const miRepository = {
|
||||
return this.metadata.columns.filter(column => column.isSelect && !column.isVirtual).map(column => column.databaseName);
|
||||
},
|
||||
async insertOne(entity, findOptions?) {
|
||||
const opt = this.manager.connection.options as PostgresConnectionOptions;
|
||||
if (opt.replication) {
|
||||
const queryRunner = this.manager.connection.createQueryRunner('master');
|
||||
try {
|
||||
return this.insertOneImpl(entity, findOptions, queryRunner);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
} else {
|
||||
return this.insertOneImpl(entity, findOptions);
|
||||
}
|
||||
},
|
||||
async insertOneImpl(entity, findOptions?, queryRunner?) {
|
||||
// ---- insert + returningの結果を共通テーブル式(CTE)に保持するクエリを生成 ----
|
||||
|
||||
const queryBuilder = this.createQueryBuilder().insert().values(entity);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const mainAlias = queryBuilder.expressionMap.mainAlias!;
|
||||
@@ -101,7 +130,9 @@ export const miRepository = {
|
||||
mainAlias.name = 't';
|
||||
const columnNames = this.createTableColumnNames();
|
||||
queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2));
|
||||
const builder = this.createQueryBuilder().addCommonTableExpression(queryBuilder, 'cte', { columnNames });
|
||||
|
||||
// ---- 共通テーブル式(CTE)から結果を取得 ----
|
||||
const builder = this.createQueryBuilder(undefined, queryRunner).addCommonTableExpression(queryBuilder, 'cte', { columnNames });
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
builder.expressionMap.mainAlias!.tablePath = 'cte';
|
||||
this.selectAliasColumnNames(queryBuilder, builder);
|
||||
@@ -204,7 +235,9 @@ export {
|
||||
};
|
||||
|
||||
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
|
||||
export type AbuseReportNotificationRecipientRepository = Repository<MiAbuseReportNotificationRecipient> & MiRepository<MiAbuseReportNotificationRecipient>;
|
||||
export type AbuseReportNotificationRecipientRepository =
|
||||
Repository<MiAbuseReportNotificationRecipient>
|
||||
& MiRepository<MiAbuseReportNotificationRecipient>;
|
||||
export type AccessTokensRepository = Repository<MiAccessToken> & MiRepository<MiAccessToken>;
|
||||
export type AdsRepository = Repository<MiAd> & MiRepository<MiAd>;
|
||||
export type AnnouncementsRepository = Repository<MiAnnouncement> & MiRepository<MiAnnouncement>;
|
||||
|
||||
@@ -100,7 +100,7 @@ export const packedAntennaSchema = {
|
||||
optional: false, nullable: false,
|
||||
default: false,
|
||||
},
|
||||
hideNotesInSensitiveChannel: {
|
||||
excludeNotesInSensitiveChannel: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
default: false,
|
||||
|
||||
@@ -292,9 +292,10 @@ export const packedRolePoliciesSchema = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canChat: {
|
||||
type: 'boolean',
|
||||
chatAvailability: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['available', 'readonly', 'unavailable'],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
// https://github.com/typeorm/typeorm/issues/2400
|
||||
import pg from 'pg';
|
||||
import { DataSource, Logger } from 'typeorm';
|
||||
import { DataSource, Logger, type QueryRunner } from 'typeorm';
|
||||
import * as highlight from 'cli-highlight';
|
||||
import { entities as charts } from '@/core/chart/entities.js';
|
||||
import { Config } from '@/config.js';
|
||||
@@ -96,6 +96,7 @@ const sqlLogger = dbLogger.createSubLogger('sql', 'gray');
|
||||
export type LoggerProps = {
|
||||
disableQueryTruncation?: boolean;
|
||||
enableQueryParamLogging?: boolean;
|
||||
printReplicationMode?: boolean,
|
||||
};
|
||||
|
||||
function highlightSql(sql: string) {
|
||||
@@ -121,8 +122,10 @@ class MyCustomLogger implements Logger {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private transformQueryLog(sql: string) {
|
||||
let modded = sql;
|
||||
private transformQueryLog(sql: string, opts?: {
|
||||
prefix?: string;
|
||||
}) {
|
||||
let modded = opts?.prefix ? opts.prefix + sql : sql;
|
||||
if (!this.props.disableQueryTruncation) {
|
||||
modded = truncateSql(modded);
|
||||
}
|
||||
@@ -140,18 +143,27 @@ class MyCustomLogger implements Logger {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public logQuery(query: string, parameters?: any[]) {
|
||||
sqlLogger.info(this.transformQueryLog(query), this.transformParameters(parameters));
|
||||
public logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) {
|
||||
const prefix = (this.props.printReplicationMode && queryRunner)
|
||||
? `[${queryRunner.getReplicationMode()}] `
|
||||
: undefined;
|
||||
sqlLogger.info(this.transformQueryLog(query, { prefix }), this.transformParameters(parameters));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public logQueryError(error: string, query: string, parameters?: any[]) {
|
||||
sqlLogger.error(this.transformQueryLog(query), this.transformParameters(parameters));
|
||||
public logQueryError(error: string, query: string, parameters?: any[], queryRunner?: QueryRunner) {
|
||||
const prefix = (this.props.printReplicationMode && queryRunner)
|
||||
? `[${queryRunner.getReplicationMode()}] `
|
||||
: undefined;
|
||||
sqlLogger.error(this.transformQueryLog(query, { prefix }), this.transformParameters(parameters));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public logQuerySlow(time: number, query: string, parameters?: any[]) {
|
||||
sqlLogger.warn(this.transformQueryLog(query), this.transformParameters(parameters));
|
||||
public logQuerySlow(time: number, query: string, parameters?: any[], queryRunner?: QueryRunner) {
|
||||
const prefix = (this.props.printReplicationMode && queryRunner)
|
||||
? `[${queryRunner.getReplicationMode()}] `
|
||||
: undefined;
|
||||
sqlLogger.warn(this.transformQueryLog(query, { prefix }), this.transformParameters(parameters));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -298,6 +310,7 @@ export function createPostgresDataSource(config: Config) {
|
||||
? new MyCustomLogger({
|
||||
disableQueryTruncation: config.logging?.sql?.disableQueryTruncation,
|
||||
enableQueryParamLogging: config.logging?.sql?.enableQueryParamLogging,
|
||||
printReplicationMode: !!config.dbReplications,
|
||||
})
|
||||
: undefined,
|
||||
maxQueryExecutionTime: 300,
|
||||
|
||||
@@ -44,7 +44,7 @@ import { BakeBufferedReactionsProcessorService } from './processors/BakeBuffered
|
||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||
import { QUEUE, baseQueueOptions } from './const.js';
|
||||
import { QUEUE, baseWorkerOptions } from './const.js';
|
||||
|
||||
// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
|
||||
function httpRelatedBackoff(attemptsMade: number) {
|
||||
@@ -175,7 +175,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
return processer(job);
|
||||
}
|
||||
}, {
|
||||
...baseQueueOptions(this.config, QUEUE.SYSTEM),
|
||||
...baseWorkerOptions(this.config, QUEUE.SYSTEM),
|
||||
autorun: false,
|
||||
});
|
||||
|
||||
@@ -232,7 +232,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
return processer(job);
|
||||
}
|
||||
}, {
|
||||
...baseQueueOptions(this.config, QUEUE.DB),
|
||||
...baseWorkerOptions(this.config, QUEUE.DB),
|
||||
autorun: false,
|
||||
});
|
||||
|
||||
@@ -264,7 +264,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
return this.deliverProcessorService.process(job);
|
||||
}
|
||||
}, {
|
||||
...baseQueueOptions(this.config, QUEUE.DELIVER),
|
||||
...baseWorkerOptions(this.config, QUEUE.DELIVER),
|
||||
autorun: false,
|
||||
concurrency: this.config.deliverJobConcurrency ?? 128,
|
||||
limiter: {
|
||||
@@ -304,7 +304,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
return this.inboxProcessorService.process(job);
|
||||
}
|
||||
}, {
|
||||
...baseQueueOptions(this.config, QUEUE.INBOX),
|
||||
...baseWorkerOptions(this.config, QUEUE.INBOX),
|
||||
autorun: false,
|
||||
concurrency: this.config.inboxJobConcurrency ?? 16,
|
||||
limiter: {
|
||||
@@ -344,7 +344,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
return this.userWebhookDeliverProcessorService.process(job);
|
||||
}
|
||||
}, {
|
||||
...baseQueueOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER),
|
||||
...baseWorkerOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER),
|
||||
autorun: false,
|
||||
concurrency: 64,
|
||||
limiter: {
|
||||
@@ -384,7 +384,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
return this.systemWebhookDeliverProcessorService.process(job);
|
||||
}
|
||||
}, {
|
||||
...baseQueueOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER),
|
||||
...baseWorkerOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER),
|
||||
autorun: false,
|
||||
concurrency: 16,
|
||||
limiter: {
|
||||
@@ -434,7 +434,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
return processer(job);
|
||||
}
|
||||
}, {
|
||||
...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
|
||||
...baseWorkerOptions(this.config, QUEUE.RELATIONSHIP),
|
||||
autorun: false,
|
||||
concurrency: this.config.relationshipJobConcurrency ?? 16,
|
||||
limiter: {
|
||||
@@ -479,7 +479,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
return processer(job);
|
||||
}
|
||||
}, {
|
||||
...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE),
|
||||
...baseWorkerOptions(this.config, QUEUE.OBJECT_STORAGE),
|
||||
autorun: false,
|
||||
concurrency: 16,
|
||||
});
|
||||
@@ -512,7 +512,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
return this.endedPollNotificationProcessorService.process(job);
|
||||
}
|
||||
}, {
|
||||
...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
|
||||
...baseWorkerOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
|
||||
autorun: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { MetricsTime } from 'bullmq';
|
||||
import { Config } from '@/config.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
@@ -27,3 +28,12 @@ export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof t
|
||||
prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue:${queueName}` : `queue:${queueName}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function baseWorkerOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.WorkerOptions {
|
||||
return {
|
||||
...baseQueueOptions(config, queueName),
|
||||
metrics: {
|
||||
maxDataPoints: MetricsTime.ONE_WEEK,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
|
||||
import type { FindOptionsWhere } from 'typeorm';
|
||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
||||
|
||||
const ACTIVITY_JSON = 'application/activity+json; charset=utf-8';
|
||||
const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8';
|
||||
@@ -75,6 +76,7 @@ export class ActivityPubServerService {
|
||||
private queueService: QueueService,
|
||||
private userKeypairService: UserKeypairService,
|
||||
private queryService: QueryService,
|
||||
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
|
||||
) {
|
||||
//this.createServer = this.createServer.bind(this);
|
||||
}
|
||||
@@ -461,16 +463,28 @@ export class ActivityPubServerService {
|
||||
const partOf = `${this.config.url}/users/${userId}/outbox`;
|
||||
|
||||
if (page) {
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId)
|
||||
.andWhere('note.userId = :userId', { userId: user.id })
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.visibility = \'public\'')
|
||||
.orWhere('note.visibility = \'home\'');
|
||||
}))
|
||||
.andWhere('note.localOnly = FALSE');
|
||||
|
||||
const notes = await query.limit(limit).getMany();
|
||||
const notes = this.meta.enableFanoutTimeline ? await this.fanoutTimelineEndpointService.getMiNotes({
|
||||
sinceId: sinceId ?? null,
|
||||
untilId: untilId ?? null,
|
||||
limit: limit,
|
||||
allowPartial: false, // Possibly true? IDK it's OK for ordered collection.
|
||||
me: null,
|
||||
redisTimelines: [
|
||||
`userTimeline:${user.id}`,
|
||||
`userTimelineWithReplies:${user.id}`,
|
||||
],
|
||||
useDbFallback: true,
|
||||
ignoreAuthorFromMute: true,
|
||||
excludePureRenotes: false,
|
||||
noteFilter: (note) => {
|
||||
if (note.visibility !== 'home' && note.visibility !== 'public') return false;
|
||||
if (note.localOnly) return false;
|
||||
return true;
|
||||
},
|
||||
dbFallback: async (untilId, sinceId, limit) => {
|
||||
return await this.getUserNotesFromDb(sinceId, untilId, limit, user.id);
|
||||
},
|
||||
}) : await this.getUserNotesFromDb(sinceId ?? null, untilId ?? null, limit, user.id);
|
||||
|
||||
if (sinceId) notes.reverse();
|
||||
|
||||
@@ -508,6 +522,20 @@ export class ActivityPubServerService {
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async getUserNotesFromDb(untilId: string | null, sinceId: string | null, limit: number, userId: MiUser['id']) {
|
||||
return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId)
|
||||
.andWhere('note.userId = :userId', { userId })
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.visibility = \'public\'')
|
||||
.orWhere('note.visibility = \'home\'');
|
||||
}))
|
||||
.andWhere('note.localOnly = FALSE')
|
||||
.limit(limit)
|
||||
.getMany();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async userInfo(request: FastifyRequest, reply: FastifyReply, user: MiUser | null) {
|
||||
if (this.meta.federation === 'none') {
|
||||
@@ -735,7 +763,7 @@ export class ActivityPubServerService {
|
||||
const acct = Acct.parse(request.params.acct);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
usernameLower: acct.username,
|
||||
usernameLower: acct.username.toLowerCase(),
|
||||
host: acct.host ?? IsNull(),
|
||||
isSuspended: false,
|
||||
});
|
||||
|
||||
@@ -221,7 +221,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||
reply.header('Cache-Control', 'public, max-age=86400');
|
||||
|
||||
if (user) {
|
||||
reply.redirect(user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user));
|
||||
reply.redirect((user.avatarId == null ? null : user.avatarUrl) ?? this.userEntityService.getIdenticonUrl(user));
|
||||
} else {
|
||||
reply.redirect('/static-assets/user-unknown.png');
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => {
|
||||
|
||||
const fromAcct = (acct: Acct.Acct): FindOptionsWhere<MiUser> | number =>
|
||||
!acct.host || acct.host === this.config.host.toLowerCase() ? {
|
||||
usernameLower: acct.username,
|
||||
usernameLower: acct.username.toLowerCase(),
|
||||
host: IsNull(),
|
||||
isSuspended: false,
|
||||
} : 422;
|
||||
|
||||
@@ -67,8 +67,14 @@ export * as 'admin/promo/create' from './endpoints/admin/promo/create.js';
|
||||
export * as 'admin/queue/clear' from './endpoints/admin/queue/clear.js';
|
||||
export * as 'admin/queue/deliver-delayed' from './endpoints/admin/queue/deliver-delayed.js';
|
||||
export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-delayed.js';
|
||||
export * as 'admin/queue/promote' from './endpoints/admin/queue/promote.js';
|
||||
export * as 'admin/queue/retry-job' from './endpoints/admin/queue/retry-job.js';
|
||||
export * as 'admin/queue/remove-job' from './endpoints/admin/queue/remove-job.js';
|
||||
export * as 'admin/queue/show-job' from './endpoints/admin/queue/show-job.js';
|
||||
export * as 'admin/queue/promote-jobs' from './endpoints/admin/queue/promote-jobs.js';
|
||||
export * as 'admin/queue/jobs' from './endpoints/admin/queue/jobs.js';
|
||||
export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js';
|
||||
export * as 'admin/queue/queues' from './endpoints/admin/queue/queues.js';
|
||||
export * as 'admin/queue/queue-stats' from './endpoints/admin/queue/queue-stats.js';
|
||||
export * as 'admin/relays/add' from './endpoints/admin/relays/add.js';
|
||||
export * as 'admin/relays/list' from './endpoints/admin/relays/list.js';
|
||||
export * as 'admin/relays/remove' from './endpoints/admin/relays/remove.js';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -18,8 +18,11 @@ export const meta = {
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
properties: {
|
||||
queue: { type: 'string', enum: QUEUE_TYPES },
|
||||
state: { type: 'string', enum: ['*', 'completed', 'wait', 'active', 'paused', 'prioritized', 'delayed', 'failed'] },
|
||||
},
|
||||
required: ['queue', 'state'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
@@ -29,7 +32,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
this.queueService.destroy();
|
||||
this.queueService.queueClear(ps.queue, ps.state);
|
||||
|
||||
this.moderationLogService.log(me, 'clearQueue');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'read:admin:queue',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
queue: { type: 'string', enum: QUEUE_TYPES },
|
||||
state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed'] } },
|
||||
search: { type: 'string' },
|
||||
},
|
||||
required: ['queue', 'state'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
return this.queueService.queueGetJobs(ps.queue, ps.state, ps.search);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:queue',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
queue: { type: 'string', enum: QUEUE_TYPES },
|
||||
},
|
||||
required: ['queue'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
this.queueService.queuePromoteJobs(ps.queue);
|
||||
|
||||
this.moderationLogService.log(me, 'promoteQueue');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:queue',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { type: 'string', enum: ['deliver', 'inbox'] },
|
||||
},
|
||||
required: ['type'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
let delayedQueues;
|
||||
|
||||
switch (ps.type) {
|
||||
case 'deliver':
|
||||
delayedQueues = await this.queueService.deliverQueue.getDelayed();
|
||||
for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) {
|
||||
const queue = delayedQueues[queueIndex];
|
||||
try {
|
||||
await queue.promote();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.message.indexOf('not in a delayed state') !== -1) {
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'inbox':
|
||||
delayedQueues = await this.queueService.inboxQueue.getDelayed();
|
||||
for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) {
|
||||
const queue = delayedQueues[queueIndex];
|
||||
try {
|
||||
await queue.promote();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.message.indexOf('not in a delayed state') !== -1) {
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.moderationLogService.log(me, 'promoteQueue');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'read:admin:queue',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
queue: { type: 'string', enum: QUEUE_TYPES },
|
||||
},
|
||||
required: ['queue'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
return this.queueService.queueGetQueue(ps.queue);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'read:admin:queue',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
return this.queueService.queueGetQueues();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:queue',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
queue: { type: 'string', enum: QUEUE_TYPES },
|
||||
jobId: { type: 'string' },
|
||||
},
|
||||
required: ['queue', 'jobId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
this.queueService.queueRemoveJob(ps.queue, ps.jobId);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:queue',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
queue: { type: 'string', enum: QUEUE_TYPES },
|
||||
jobId: { type: 'string' },
|
||||
},
|
||||
required: ['queue', 'jobId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
this.queueService.queueRetryJob(ps.queue, ps.jobId);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'read:admin:queue',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
queue: { type: 'string', enum: QUEUE_TYPES },
|
||||
jobId: { type: 'string' },
|
||||
},
|
||||
required: ['queue', 'jobId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
return this.queueService.queueGetJob(ps.queue, ps.jobId);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ export const paramDef = {
|
||||
excludeBots: { type: 'boolean' },
|
||||
withReplies: { type: 'boolean' },
|
||||
withFile: { type: 'boolean' },
|
||||
hideNotesInSensitiveChannel: { type: 'boolean' },
|
||||
excludeNotesInSensitiveChannel: { type: 'boolean' },
|
||||
},
|
||||
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
|
||||
} as const;
|
||||
@@ -134,7 +134,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
excludeBots: ps.excludeBots,
|
||||
withReplies: ps.withReplies,
|
||||
withFile: ps.withFile,
|
||||
hideNotesInSensitiveChannel: ps.hideNotesInSensitiveChannel,
|
||||
excludeNotesInSensitiveChannel: ps.excludeNotesInSensitiveChannel,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('antennaCreated', antenna);
|
||||
|
||||
@@ -72,7 +72,7 @@ export const paramDef = {
|
||||
excludeBots: { type: 'boolean' },
|
||||
withReplies: { type: 'boolean' },
|
||||
withFile: { type: 'boolean' },
|
||||
hideNotesInSensitiveChannel: { type: 'boolean' },
|
||||
excludeNotesInSensitiveChannel: { type: 'boolean' },
|
||||
},
|
||||
required: ['antennaId'],
|
||||
} as const;
|
||||
@@ -130,7 +130,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
excludeBots: ps.excludeBots,
|
||||
withReplies: ps.withReplies,
|
||||
withFile: ps.withFile,
|
||||
hideNotesInSensitiveChannel: ps.hideNotesInSensitiveChannel,
|
||||
excludeNotesInSensitiveChannel: ps.excludeNotesInSensitiveChannel,
|
||||
isActive: true,
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
@@ -46,6 +46,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'read');
|
||||
|
||||
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);
|
||||
|
||||
@@ -16,7 +16,6 @@ export const meta = {
|
||||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canChat',
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
@@ -74,6 +73,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'write');
|
||||
|
||||
const room = await this.chatService.findRoomById(ps.toRoomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
|
||||
@@ -16,7 +16,6 @@ export const meta = {
|
||||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canChat',
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
@@ -86,6 +85,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'write');
|
||||
|
||||
let file = null;
|
||||
if (ps.fileId != null) {
|
||||
file = await this.driveFilesRepository.findOneBy({
|
||||
|
||||
@@ -13,7 +13,6 @@ export const meta = {
|
||||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canChat',
|
||||
|
||||
kind: 'write:chat',
|
||||
|
||||
@@ -43,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'write');
|
||||
|
||||
const message = await this.chatService.findMyMessageById(me.id, ps.messageId);
|
||||
if (message == null) {
|
||||
throw new ApiError(meta.errors.noSuchMessage);
|
||||
|
||||
@@ -13,7 +13,6 @@ export const meta = {
|
||||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canChat',
|
||||
|
||||
kind: 'write:chat',
|
||||
|
||||
@@ -44,6 +43,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'write');
|
||||
|
||||
await this.chatService.react(ps.messageId, me.id, ps.reaction);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'read');
|
||||
|
||||
const room = await this.chatService.findRoomById(ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
|
||||
@@ -54,6 +54,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'read');
|
||||
|
||||
if (ps.roomId != null) {
|
||||
const room = await this.chatService.findRoomById(ps.roomId);
|
||||
if (room == null) {
|
||||
|
||||
@@ -50,6 +50,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatEntityService: ChatEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'read');
|
||||
|
||||
const message = await this.chatService.findMessageById(ps.messageId);
|
||||
if (message == null) {
|
||||
throw new ApiError(meta.errors.noSuchMessage);
|
||||
|
||||
@@ -13,7 +13,6 @@ export const meta = {
|
||||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canChat',
|
||||
|
||||
kind: 'write:chat',
|
||||
|
||||
@@ -44,6 +43,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'write');
|
||||
|
||||
await this.chatService.unreact(ps.messageId, me.id, ps.reaction);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private getterService: GetterService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'read');
|
||||
|
||||
const other = await this.getterService.getUser(ps.userId).catch(err => {
|
||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw err;
|
||||
|
||||
@@ -15,7 +15,6 @@ export const meta = {
|
||||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canChat',
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
@@ -52,6 +51,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatEntityService: ChatEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'write');
|
||||
|
||||
const room = await this.chatService.createRoom(me, {
|
||||
name: ps.name,
|
||||
description: ps.description ?? '',
|
||||
|
||||
@@ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'write');
|
||||
|
||||
const room = await this.chatService.findRoomById(ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
|
||||
@@ -15,7 +15,6 @@ export const meta = {
|
||||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canChat',
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
@@ -57,6 +56,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatEntityService: ChatEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'write');
|
||||
|
||||
const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
|
||||
@@ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'write');
|
||||
|
||||
await this.chatService.ignoreRoomInvitation(me.id, ps.roomId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'read');
|
||||
|
||||
const invitations = await this.chatService.getReceivedRoomInvitationsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId);
|
||||
return this.chatEntityService.packRoomInvitations(invitations, me);
|
||||
});
|
||||
|
||||
@@ -55,6 +55,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatEntityService: ChatEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'read');
|
||||
|
||||
const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
|
||||
@@ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'write');
|
||||
|
||||
await this.chatService.joinToRoom(me.id, ps.roomId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatEntityService: ChatEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'read');
|
||||
|
||||
const memberships = await this.chatService.getMyMemberships(me.id, ps.limit, ps.sinceId, ps.untilId);
|
||||
|
||||
return this.chatEntityService.packRoomMemberships(memberships, me, {
|
||||
|
||||
@@ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'write');
|
||||
|
||||
await this.chatService.leaveRoom(me.id, ps.roomId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatEntityService: ChatEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'read');
|
||||
|
||||
const room = await this.chatService.findRoomById(ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
|
||||
@@ -43,6 +43,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'write');
|
||||
|
||||
await this.chatService.muteRoom(me.id, ps.roomId, ps.mute);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'read');
|
||||
|
||||
const rooms = await this.chatService.getOwnedRoomsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId);
|
||||
return this.chatEntityService.packRooms(rooms, me);
|
||||
});
|
||||
|
||||
@@ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatEntityService: ChatEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'read');
|
||||
|
||||
const room = await this.chatService.findRoomById(ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
|
||||
@@ -49,6 +49,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private chatEntityService: ChatEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.checkChatAvailability(me.id, 'write');
|
||||
|
||||
const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
|
||||
@@ -534,7 +534,7 @@ export class ClientServerService {
|
||||
|
||||
return await reply.view('user', {
|
||||
user, profile, me,
|
||||
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
|
||||
avatarUrl: _user.avatarUrl,
|
||||
sub: request.params.sub,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
clientCtx: htmlSafeJsonStringify({
|
||||
|
||||
@@ -65,7 +65,7 @@ export class FeedService {
|
||||
generator: 'Misskey',
|
||||
description: `${user.notesCount} Notes, ${profile.followingVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.followersVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
|
||||
link: author.link,
|
||||
image: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
|
||||
image: (user.avatarId == null ? null : user.avatarUrl) ?? this.userEntityService.getIdenticonUrl(user),
|
||||
feedLinks: {
|
||||
json: `${author.link}.json`,
|
||||
atom: `${author.link}.atom`,
|
||||
|
||||
@@ -381,7 +381,8 @@ describe('User', () => {
|
||||
|
||||
await alice.client.request('i/delete-account', { password: alice.password });
|
||||
// NOTE: user deletion query is slow
|
||||
await sleep(4000);
|
||||
// FIXME: ensure user is removed successfully
|
||||
await sleep(10000);
|
||||
|
||||
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||
strictEqual(following.length, 0); // no following relation
|
||||
@@ -480,7 +481,8 @@ describe('User', () => {
|
||||
|
||||
await aAdmin.client.request('admin/suspend-user', { userId: alice.id });
|
||||
// NOTE: user deletion query is slow
|
||||
await sleep(4000);
|
||||
// FIXME: ensure user is removed successfully
|
||||
await sleep(10000);
|
||||
|
||||
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||
strictEqual(following.length, 0); // no following relation
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||
import {
|
||||
api,
|
||||
failedApiCall,
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
userList,
|
||||
} from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||
|
||||
const compareBy = <T extends { id: string }>(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => {
|
||||
return selector(a).localeCompare(selector(b));
|
||||
@@ -146,7 +146,7 @@ describe('アンテナ', () => {
|
||||
caseSensitive: false,
|
||||
createdAt: new Date(response.createdAt).toISOString(),
|
||||
excludeKeywords: [['']],
|
||||
hideNotesInSensitiveChannel: false,
|
||||
excludeNotesInSensitiveChannel: false,
|
||||
hasUnreadNote: false,
|
||||
isActive: true,
|
||||
keywords: [['keyword']],
|
||||
@@ -218,8 +218,8 @@ describe('アンテナ', () => {
|
||||
{ parameters: () => ({ withReplies: true }) },
|
||||
{ parameters: () => ({ withFile: false }) },
|
||||
{ parameters: () => ({ withFile: true }) },
|
||||
{ parameters: () => ({ hideNotesInSensitiveChannel: false }) },
|
||||
{ parameters: () => ({ hideNotesInSensitiveChannel: true }) },
|
||||
{ parameters: () => ({ excludeNotesInSensitiveChannel: false }) },
|
||||
{ parameters: () => ({ excludeNotesInSensitiveChannel: true }) },
|
||||
];
|
||||
test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
|
||||
const response = await successfulApiCall({
|
||||
@@ -235,12 +235,12 @@ describe('アンテナ', () => {
|
||||
await failedApiCall({
|
||||
endpoint: 'antennas/create',
|
||||
parameters: { ...defaultParam, keywords: [[]], excludeKeywords: [[]] },
|
||||
user: alice
|
||||
user: alice,
|
||||
}, {
|
||||
status: 400,
|
||||
code: 'EMPTY_KEYWORD',
|
||||
id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a'
|
||||
})
|
||||
id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a',
|
||||
});
|
||||
});
|
||||
//#endregion
|
||||
//#region 更新(antennas/update)
|
||||
@@ -274,12 +274,12 @@ describe('アンテナ', () => {
|
||||
await failedApiCall({
|
||||
endpoint: 'antennas/update',
|
||||
parameters: { ...defaultParam, antennaId: antenna.id, keywords: [[]], excludeKeywords: [[]] },
|
||||
user: alice
|
||||
user: alice,
|
||||
}, {
|
||||
status: 400,
|
||||
code: 'EMPTY_KEYWORD',
|
||||
id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4'
|
||||
})
|
||||
id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4',
|
||||
});
|
||||
});
|
||||
|
||||
//#endregion
|
||||
@@ -375,14 +375,23 @@ describe('アンテナ', () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
// https://github.com/misskey-dev/misskey/issues/9025
|
||||
label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。',
|
||||
label: 'フォロワー限定投稿とDM投稿を含む',
|
||||
parameters: () => ({}),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true },
|
||||
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true },
|
||||
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'followers' }) },
|
||||
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'specified', visibleUserIds: [alice.id] }) },
|
||||
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'followers' }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, visibility: 'specified', visibleUserIds: [alice.id] }), included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'フォロワー限定投稿とDM投稿を含まない',
|
||||
parameters: () => ({}),
|
||||
posts: [
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, visibility: 'public' }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, visibility: 'home' }), included: true },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, visibility: 'followers' }) },
|
||||
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, visibility: 'specified', visibleUserIds: [carol.id] }) },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -633,7 +642,7 @@ describe('アンテナ', () => {
|
||||
const keyword = 'キーワード';
|
||||
const antenna = await successfulApiCall({
|
||||
endpoint: 'antennas/create',
|
||||
parameters: { ...defaultParam, keywords: [[keyword]], hideNotesInSensitiveChannel: true },
|
||||
parameters: { ...defaultParam, keywords: [[keyword]], excludeNotesInSensitiveChannel: true },
|
||||
user: alice,
|
||||
});
|
||||
const nonSensitiveChannel = await successfulApiCall({
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('AnnouncementService', () => {
|
||||
return usersRepository.insert({
|
||||
id: genAidx(Date.now()),
|
||||
username: un,
|
||||
usernameLower: un,
|
||||
usernameLower: un.toLowerCase(),
|
||||
...data,
|
||||
})
|
||||
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
@@ -89,8 +89,8 @@ describe('SigninWithPasskeyApiService', () => {
|
||||
app = await Test.createTestingModule({
|
||||
imports: [GlobalModule, CoreModule],
|
||||
providers: [
|
||||
SigninWithPasskeyApiService,
|
||||
{ provide: RateLimiterService, useClass: FakeLimiter },
|
||||
SigninWithPasskeyApiService,
|
||||
{ provide: RateLimiterService, useClass: FakeLimiter },
|
||||
{ provide: SigninService, useClass: FakeSigninService },
|
||||
],
|
||||
}).useMocker((token) => {
|
||||
@@ -115,7 +115,7 @@ describe('SigninWithPasskeyApiService', () => {
|
||||
jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication').mockImplementation(FakeWebauthnVerify);
|
||||
|
||||
const dummyUser = {
|
||||
id: uid, username: uid, usernameLower: uid.toLocaleLowerCase(), uri: null, host: null,
|
||||
id: uid, username: uid, usernameLower: uid.toLowerCase(), uri: null, host: null,
|
||||
};
|
||||
const dummyProfile = {
|
||||
userId: uid,
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('UserEntityService', () => {
|
||||
...userData,
|
||||
id: genAidx(Date.now()),
|
||||
username: un,
|
||||
usernameLower: un,
|
||||
usernameLower: un.toLowerCase(),
|
||||
})
|
||||
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
|
||||
@@ -21,34 +21,34 @@
|
||||
"astring": "1.9.0",
|
||||
"buraha": "0.0.1",
|
||||
"estree-walker": "3.0.3",
|
||||
"frontend-shared": "workspace:*",
|
||||
"json5": "2.2.3",
|
||||
"mfm-js": "0.24.0",
|
||||
"misskey-js": "workspace:*",
|
||||
"frontend-shared": "workspace:*",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.36.0",
|
||||
"sass": "1.86.0",
|
||||
"shiki": "3.2.1",
|
||||
"rollup": "4.39.0",
|
||||
"sass": "1.86.3",
|
||||
"shiki": "3.2.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.11",
|
||||
"tsc-alias": "1.8.15",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.8.2",
|
||||
"typescript": "5.8.3",
|
||||
"uuid": "11.1.0",
|
||||
"json5": "2.2.3",
|
||||
"vite": "6.2.4",
|
||||
"vite": "6.3.1",
|
||||
"vue": "3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/summaly": "5.2.0",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/estree": "1.0.6",
|
||||
"@types/estree": "1.0.7",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "22.13.11",
|
||||
"@types/node": "22.14.0",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.27.0",
|
||||
"@typescript-eslint/parser": "8.27.0",
|
||||
"@vitest/coverage-v8": "3.0.9",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.29.1",
|
||||
"@typescript-eslint/parser": "8.29.1",
|
||||
"@vitest/coverage-v8": "3.1.1",
|
||||
"@vue/runtime-core": "3.5.13",
|
||||
"acorn": "8.14.1",
|
||||
"cross-env": "7.0.3",
|
||||
@@ -64,7 +64,7 @@
|
||||
"start-server-and-test": "2.0.11",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vue-component-type-helpers": "2.2.8",
|
||||
"vue-eslint-parser": "10.1.1",
|
||||
"vue-eslint-parser": "10.1.3",
|
||||
"vue-tsc": "2.2.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export const ROLE_POLICIES = [
|
||||
'canImportFollowing',
|
||||
'canImportMuting',
|
||||
'canImportUserLists',
|
||||
'canChat',
|
||||
'chatAvailability',
|
||||
] as const;
|
||||
|
||||
export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error.jpg';
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.13.11",
|
||||
"@typescript-eslint/eslint-plugin": "8.27.0",
|
||||
"@typescript-eslint/parser": "8.27.0",
|
||||
"esbuild": "0.25.1",
|
||||
"@types/node": "22.14.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.29.1",
|
||||
"@typescript-eslint/parser": "8.29.1",
|
||||
"esbuild": "0.25.2",
|
||||
"eslint-plugin-vue": "10.0.0",
|
||||
"nodemon": "3.1.9",
|
||||
"typescript": "5.8.2",
|
||||
"vue-eslint-parser": "10.1.1"
|
||||
"typescript": "5.8.3",
|
||||
"vue-eslint-parser": "10.1.3"
|
||||
},
|
||||
"files": [
|
||||
"js-built"
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
navFg: '@fg',
|
||||
navActive: '@accent',
|
||||
navIndicator: '@indicator',
|
||||
pageHeaderBg: '@bg',
|
||||
pageHeaderFg: '@fg',
|
||||
link: '#44a4c1',
|
||||
hashtag: '#ff9156',
|
||||
mention: '@accent',
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
navFg: '@fg',
|
||||
navActive: '@accent',
|
||||
navIndicator: '@indicator',
|
||||
pageHeaderBg: '@bg',
|
||||
pageHeaderFg: '@fg',
|
||||
link: '#44a4c1',
|
||||
hashtag: '#ff9156',
|
||||
mention: '@accent',
|
||||
|
||||
@@ -43,6 +43,41 @@ export function channel(id = 'somechannelid', name = 'Some Channel', bannerUrl:
|
||||
};
|
||||
}
|
||||
|
||||
export function chatMessage(room = false, id = 'somechatmessageid', text = 'Hello!'): entities.ChatMessage {
|
||||
const fromUser = userLite();
|
||||
const toRoom = chatRoom();
|
||||
const toUser = userLite('touserid');
|
||||
return {
|
||||
id,
|
||||
createdAt: '2016-12-28T22:49:51.000Z',
|
||||
fromUserId: fromUser.id,
|
||||
fromUser,
|
||||
text,
|
||||
isRead: false,
|
||||
reactions: [],
|
||||
...room ? {
|
||||
toRoomId: toRoom.id,
|
||||
toRoom,
|
||||
} : {
|
||||
toUserId: toUser.id,
|
||||
toUser,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function chatRoom(id = 'somechatroomid', name = 'Some Chat Room'): entities.ChatRoom {
|
||||
const owner = userLite('someownerid');
|
||||
return {
|
||||
id,
|
||||
createdAt: '2016-12-28T22:49:51.000Z',
|
||||
ownerId: owner.id,
|
||||
owner,
|
||||
name,
|
||||
description: 'A chat room for testing',
|
||||
isMuted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function clip(id = 'someclipid', name = 'Some Clip'): entities.Clip {
|
||||
return {
|
||||
id,
|
||||
|
||||
@@ -173,21 +173,21 @@ function customStringify(obj: unknown): string {
|
||||
/**
|
||||
* 要素のノードの中身のテキストを抽出する
|
||||
*/
|
||||
function extractElementText(node: ElementNode): string | null {
|
||||
return extractElementTextChecked(node, node.tag);
|
||||
function extractElementText(node: ElementNode, id: string): string | null {
|
||||
return extractElementTextChecked(node, node.tag, id);
|
||||
}
|
||||
|
||||
function extractElementTextChecked(node: ElementNode, processingNodeName: string): string | null {
|
||||
function extractElementTextChecked(node: ElementNode, processingNodeName: string, id: string): string | null {
|
||||
const result: string[] = [];
|
||||
for (const child of node.children) {
|
||||
const text = extractElementText2Inner(child, processingNodeName);
|
||||
const text = extractElementText2Inner(child, processingNodeName, id);
|
||||
if (text == null) return null;
|
||||
result.push(text);
|
||||
}
|
||||
return result.join('');
|
||||
}
|
||||
|
||||
function extractElementText2Inner(node: TemplateChildNode, processingNodeName: string): string | null {
|
||||
function extractElementText2Inner(node: TemplateChildNode, processingNodeName: string, id: string): string | null {
|
||||
if (node.type === NodeTypes.COMPOUND_EXPRESSION) throw new Error("Unexpected COMPOUND_EXPRESSION");
|
||||
|
||||
switch (node.type) {
|
||||
@@ -196,16 +196,16 @@ function extractElementText2Inner(node: TemplateChildNode, processingNodeName: s
|
||||
if (expr.type === NodeTypes.COMPOUND_EXPRESSION) throw new Error(`Unexpected COMPOUND_EXPRESSION`);
|
||||
const exprResult = evalExpression(expr.content);
|
||||
if (typeof exprResult !== 'string') {
|
||||
logger.error(`Result of interpolation node is not string at line ${node.loc.start.line}`);
|
||||
logger.error(`Result of interpolation node is not string at line ${id}:${node.loc.start.line}`);
|
||||
return null;
|
||||
}
|
||||
return exprResult;
|
||||
}
|
||||
case NodeTypes.ELEMENT:
|
||||
if (node.tagType === ElementTypes.ELEMENT) {
|
||||
return extractElementTextChecked(node, processingNodeName);
|
||||
return extractElementTextChecked(node, processingNodeName, id);
|
||||
} else {
|
||||
logger.error(`Unexpected ${node.tag} extracting text of ${processingNodeName} ${node.loc.start.line}`);
|
||||
logger.error(`Unexpected ${node.tag} extracting text of ${processingNodeName} ${id}:${node.loc.start.line}`);
|
||||
return null;
|
||||
}
|
||||
case NodeTypes.TEXT:
|
||||
@@ -217,7 +217,7 @@ function extractElementText2Inner(node: TemplateChildNode, processingNodeName: s
|
||||
case NodeTypes.IF_BRANCH:
|
||||
case NodeTypes.FOR:
|
||||
case NodeTypes.TEXT_CALL:
|
||||
logger.error(`Unexpected controlflow element extracting text of ${processingNodeName} ${node.loc.start.line}`);
|
||||
logger.error(`Unexpected controlflow element extracting text of ${processingNodeName} ${id}:${node.loc.start.line}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -229,7 +229,7 @@ function extractElementText2Inner(node: TemplateChildNode, processingNodeName: s
|
||||
/**
|
||||
* SearchLabel/SearchKeyword/SearchIconを探して抽出する関数
|
||||
*/
|
||||
function extractSugarTags(nodes: TemplateChildNode[]): { label: string | null, keywords: string[], icon: string | null } {
|
||||
function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: string | null, keywords: string[], icon: string | null } {
|
||||
let label: string | null | undefined = undefined;
|
||||
let icon: string | null | undefined = undefined;
|
||||
const keywords: string[] = [];
|
||||
@@ -242,35 +242,35 @@ function extractSugarTags(nodes: TemplateChildNode[]): { label: string | null, k
|
||||
return false; // SearchMarkerはスキップ
|
||||
case 'SearchLabel':
|
||||
if (label !== undefined) {
|
||||
logger.warn(`Duplicate SearchLabel found, ignoring the second one at ${node.loc.start.line}`);
|
||||
logger.warn(`Duplicate SearchLabel found, ignoring the second one at ${id}:${node.loc.start.line}`);
|
||||
break; // 2つ目のSearchLabelは無視
|
||||
}
|
||||
|
||||
label = extractElementText(node);
|
||||
label = extractElementText(node, id);
|
||||
return;
|
||||
case 'SearchKeyword':
|
||||
const content = extractElementText(node);
|
||||
const content = extractElementText(node, id);
|
||||
if (content) {
|
||||
keywords.push(content);
|
||||
}
|
||||
return;
|
||||
case 'SearchIcon':
|
||||
if (icon !== undefined) {
|
||||
logger.warn(`Duplicate SearchIcon found, ignoring the second one at ${node.loc.start.line}`);
|
||||
logger.warn(`Duplicate SearchIcon found, ignoring the second one at ${id}:${node.loc.start.line}`);
|
||||
break; // 2つ目のSearchIconは無視
|
||||
}
|
||||
|
||||
if (node.children.length !== 1) {
|
||||
logger.error(`SearchIcon must have exactly one child at ${node.loc.start.line}`);
|
||||
logger.error(`SearchIcon must have exactly one child at ${id}:${node.loc.start.line}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const iconNode = node.children[0];
|
||||
if (iconNode.type !== NodeTypes.ELEMENT) {
|
||||
logger.error(`SearchIcon must have a child element at ${node.loc.start.line}`);
|
||||
logger.error(`SearchIcon must have a child element at ${id}:${node.loc.start.line}`);
|
||||
return;
|
||||
}
|
||||
icon = getStringProp(findAttribute(iconNode.props, 'class'));
|
||||
icon = getStringProp(findAttribute(iconNode.props, 'class'), id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ function extractSugarTags(nodes: TemplateChildNode[]): { label: string | null, k
|
||||
return { label: label ?? null, keywords, icon: icon ?? null };
|
||||
}
|
||||
|
||||
function getStringProp(attr: AttributeNode | DirectiveNode | null): string | null {
|
||||
function getStringProp(attr: AttributeNode | DirectiveNode | null, id: string): string | null {
|
||||
switch (attr?.type) {
|
||||
case null:
|
||||
case undefined:
|
||||
@@ -294,27 +294,27 @@ function getStringProp(attr: AttributeNode | DirectiveNode | null): string | nul
|
||||
if (attr.exp.type === NodeTypes.COMPOUND_EXPRESSION) throw new Error('Unexpected COMPOUND_EXPRESSION');
|
||||
const value = evalExpression(attr.exp.content ?? '');
|
||||
if (typeof value !== 'string') {
|
||||
logger.error(`Expected string value, got ${typeof value} at line ${attr.loc.start.line}`);
|
||||
logger.error(`Expected string value, got ${typeof value} at ${id}:${attr.loc.start.line}`);
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function getStringArrayProp(attr: AttributeNode | DirectiveNode | null): string[] | null {
|
||||
function getStringArrayProp(attr: AttributeNode | DirectiveNode | null, id: string): string[] | null {
|
||||
switch (attr?.type) {
|
||||
case null:
|
||||
case undefined:
|
||||
return null;
|
||||
case NodeTypes.ATTRIBUTE:
|
||||
logger.error(`Expected directive, got attribute at line ${attr.loc.start.line}`);
|
||||
logger.error(`Expected directive, got attribute at ${id}:${attr.loc.start.line}`);
|
||||
return null;
|
||||
case NodeTypes.DIRECTIVE:
|
||||
if (attr.exp == null) return null;
|
||||
if (attr.exp.type === NodeTypes.COMPOUND_EXPRESSION) throw new Error('Unexpected COMPOUND_EXPRESSION');
|
||||
const value = evalExpression(attr.exp.content ?? '');
|
||||
if (!Array.isArray(value) || !value.every(x => typeof x === 'string')) {
|
||||
logger.error(`Expected string array value, got ${typeof value} at line ${attr.loc.start.line}`);
|
||||
logger.error(`Expected string array value, got ${typeof value} at ${id}:${attr.loc.start.line}`);
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
@@ -354,11 +354,11 @@ function extractUsageInfoFromTemplateAst(
|
||||
};
|
||||
|
||||
// バインドプロパティを取得
|
||||
const path = getStringProp(findAttribute(node.props, 'path'))
|
||||
const icon = getStringProp(findAttribute(node.props, 'icon'))
|
||||
const label = getStringProp(findAttribute(node.props, 'label'))
|
||||
const inlining = getStringArrayProp(findAttribute(node.props, 'inlining'))
|
||||
const keywords = getStringArrayProp(findAttribute(node.props, 'keywords'))
|
||||
const path = getStringProp(findAttribute(node.props, 'path'), id)
|
||||
const icon = getStringProp(findAttribute(node.props, 'icon'), id)
|
||||
const label = getStringProp(findAttribute(node.props, 'label'), id)
|
||||
const inlining = getStringArrayProp(findAttribute(node.props, 'inlining'), id)
|
||||
const keywords = getStringArrayProp(findAttribute(node.props, 'keywords'), id)
|
||||
|
||||
if (path) markerInfo.path = path;
|
||||
if (icon) markerInfo.icon = icon;
|
||||
@@ -373,7 +373,7 @@ function extractUsageInfoFromTemplateAst(
|
||||
|
||||
// SearchLabelとSearchKeywordを抽出 (AST全体を探索)
|
||||
{
|
||||
const extracted = extractSugarTags(node.children);
|
||||
const extracted = extractSugarTags(node.children, id);
|
||||
if (extracted.label && markerInfo.label) logger.warn(`Duplicate label found for ${markerId} at ${id}:${node.loc.start.line}`);
|
||||
if (extracted.icon && markerInfo.icon) logger.warn(`Duplicate icon found for ${markerId} at ${id}:${node.loc.start.line}`);
|
||||
markerInfo.label = extracted.label ?? markerInfo.label ?? '';
|
||||
@@ -585,7 +585,7 @@ export class MarkerIdAssigner {
|
||||
}
|
||||
|
||||
// AST で :children 属性が検出された場合、それを更新
|
||||
const childrenValue = getStringArrayProp(childrenProp);
|
||||
const childrenValue = getStringArrayProp(childrenProp, id);
|
||||
if (childrenValue == null) continue;
|
||||
|
||||
const newValue: string[] = [...childrenValue];
|
||||
@@ -740,7 +740,7 @@ export function pluginCreateSearchIndexVirtualModule(options: Options, asigner:
|
||||
this.addWatchFile(searchIndexFilePath);
|
||||
|
||||
const code = await asigner.getOrLoad(searchIndexFilePath);
|
||||
return generateJavaScriptCode(collectFileMarkers(id, code));
|
||||
return generateJavaScriptCode(collectFileMarkers(searchIndexFilePath, code));
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "6.0.2",
|
||||
"@rollup/pluginutils": "5.1.4",
|
||||
"@sentry/vue": "9.8.0",
|
||||
"@sentry/vue": "9.12.0",
|
||||
"@syuilo/aiscript": "0.19.0",
|
||||
"@tabler/icons-webfont": "3.31.0",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
@@ -33,7 +33,7 @@
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
|
||||
"analytics": "0.8.16",
|
||||
"astring": "1.9.0",
|
||||
"broadcast-channel": "7.0.0",
|
||||
"broadcast-channel": "7.1.0",
|
||||
"buraha": "0.0.1",
|
||||
"canvas-confetti": "1.9.3",
|
||||
"chart.js": "4.4.8",
|
||||
@@ -41,7 +41,7 @@
|
||||
"chartjs-chart-matrix": "2.1.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.2.0",
|
||||
"chromatic": "11.27.0",
|
||||
"chromatic": "11.28.0",
|
||||
"compare-versions": "6.1.1",
|
||||
"cropperjs": "2.0.0",
|
||||
"date-fns": "4.1.0",
|
||||
@@ -60,65 +60,65 @@
|
||||
"misskey-reversi": "workspace:*",
|
||||
"photoswipe": "5.4.4",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.36.0",
|
||||
"rollup": "4.39.0",
|
||||
"sanitize-html": "2.15.0",
|
||||
"sass": "1.86.0",
|
||||
"shiki": "3.2.1",
|
||||
"sass": "1.86.3",
|
||||
"shiki": "3.2.2",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.174.0",
|
||||
"three": "0.175.0",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.11",
|
||||
"tsc-alias": "1.8.15",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.8.2",
|
||||
"typescript": "5.8.3",
|
||||
"uuid": "11.1.0",
|
||||
"v-code-diff": "1.13.1",
|
||||
"vite": "6.2.4",
|
||||
"vite": "6.3.1",
|
||||
"vue": "3.5.13",
|
||||
"vuedraggable": "next",
|
||||
"wanakana": "5.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/summaly": "5.2.0",
|
||||
"@storybook/addon-actions": "8.6.7",
|
||||
"@storybook/addon-essentials": "8.6.7",
|
||||
"@storybook/addon-interactions": "8.6.7",
|
||||
"@storybook/addon-links": "8.6.7",
|
||||
"@storybook/addon-mdx-gfm": "8.6.7",
|
||||
"@storybook/addon-storysource": "8.6.7",
|
||||
"@storybook/blocks": "8.6.7",
|
||||
"@storybook/components": "8.6.7",
|
||||
"@storybook/core-events": "8.6.7",
|
||||
"@storybook/manager-api": "8.6.7",
|
||||
"@storybook/preview-api": "8.6.7",
|
||||
"@storybook/react": "8.6.7",
|
||||
"@storybook/react-vite": "8.6.7",
|
||||
"@storybook/test": "8.6.7",
|
||||
"@storybook/theming": "8.6.7",
|
||||
"@storybook/types": "8.6.7",
|
||||
"@storybook/vue3": "8.6.7",
|
||||
"@storybook/vue3-vite": "8.6.7",
|
||||
"@storybook/addon-actions": "8.6.12",
|
||||
"@storybook/addon-essentials": "8.6.12",
|
||||
"@storybook/addon-interactions": "8.6.12",
|
||||
"@storybook/addon-links": "8.6.12",
|
||||
"@storybook/addon-mdx-gfm": "8.6.12",
|
||||
"@storybook/addon-storysource": "8.6.12",
|
||||
"@storybook/blocks": "8.6.12",
|
||||
"@storybook/components": "8.6.12",
|
||||
"@storybook/core-events": "8.6.12",
|
||||
"@storybook/manager-api": "8.6.12",
|
||||
"@storybook/preview-api": "8.6.12",
|
||||
"@storybook/react": "8.6.12",
|
||||
"@storybook/react-vite": "8.6.12",
|
||||
"@storybook/test": "8.6.12",
|
||||
"@storybook/theming": "8.6.12",
|
||||
"@storybook/types": "8.6.12",
|
||||
"@storybook/vue3": "8.6.12",
|
||||
"@storybook/vue3-vite": "8.6.12",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/canvas-confetti": "1.9.0",
|
||||
"@types/estree": "1.0.6",
|
||||
"@types/estree": "1.0.7",
|
||||
"@types/matter-js": "0.19.8",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "22.13.11",
|
||||
"@types/node": "22.14.0",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
"@types/sanitize-html": "2.15.0",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.27.0",
|
||||
"@typescript-eslint/parser": "8.27.0",
|
||||
"@vitest/coverage-v8": "3.0.9",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.29.1",
|
||||
"@typescript-eslint/parser": "8.29.1",
|
||||
"@vitest/coverage-v8": "3.1.1",
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/runtime-core": "3.5.13",
|
||||
"acorn": "8.14.1",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "14.2.0",
|
||||
"cypress": "14.3.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-vue": "10.0.0",
|
||||
"fast-glob": "3.3.3",
|
||||
@@ -130,18 +130,17 @@
|
||||
"msw-storybook-addon": "2.0.4",
|
||||
"nodemon": "3.1.9",
|
||||
"prettier": "3.5.3",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "2.0.11",
|
||||
"storybook": "8.6.7",
|
||||
"storybook": "8.6.12",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"vite-node": "3.0.9",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "3.0.9",
|
||||
"vitest": "3.1.1",
|
||||
"vitest-fetch-mock": "0.4.5",
|
||||
"vue-component-type-helpers": "2.2.8",
|
||||
"vue-eslint-parser": "10.1.1",
|
||||
"vue-eslint-parser": "10.1.3",
|
||||
"vue-tsc": "2.2.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,19 @@ type AccountWithToken = Misskey.entities.MeDetailed & { token: string };
|
||||
|
||||
export async function getAccounts(): Promise<{
|
||||
host: string;
|
||||
user: Misskey.entities.User;
|
||||
id: Misskey.entities.User['id'];
|
||||
username: Misskey.entities.User['username'];
|
||||
user?: Misskey.entities.User | null;
|
||||
token: string | null;
|
||||
}[]> {
|
||||
const tokens = store.s.accountTokens;
|
||||
const accountInfos = store.s.accountInfos;
|
||||
const accounts = prefer.s.accounts;
|
||||
return accounts.map(([host, user]) => ({
|
||||
host,
|
||||
user,
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
user: accountInfos[host + '/' + user.id],
|
||||
token: tokens[host + '/' + user.id] ?? null,
|
||||
}));
|
||||
}
|
||||
@@ -36,7 +41,8 @@ export async function getAccounts(): Promise<{
|
||||
async function addAccount(host: string, user: Misskey.entities.User, token: AccountWithToken['token']) {
|
||||
if (!prefer.s.accounts.some(x => x[0] === host && x[1].id === user.id)) {
|
||||
store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + user.id]: token });
|
||||
prefer.commit('accounts', [...prefer.s.accounts, [host, user]]);
|
||||
store.set('accountInfos', { ...store.s.accountInfos, [host + '/' + user.id]: user });
|
||||
prefer.commit('accounts', [...prefer.s.accounts, [host, { id: user.id, username: user.username }]]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +50,10 @@ export async function removeAccount(host: string, id: AccountWithToken['id']) {
|
||||
const tokens = JSON.parse(JSON.stringify(store.s.accountTokens));
|
||||
delete tokens[host + '/' + id];
|
||||
store.set('accountTokens', tokens);
|
||||
const accountInfos = JSON.parse(JSON.stringify(store.s.accountInfos));
|
||||
delete accountInfos[host + '/' + id];
|
||||
store.set('accountInfos', accountInfos);
|
||||
|
||||
prefer.commit('accounts', prefer.s.accounts.filter(x => x[0] !== host || x[1].id !== id));
|
||||
}
|
||||
|
||||
@@ -121,14 +131,7 @@ export function updateCurrentAccount(accountData: Misskey.entities.MeDetailed) {
|
||||
for (const [key, value] of Object.entries(accountData)) {
|
||||
$i[key] = value;
|
||||
}
|
||||
prefer.commit('accounts', prefer.s.accounts.map(([host, user]) => {
|
||||
// TODO: $iのホストも比較したいけど通常null
|
||||
if (user.id === $i.id) {
|
||||
return [host, $i];
|
||||
} else {
|
||||
return [host, user];
|
||||
}
|
||||
}));
|
||||
store.set('accountInfos', { ...store.s.accountInfos, [host + '/' + $i.id]: $i });
|
||||
$i.token = token;
|
||||
miLocalStorage.setItem('account', JSON.stringify($i));
|
||||
}
|
||||
@@ -138,17 +141,9 @@ export function updateCurrentAccountPartial(accountData: Partial<Misskey.entitie
|
||||
for (const [key, value] of Object.entries(accountData)) {
|
||||
$i[key] = value;
|
||||
}
|
||||
prefer.commit('accounts', prefer.s.accounts.map(([host, user]) => {
|
||||
// TODO: $iのホストも比較したいけど通常null
|
||||
if (user.id === $i.id) {
|
||||
const newUser = JSON.parse(JSON.stringify($i));
|
||||
for (const [key, value] of Object.entries(accountData)) {
|
||||
newUser[key] = value;
|
||||
}
|
||||
return [host, newUser];
|
||||
}
|
||||
return [host, user];
|
||||
}));
|
||||
|
||||
store.set('accountInfos', { ...store.s.accountInfos, [host + '/' + $i.id]: $i });
|
||||
|
||||
miLocalStorage.setItem('account', JSON.stringify($i));
|
||||
}
|
||||
|
||||
@@ -223,25 +218,42 @@ export async function openAccountMenu(opts: {
|
||||
}, ev: MouseEvent) {
|
||||
if (!$i) return;
|
||||
|
||||
function createItem(host: string, account: Misskey.entities.User): MenuItem {
|
||||
return {
|
||||
type: 'user' as const,
|
||||
user: account,
|
||||
active: opts.active != null ? opts.active === account.id : false,
|
||||
action: async () => {
|
||||
if (opts.onChoose) {
|
||||
opts.onChoose(account);
|
||||
} else {
|
||||
switchAccount(host, account.id);
|
||||
}
|
||||
},
|
||||
};
|
||||
function createItem(host: string, id: Misskey.entities.User['id'], username: Misskey.entities.User['username'], account: Misskey.entities.User | null | undefined, token: string): MenuItem {
|
||||
if (account) {
|
||||
return {
|
||||
type: 'user' as const,
|
||||
user: account,
|
||||
active: opts.active != null ? opts.active === id : false,
|
||||
action: async () => {
|
||||
if (opts.onChoose) {
|
||||
opts.onChoose(account);
|
||||
} else {
|
||||
switchAccount(host, id);
|
||||
}
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'button' as const,
|
||||
text: username,
|
||||
active: opts.active != null ? opts.active === id : false,
|
||||
action: async () => {
|
||||
if (opts.onChoose) {
|
||||
fetchAccount(token, id).then(account => {
|
||||
opts.onChoose(account);
|
||||
});
|
||||
} else {
|
||||
switchAccount(host, id);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
// TODO: $iのホストも比較したいけど通常null
|
||||
const accountItems = (await getAccounts().then(accounts => accounts.filter(x => x.user.id !== $i.id))).map(a => createItem(a.host, a.user));
|
||||
const accountItems = (await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id))).map(a => createItem(a.host, a.id, a.username, a.user, a.token));
|
||||
|
||||
if (opts.withExtraOperation) {
|
||||
menuItems.push({
|
||||
@@ -254,7 +266,7 @@ export async function openAccountMenu(opts: {
|
||||
});
|
||||
|
||||
if (opts.includeCurrentAccount) {
|
||||
menuItems.push(createItem(host, $i));
|
||||
menuItems.push(createItem(host, $i.id, $i.username, $i, $i.token));
|
||||
}
|
||||
|
||||
menuItems.push(...accountItems);
|
||||
@@ -290,7 +302,7 @@ export async function openAccountMenu(opts: {
|
||||
});
|
||||
} else {
|
||||
if (opts.includeCurrentAccount) {
|
||||
menuItems.push(createItem(host, $i));
|
||||
menuItems.push(createItem(host, $i.id, $i.username, $i, $i.token));
|
||||
}
|
||||
|
||||
menuItems.push(...accountItems);
|
||||
|
||||
@@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
|
||||
<MkSwitch v-model="caseSensitive">{{ i18n.ts.caseSensitive }}</MkSwitch>
|
||||
<MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch>
|
||||
<MkSwitch v-model="hideNotesInSensitiveChannel">{{ i18n.ts.hideNotesInSensitiveChannel }}</MkSwitch>
|
||||
<MkSwitch v-model="excludeNotesInSensitiveChannel">{{ i18n.ts.excludeNotesInSensitiveChannel }}</MkSwitch>
|
||||
</div>
|
||||
<div :class="$style.actions">
|
||||
<div class="_buttons">
|
||||
@@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { DeepPartial } from '@/utility/merge.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
@@ -63,7 +64,6 @@ import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { deepMerge } from '@/utility/merge.js';
|
||||
import type { DeepPartial } from '@/utility/merge.js';
|
||||
|
||||
type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
|
||||
id?: string;
|
||||
@@ -87,7 +87,7 @@ const initialAntenna = deepMerge<PartialAllowedAntenna>(props.antenna ?? {}, {
|
||||
caseSensitive: false,
|
||||
localOnly: false,
|
||||
withFile: false,
|
||||
hideNotesInSensitiveChannel: false,
|
||||
excludeNotesInSensitiveChannel: false,
|
||||
isActive: true,
|
||||
hasUnreadNote: false,
|
||||
notify: false,
|
||||
@@ -110,7 +110,7 @@ const localOnly = ref<boolean>(initialAntenna.localOnly);
|
||||
const excludeBots = ref<boolean>(initialAntenna.excludeBots);
|
||||
const withReplies = ref<boolean>(initialAntenna.withReplies);
|
||||
const withFile = ref<boolean>(initialAntenna.withFile);
|
||||
const hideNotesInSensitiveChannel = ref<boolean>(initialAntenna.hideNotesInSensitiveChannel);
|
||||
const excludeNotesInSensitiveChannel = ref<boolean>(initialAntenna.excludeNotesInSensitiveChannel);
|
||||
const userLists = ref<Misskey.entities.UserList[] | null>(null);
|
||||
|
||||
watch(() => src.value, async () => {
|
||||
@@ -127,7 +127,7 @@ async function saveAntenna() {
|
||||
excludeBots: excludeBots.value,
|
||||
withReplies: withReplies.value,
|
||||
withFile: withFile.value,
|
||||
hideNotesInSensitiveChannel: hideNotesInSensitiveChannel.value,
|
||||
excludeNotesInSensitiveChannel: excludeNotesInSensitiveChannel.value,
|
||||
caseSensitive: caseSensitive.value,
|
||||
localOnly: localOnly.value,
|
||||
users: users.value.trim().split('\n').map(x => x.trim()),
|
||||
|
||||
@@ -157,7 +157,7 @@ async function init() {
|
||||
|
||||
const accounts = await getAccounts();
|
||||
|
||||
const accountIdsToFetch = accounts.map(a => a.user.id).filter(id => !users.value.has(id));
|
||||
const accountIdsToFetch = accounts.map(a => a.id).filter(id => !users.value.has(id));
|
||||
|
||||
if (accountIdsToFetch.length > 0) {
|
||||
const usersRes = await misskeyApi('users/show', {
|
||||
@@ -169,7 +169,7 @@ async function init() {
|
||||
|
||||
users.value.set(user.id, {
|
||||
...user,
|
||||
token: accounts.find(a => a.user.id === user.id)!.token,
|
||||
token: accounts.find(a => a.id === user.id)!.token,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</li>
|
||||
<li tabindex="-1" :class="$style.item" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li>
|
||||
</ol>
|
||||
<ol v-else-if="hashtags.length > 0" ref="suggests" :class="$style.list">
|
||||
<ol v-else-if="type === 'hashtag' && hashtags.length > 0" ref="suggests" :class="$style.list">
|
||||
<li v-for="hashtag in hashtags" tabindex="-1" :class="$style.item" @click="complete(type, hashtag)" @keydown="onKeydown">
|
||||
<span class="name">{{ hashtag }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
<ol v-else-if="emojis.length > 0" ref="suggests" :class="$style.list">
|
||||
<ol v-else-if="type === 'emoji' || type === 'emojiComplete' && emojis.length > 0" ref="suggests" :class="$style.list">
|
||||
<li v-for="emoji in emojis" :key="emoji.emoji" :class="$style.item" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
|
||||
<MkCustomEmoji v-if="'isCustomEmoji' in emoji && emoji.isCustomEmoji" :name="emoji.emoji" :class="$style.emoji" :fallbackToImage="true"/>
|
||||
<MkEmoji v-else :emoji="emoji.emoji" :class="$style.emoji"/>
|
||||
@@ -30,12 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span v-if="emoji.aliasOf" :class="$style.emojiAlias">({{ emoji.aliasOf }})</span>
|
||||
</li>
|
||||
</ol>
|
||||
<ol v-else-if="mfmTags.length > 0" ref="suggests" :class="$style.list">
|
||||
<ol v-else-if="type === 'mfmTag' && mfmTags.length > 0" ref="suggests" :class="$style.list">
|
||||
<li v-for="tag in mfmTags" tabindex="-1" :class="$style.item" @click="complete(type, tag)" @keydown="onKeydown">
|
||||
<span>{{ tag }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
<ol v-else-if="mfmParams.length > 0" ref="suggests" :class="$style.list">
|
||||
<ol v-else-if="type === 'mfmParam' && mfmParams.length > 0" ref="suggests" :class="$style.list">
|
||||
<li v-for="param in mfmParams" tabindex="-1" :class="$style.item" @click="complete(type, q.params.toSpliced(-1, 1, param).join(','))" @keydown="onKeydown">
|
||||
<span>{{ param }}</span>
|
||||
</li>
|
||||
@@ -58,12 +58,44 @@ import { store } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import { searchEmoji } from '@/utility/search-emoji.js';
|
||||
import { searchEmoji, searchEmojiExact } from '@/utility/search-emoji.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
export type CompleteInfo = {
|
||||
user: {
|
||||
payload: any;
|
||||
query: string | null;
|
||||
},
|
||||
hashtag: {
|
||||
payload: string;
|
||||
query: string;
|
||||
},
|
||||
// `:emo` -> `:emoji:` or some unicode emoji
|
||||
emoji: {
|
||||
payload: string;
|
||||
query: string;
|
||||
},
|
||||
// like emoji but for `:emoji:` -> unicode emoji
|
||||
emojiComplete: {
|
||||
payload: string;
|
||||
query: string;
|
||||
},
|
||||
mfmTag: {
|
||||
payload: string;
|
||||
query: string;
|
||||
},
|
||||
mfmParam: {
|
||||
payload: string;
|
||||
query: {
|
||||
tag: string;
|
||||
params: string[];
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const lib = emojilist.filter(x => x.category !== 'flags');
|
||||
|
||||
const emojiDb = computed(() => {
|
||||
const unicodeEmojiDB = computed(() => {
|
||||
//#region Unicode Emoji
|
||||
const char2path = prefer.r.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
|
||||
|
||||
@@ -87,6 +119,12 @@ const emojiDb = computed(() => {
|
||||
}
|
||||
|
||||
unicodeEmojiDB.sort((a, b) => a.name.length - b.name.length);
|
||||
|
||||
return unicodeEmojiDB;
|
||||
});
|
||||
|
||||
const emojiDb = computed(() => {
|
||||
//#region Unicode Emoji
|
||||
//#endregion
|
||||
|
||||
//#region Custom Emoji
|
||||
@@ -114,7 +152,7 @@ const emojiDb = computed(() => {
|
||||
customEmojiDB.sort((a, b) => a.name.length - b.name.length);
|
||||
//#endregion
|
||||
|
||||
return markRaw([...customEmojiDB, ...unicodeEmojiDB]);
|
||||
return markRaw([...customEmojiDB, ...unicodeEmojiDB.value]);
|
||||
});
|
||||
|
||||
export default {
|
||||
@@ -123,18 +161,23 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
q: any;
|
||||
textarea: HTMLTextAreaElement;
|
||||
<script lang="ts" setup generic="T extends keyof CompleteInfo">
|
||||
type PropsType<T extends keyof CompleteInfo> = {
|
||||
type: T;
|
||||
q: CompleteInfo[T]['query'];
|
||||
// なぜかわからないけど HTMLTextAreaElement | HTMLInputElement だと addEventListener/removeEventListenerがエラー
|
||||
textarea: (HTMLTextAreaElement | HTMLInputElement) & HTMLElement;
|
||||
close: () => void;
|
||||
x: number;
|
||||
y: number;
|
||||
}>();
|
||||
};
|
||||
//const props = defineProps<PropsType<keyof CompleteInfo>>();
|
||||
// ↑と同じだけど↓にしないとdiscriminated unionにならない。
|
||||
// https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions
|
||||
const props = defineProps<PropsType<'user'> | PropsType<'hashtag'> | PropsType<'emoji'> | PropsType<'emojiComplete'> | PropsType<'mfmTag'> | PropsType<'mfmParam'>>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'done', value: { type: string; value: any }): void;
|
||||
<T extends keyof CompleteInfo>(event: 'done', value: { type: T; value: CompleteInfo[T]['payload'] }): void;
|
||||
(event: 'closed'): void;
|
||||
}>();
|
||||
|
||||
@@ -151,10 +194,10 @@ const mfmParams = ref<string[]>([]);
|
||||
const select = ref(-1);
|
||||
const zIndex = os.claimZIndex('high');
|
||||
|
||||
function complete(type: string, value: any) {
|
||||
function complete<T extends keyof CompleteInfo>(type: T, value: CompleteInfo[T]['payload']) {
|
||||
emit('done', { type, value });
|
||||
emit('closed');
|
||||
if (type === 'emoji') {
|
||||
if (type === 'emoji' || type === 'emojiComplete') {
|
||||
let recents = store.s.recentlyUsedEmojis;
|
||||
recents = recents.filter((emoji: any) => emoji !== value);
|
||||
recents.unshift(value);
|
||||
@@ -243,6 +286,8 @@ function exec() {
|
||||
}
|
||||
|
||||
emojis.value = searchEmoji(props.q, emojiDb.value);
|
||||
} else if (props.type === 'emojiComplete') {
|
||||
emojis.value = searchEmojiExact(props.q, unicodeEmojiDB.value);
|
||||
} else if (props.type === 'mfmTag') {
|
||||
if (!props.q || props.q === '') {
|
||||
mfmTags.value = MFM_TAGS;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { chatMessage } from '../../.storybook/fakes';
|
||||
import MkChatHistories from './MkChatHistories.vue';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkChatHistories,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkChatHistories v-bind="props" />',
|
||||
};
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
msw: {
|
||||
handlers: [
|
||||
http.post('/api/chat/history', async ({ request }) => {
|
||||
const body = await request.json() as Misskey.entities.ChatHistoryRequest;
|
||||
action('POST /api/chat/history')(body);
|
||||
return HttpResponse.json([chatMessage(body.room)]);
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies StoryObj<typeof MkChatHistories>;
|
||||
@@ -0,0 +1,208 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="history.length > 0" class="_gaps_s">
|
||||
<MkA
|
||||
v-for="item in history"
|
||||
:key="item.id"
|
||||
:class="[$style.message, { [$style.isMe]: item.isMe, [$style.isRead]: item.message.isRead }]"
|
||||
class="_panel"
|
||||
:to="item.message.toRoomId ? `/chat/room/${item.message.toRoomId}` : `/chat/user/${item.other!.id}`"
|
||||
>
|
||||
<MkAvatar v-if="item.message.toRoomId" :class="$style.messageAvatar" :user="item.message.fromUser" indicator :preview="false"/>
|
||||
<MkAvatar v-else-if="item.other" :class="$style.messageAvatar" :user="item.other" indicator :preview="false"/>
|
||||
<div :class="$style.messageBody">
|
||||
<header v-if="item.message.toRoom" :class="$style.messageHeader">
|
||||
<span :class="$style.messageHeaderName"><i class="ti ti-users"></i> {{ item.message.toRoom.name }}</span>
|
||||
<MkTime :time="item.message.createdAt" :class="$style.messageHeaderTime"/>
|
||||
</header>
|
||||
<header v-else :class="$style.messageHeader">
|
||||
<MkUserName :class="$style.messageHeaderName" :user="item.other!"/>
|
||||
<MkAcct :class="$style.messageHeaderUsername" :user="item.other!"/>
|
||||
<MkTime :time="item.message.createdAt" :class="$style.messageHeaderTime"/>
|
||||
</header>
|
||||
<div :class="$style.messageBodyText"><span v-if="item.isMe" :class="$style.youSaid">{{ i18n.ts.you }}:</span>{{ item.message.text }}</div>
|
||||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
<div v-if="!initializing && history.length == 0" class="_fullinfo">
|
||||
<div>{{ i18n.ts._chat.noHistory }}</div>
|
||||
</div>
|
||||
<MkLoading v-if="initializing"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onActivated, onDeactivated, onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const history = ref<{
|
||||
id: string;
|
||||
message: Misskey.entities.ChatMessage;
|
||||
other: Misskey.entities.ChatMessage['fromUser'] | Misskey.entities.ChatMessage['toUser'] | null;
|
||||
isMe: boolean;
|
||||
}[]>([]);
|
||||
|
||||
const initializing = ref(true);
|
||||
const fetching = ref(false);
|
||||
|
||||
async function fetchHistory() {
|
||||
if (fetching.value) return;
|
||||
|
||||
fetching.value = true;
|
||||
|
||||
const [userMessages, roomMessages] = await Promise.all([
|
||||
misskeyApi('chat/history', { room: false }),
|
||||
misskeyApi('chat/history', { room: true }),
|
||||
]);
|
||||
|
||||
history.value = [...userMessages, ...roomMessages]
|
||||
.toSorted((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
.map(m => ({
|
||||
id: m.id,
|
||||
message: m,
|
||||
other: (!('room' in m) || m.room == null) ? (m.fromUserId === $i.id ? m.toUser : m.fromUser) : null,
|
||||
isMe: m.fromUserId === $i.id,
|
||||
}));
|
||||
|
||||
fetching.value = false;
|
||||
initializing.value = false;
|
||||
}
|
||||
|
||||
let isActivated = true;
|
||||
|
||||
onActivated(() => {
|
||||
isActivated = true;
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
isActivated = false;
|
||||
});
|
||||
|
||||
useInterval(() => {
|
||||
// TODO: DOM的にバックグラウンドになっていないかどうかも考慮する
|
||||
if (!window.document.hidden && isActivated) {
|
||||
fetchHistory();
|
||||
}
|
||||
}, 1000 * 10, {
|
||||
immediate: false,
|
||||
afterMounted: true,
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
fetchHistory();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchHistory();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.message {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 16px 24px;
|
||||
|
||||
&.isRead,
|
||||
&.isMe {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:not(.isMe):not(.isRead) {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
background-color: var(--MI_THEME-accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 500px) {
|
||||
.message {
|
||||
font-size: 90%;
|
||||
padding: 14px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 450px) {
|
||||
.message {
|
||||
font-size: 80%;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.messageAvatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 0 16px 0 0;
|
||||
}
|
||||
|
||||
@container (max-width: 500px) {
|
||||
.messageAvatar {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 450px) {
|
||||
.messageAvatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.messageBody {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.messageHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.messageHeaderName {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.messageHeaderUsername {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.messageHeaderTime {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.messageBodyText {
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.youSaid {
|
||||
font-weight: bold;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
</style>
|
||||
@@ -12,8 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #fallback>
|
||||
<MkLoading/>
|
||||
</template>
|
||||
<XCode v-if="show && lang" :code="code" :lang="lang"/>
|
||||
<pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
|
||||
<XCode v-if="show && lang" class="_selectable" :code="code" :lang="lang"/>
|
||||
<pre v-else-if="show" class="_selectable" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
|
||||
<button v-else :class="$style.codePlaceholderRoot" @click="show = true">
|
||||
<div :class="$style.codePlaceholderContainer">
|
||||
<div><i class="ti ti-code"></i> {{ i18n.ts.code }}</div>
|
||||
@@ -70,11 +70,9 @@ function copy() {
|
||||
.codeBlockFallbackRoot {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
background: var(--MI_THEME-bg);
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.codeBlockFallbackCode {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MkDisableSection from './MkDisableSection.vue';
|
||||
void MkDisableSection;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user