diff --git a/CHANGELOG_CHERRYPICK.md b/CHANGELOG_CHERRYPICK.md index b6105e353e..613b055720 100644 --- a/CHANGELOG_CHERRYPICK.md +++ b/CHANGELOG_CHERRYPICK.md @@ -23,8 +23,81 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024xx](CHANGE # 릴리즈 노트 이 문서는 CherryPick의 변경 사항만 포함합니다. +## 4.12.1 +출시일: 2024/11/5
+기반 Misskey 버전: 2024.9.0
+Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#202490](CHANGELOG.md#202490) 문서를 참고하십시오. + +## NOTE +- `오브젝트 스토리지 (리모트)`의 내부 변경으로 인해 DB 마이그레이션 과정에서 이전에 설정한 값이 제거됩니다. 마이그레이션 이후 값을 다시 설정해 주십시오. + +### General +- Feat: 사용자 메뉴에서 서버를 뮤트할 수 있음 (kokonect-link/cherrypick#502) + - 이전 빌드에 추가된 기능은 관리자 전용이며, 이 빌드에서 추가된 기능은 일반 사용자용 기능입니다. +- Feat: 새 노트 알림을 묶어서 표시할 수 있음 (yojo-art/cherrypick#328) +- Feat: 노트 게시를 예약할 수 있음 (kokonect-link/cherrypick#513, yojo-art/cherrypick#483, [Type4ny-Project/Type4ny@271c872c](https://github.com/Type4ny-Project/Type4ny/commit/271c872c97f215ef5d8e0be62251dd422a52e5b1)) + +### Client +- Enhance: (Friendly) 모바일 환경에서 계정 목록을 표시할 때 내 프로필을 표시함 +- Enhance: 업데이트 및 마이그레이션 알림에서 CherryPick의 변경 사항만 표시함 +- Enhance: 검색과 같은 입력 블록에서 `Enter`를 입력하면 자동으로 가상 키보드를 숨김 +- Enhance: 환영 페이지의 타임라인에서 사용할 수 있는 일부 노트 메뉴 추가 + - QR 코드 생성 + - 새 탭에서 열기 + - 리노트 목록 + - 리액션 목록 + - 텍스트 소스 보기 + - 고양이체로 표시하지 않기 +- Enhance: 노트 작성 폼의 사용자 선택 메뉴에서 현재 계정과 로그인된 추가 계정 영역을 구분함 +- Enhance: 모바일 환경에서 토스트 알림의 디자인 및 배치를 개선함 +- Enhance: 로그인 시 표시되는 환영 메시지의 표시 유무를 선택할 수 있음 +- Ehhance: `텍스트 소스 보기`를 사용하면 자동으로 내용을 펼침 +- Enhance: 사용자 메뉴에서 서버 정보 페이지로 갈 수 있는 바로 가기를 추가함 +- Enhance: 사용자 페이지와 사용자 팝업 개선 + - 사용자 페이지와 사용자 팝업에 `새 노트 알림 켜기` 버튼이 추가됨 + - 사용자 페이지에 `사용자 노트 검색` 버튼이 추가됨 +- Enhance: 상대방이 나를 차단한 경우 차단되었음을 알 수 있도록 개선함 + - 차단되면 다음 기능들이 화면 상에서 사라지고 사용이 제한됩니다. + - 팔로우 버튼 + - 사용자 페이지에서 기본 정보 외 다른 모든 정보는 열람할 수 없음 + - 사용자 페이지에서 요약 탭 외에 모든 탭이 사용할 수 없게됨 +- Fix: 임베디드 코드에서 CherryPick의 색상 설정이 반영되지 않음 +- Fix: 임베디드 코드에 `fade`와 `Temml(KaTex)`가 반영되지 않음 +- Fix: 노트의 QR 코드를 생성했을 때 `링크 복사` 버튼을 누르면 잘못된 토스트 알림이 표시됨 +- Fix: 노트 메뉴에 `링크 복사` 옵션이 표시되지 않음 +- Fix: 서버 이름에 마크업 언어가 포함되어 있으면 외부 사이트로 이동할 때 표시되는 대화상자에서 서버 이름이 잘못 표시될 수 있음 +- Fix: 로그인 하지 않은 사용자가 노트 내용에 포함된 이모지를 누르면 이모지 복사 및 리액션 메뉴에 접근할 수 있음 +- Fix: 모바일 환경에서 노트 작성 폼의 미리보기 디자인이 잘못 표시될 수 있음 +- Fix: 리버시에서 커스텀 이모지를 리액션으로 보낼 수 없음 +- Fix: 리버시에서 리액션할 때 말풍선의 위치가 어긋나 보일 수 있음 +- Fix: 특정 조건에서 노트 동작 버튼을 비활성화 해도 버튼이 사라지지 않음 + - 노트에 답글을 작성할 수 없을 때 + - 노트를 리노트할 수 없을 때 +- Fix: 캡션이 512자를 초과하면 초과한 내용이 잘릴 수 있음 (kokonect-link/cherrypick#518) + - 캡션을 512자를 초과해서 작성하면 캡션 내용을 저장하기 전에 경고를 표시합니다. +- Fix: 노트 상세 페이지의 InstanceTicker에 커서를 올릴 때 마우스 포인터가 올바르게 표시되지 않음 +- Fix: `UI에 흐림 효과 사용` 옵션이 토스트 알림에서 제대로 적용되지 않음 + - 모바일 환경에서만 적용되는 문제를 해결합니다. +- Fix: 모바일 환경에서 제어판의 인디케이터가 잘못된 위치에 표시될 수 있음 +- Fix: `답글을 자동으로 더 보기`를 활성화하면 3개 미만의 답글이 있는 노트에서 답글이 보이지 않음 (kokonect-link/cherrypick#521) +- Fix: 제어판에서 문의처 URL이 설정되지 않았을 때 표시되는 경고의 바로가기가 잘못 설정되어 있음 +- Fix: 캡션이 설정된 이미지 위에 마우스 커서를 올려도 캡션이 표시되지 않음 (kokonect-link/cherrypick#514) +- Fix: 코드 편집기의 커서 위치가 올바르게 표시되지 않을 수 있음 (kokonect-link/cherrypick#520) +- Fix: 투표 기한을 `기간 지정`으로 설정한 경우 투표가 즉시 종료될 수 있음 (kokonect-link/cherrypick#523) +- Fix: 로그인하지 않은 사용자가 리버시 전적을 볼 수 없음 (yojo-art/cherrypick#404) + +### Server +- Enhance: 노트 편집 제한 완화 + - 1시간에 10번 편집할 수 있던 것을 5분에 10번 편집할 수 있도록 완화함. +- Enhance: 이모지를 등록할 때 시스템 사용자로 다시 업로드 하도록 변경함 (yojo-art/cherrypick#510) + - 이모지를 등록한 사용자가 계정을 삭제하면 이모지도 같이 삭제되기 때문에 변경되었습니다. +- Fix: 로컬 전용 노트를 편집하면 편집한 노트가 연합될 수 있음 (kokonect-link/cherrypick#519, [libnare/shiftkey@654821da](https://github.com/libnare/shiftkey/commit/654821da003be7471f3c6fc320bf50afcb599d4e)) + +--- + + ## 4.12.0 -출시일: 2024/10/08
+출시일: 2024/10/8
기반 Misskey 버전: 2024.9.0
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#202490](CHANGELOG.md#202490) 문서를 참고하십시오. diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index c30037a6a2..5ecdbd3b39 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -938,9 +938,9 @@ _aboutMisskey: donate: "Misskey তে দান করুন" morePatrons: "আরও অনেকে আমাদের সাহায্য করছেন। তাদের সবাইকে ধন্যবাদ 🥰" patrons: "সমর্থনকারী" -_cfm: - cheatSheet: "CFM চিটশিট" - intro: "CFM একটি মার্কআপ ভাষা যা CherryPick-এর মধ্যে বিভিন্ন জায়গায় ব্যবহার করা যেতে পারে। এখানে আপনি CFM-এর সিনট্যাক্সগুলির একটি তালিকা দেখতে পারবেন।" +_mfc: + cheatSheet: "MFC চিটশিট" + intro: "MFC একটি মার্কআপ ভাষা যা CherryPick-এর মধ্যে বিভিন্ন জায়গায় ব্যবহার করা যেতে পারে। এখানে আপনি MFC-এর সিনট্যাক্সগুলির একটি তালিকা দেখতে পারবেন।" dummy: "মিসকি ফেডিভার্সের বিশ্বকে প্রসারিত করে" mention: "উল্লেখ" mentionDescription: "@ চিহ্ন + ব্যবহারকারীর নাম একটি নির্দিষ্ট ব্যবহারকারীকে নির্দেশ করতে ব্যবহার করা যায়।" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index dd9101baf0..94969f4291 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -513,8 +513,8 @@ showNoteActionsOnlyHover: "Només mostra accions de la nota en passar amb el cur showReactionsCount: "Mostra el nombre de reaccions a les publicacions" noHistory: "No hi ha un registre previ" signinHistory: "Historial d'autenticacions" -enableAdvancedMfm: "Habilitar l'CFM avançat" -enableAnimatedMfm: "Habilitar l'CFM amb moviment" +enableAdvancedMfm: "Habilitar l'MFC avançat" +enableAnimatedMfm: "Habilitar l'MFC amb moviment" doing: "Processant..." category: "Categoria" tags: "Etiquetes" @@ -1229,8 +1229,8 @@ remainingN: "Queden: {n}" overwriteContentConfirm: "Vols substituir el contingut actual?" seasonalScreenEffect: "Efectes de pantalla segons les estacions" decorate: "Decorar" -addMfmFunction: "Afegeix funcions CFM" -enableQuickAddMfmFunction: "Activar accés ràpid per afegir funcions CFM" +addMfmFunction: "Afegeix funcions MFC" +enableQuickAddMfmFunction: "Activar accés ràpid per afegir funcions MFC" bubbleGame: "Bubble Game" sfx: "Efectes de so" soundWillBePlayed: "Es reproduiran efectes de so" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index e6ec005d82..2f532e0f4c 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -474,8 +474,8 @@ native: "Výchozí" showNoteActionsOnlyHover: "Zobrazit akce poznámky jenom při naběhnutí myši" noHistory: "Žádná historie" signinHistory: "Historie přihlášení" -enableAdvancedMfm: "Zapnout pokročilé CFM" -enableAnimatedMfm: "Zapnout animované CFM" +enableAdvancedMfm: "Zapnout pokročilé MFC" +enableAnimatedMfm: "Zapnout animované MFC" doing: "Procesuju..." category: "Kategorie" tags: "Štítky" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index fa7f570aa5..49e42c1885 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -506,8 +506,8 @@ joinOrCreateGroup: "Lass dich zu einer Gruppe einladen oder erstelle deine eigen showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen" noHistory: "Kein Verlauf gefunden" signinHistory: "Anmeldungsverlauf" -enableAdvancedMfm: "Erweitertes CFM aktivieren" -enableAnimatedMfm: "Animiertes CFM aktivieren" +enableAdvancedMfm: "Erweitertes MFC aktivieren" +enableAnimatedMfm: "Animiertes MFC aktivieren" doing: "In Bearbeitung …" category: "Kategorie" tags: "Aliasse" @@ -1194,7 +1194,7 @@ cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Besch doReaction: "Reagieren" code: "Code" decorate: "Dekorieren" -addMfmFunction: "CFM hinzufügen" +addMfmFunction: "MFC hinzufügen" sfx: "Soundeffekte" lastNDays: "Letzten {n} Tage" surrender: "Abbrechen" @@ -1708,9 +1708,9 @@ _displayOfSensitiveMedia: respect: "Sensible Medien verbergen" ignore: "Sensible Medien anzeigen" force: "Alle Medien verbergen" -_cfm: - cheatSheet: "CFM Spickzettel" - intro: "CFM ist eine CherryPick-exklusive Markup-Sprache, die in CherryPick an vielen Stellen verwendet werden kann. Hier kannst du eine Liste von verfügbarer CFM-Syntax einsehen." +_mfc: + cheatSheet: "MFC Spickzettel" + intro: "MFC ist eine CherryPick-exklusive Markup-Sprache, die in CherryPick an vielen Stellen verwendet werden kann. Hier kannst du eine Liste von verfügbarer MFC-Syntax einsehen." dummy: "CherryPick erweitert die Welt des Fediverse" mention: "Erwähnung" mentionDescription: "Mit At-Zeichen und Benutzername kann ein individueller Nutzer angegeben werden." @@ -1773,7 +1773,7 @@ _cfm: rotate: "Drehen" rotateDescription: "Dreht den Inhalt um einen angegebenen Winkel." plain: "Schlicht" - plainDescription: "Deaktiviert jegliche CFM-Syntax, die sich innerhalb dieses CFM-Effekts befindet." + plainDescription: "Deaktiviert jegliche MFC-Syntax, die sich innerhalb dieses MFC-Effekts befindet." _instanceTicker: none: "Nie anzeigen" remote: "Für Benutzer fremder Instanzen anzeigen" diff --git a/locales/en-US.yml b/locales/en-US.yml index e42d5cdb1f..6f1fa2e2d1 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1,5 +1,12 @@ --- _lang_: "English" +youBlocked: "You’re blocked" +youBlockedDescription: "You can’t follow or see {user}’s posts." +schedulePost: "Posting a scheduled note" +schedulePostList: "List of scheduled notes" +welcomeBackToast: "Display a welcome message when you log in after a certain period of time" +invalidTextLengthError: "Too many characters entered" +invalidTextLengthDescription: "The number of characters is limited to {limitValue} characters. The current number of characters entered is {value} characters." autoLoadMoreReplies: "Show more automatically replies" autoLoadMoreConversation: "Show more conversation automatically" useAutoTranslate: "Automatic translation" @@ -10,7 +17,7 @@ widgets: "Widgets" postNote: "Post note" bottomNavbar: "Bottom navigation bar" bottomNavbarDescription: "This setting is only available in a mobile environment." -scheduledNoteDelete: "Schedule note deletion" +scheduledNoteDelete: "Schedule deletion of note" getQRCode: "Get QR code" customSplashText: "Custom splash text" customSplashTextDescription: "This text will be displayed on the loading page." @@ -168,6 +175,7 @@ receiveFollowRequest: "Follow request received" followRequestAccepted: "Follow request accepted" mention: "Mention" mentions: "Mentions" +newNotes: "New notes" directNotes: "Direct notes" importAndExport: "Import / Export" import: "Import" @@ -627,10 +635,10 @@ showNoteActionsOnlyHover: "Only show note actions on hover" showReactionsCount: "See the number of reactions in notes" noHistory: "No history available" signinHistory: "Login history" -enableAdvancedMfm: "Enable advanced CFM" -enableAdvancedMfmDescription: "When enabled, various CFM features, such as CFM with animated are available." -enableAnimatedMfm: "Enable animated CFM" -enableAnimatedMfmDescription: "When enabled, moves text that uses CFM grammar or emoji." +enableAdvancedMfm: "Enable advanced MFC" +enableAdvancedMfmDescription: "When enabled, various MFC features, such as MFC with animated are available." +enableAnimatedMfm: "Enable animated MFC" +enableAnimatedMfmDescription: "When enabled, moves text that uses MFC grammar or emoji." doing: "Processing..." category: "Category" tags: "Aliases" @@ -1164,7 +1172,7 @@ thisPostMayBeAnnoyingCancel: "Cancel" thisPostMayBeAnnoyingIgnore: "Post anyway" collapseRenotes: "Collapse renotes you've already seen" collapseRenotesDescription: "Collapse notes that you've reacted to or renoted before." -collapseDefault: "Collapse notes using specific CFM syntax" +collapseDefault: "Collapse notes using specific MFC syntax" internalServerError: "Internal Server Error" internalServerErrorDescription: "The server has run into an unexpected error." copyErrorInfo: "Copy error details" @@ -1360,8 +1368,8 @@ remainingN: "Remaining: {n}" overwriteContentConfirm: "Are you sure you want to overwrite the current content?" seasonalScreenEffect: "Seasonal Screen Effect" decorate: "Decorate" -addMfmFunction: "Add CFM" -enableQuickAddMfmFunction: "Show advanced CFM picker" +addMfmFunction: "Add MFC" +enableQuickAddMfmFunction: "Show advanced MFC picker" bubbleGame: "Bubble Game" sfx: "Sound Effects" soundWillBePlayed: "Sound will be played" @@ -1522,7 +1530,7 @@ _initialAccountSetting: privacySetting: "Privacy settings" fontSizeSetting: "Font size settings" blurEffectsSetting: "Blur effects settings" - mfmAndAnimatedImagesSetting: "CFM and Animated images settings" + mfmAndAnimatedImagesSetting: "MFC and Animated images settings" theseSettingsCanEditLater: "You can always change these settings later." youCanEditMoreSettingsInSettingsPageLater: "There are many more settings you can configure from the \"Settings\" page. Be sure to visit it later." followUsers: "Try following some users that interest you to build up your timeline." @@ -1956,6 +1964,7 @@ _role: ltlAvailable: "Can view the local timeline" canPublicNote: "Can send public notes" canEditNote: "Note editing" + scheduleNoteMax: "Maximum number of scheduled notes" mentionMax: "Maximum number of mentions in a note" canInvite: "Can create instance invite codes" inviteLimit: "Invite limit" @@ -2113,9 +2122,9 @@ _displayOfSensitiveMedia: respect: "Hide media marked as sensitive" ignore: "Display media marked as sensitive" force: "Hide all media" -_cfm: - cheatSheet: "CFM Cheatsheet" - intro: "CFM is a CherryPick-exclusive markup language that can be used in many places. Here you can view a list of all available CFM syntax." +_mfc: + cheatSheet: "MFC Cheatsheet" + intro: "MFC is a CherryPick-exclusive markup language that can be used in many places. Here you can view a list of all available MFC syntax." dummy: "CherryPick expands the world of the Fediverse" mention: "Mention" mentionDescription: "You can specify a user by using an At-Symbol and a username." @@ -2188,7 +2197,7 @@ _cfm: bg: "Background color" bgDescription: "Set the background color to the specified value." plain: "Plain" - plainDescription: "Deactivates the effects of all CFM contained within this CFM effect." + plainDescription: "Deactivates the effects of all MFC contained within this MFC effect." ruby: "Ruby" rubyDescription: "Display ruby characters over the text." _instanceTicker: @@ -2383,6 +2392,8 @@ _permissions: "read:mutes": "View your list of muted users" "write:mutes": "Edit your list of muted users" "write:notes": "Compose or delete notes" + "read:notes-schedule": "View your list of scheduled notes" + "write:notes-schedule": "Compose or delete scheduled notes" "read:notifications": "View your notifications" "write:notifications": "Manage your notifications" "read:reactions": "View your reactions" @@ -2712,6 +2723,7 @@ _notification: reactedBySomeUsers: "{n} users reacted" likedBySomeUsers: "{n} users liked your note" renotedBySomeUsers: "Renote from {n} users" + notedBySomeUsers: "There are {n} new notes" followedBySomeUsers: "Followed by {n} users" flushNotification: "Clear notifications" _types: @@ -2918,7 +2930,7 @@ _dataSaver: description: "URL preview thumbnail images will no longer be loaded." _code: title: "Code highlighting" - description: "If code highlighting notations are used in CFM, etc., they will not load until tapped. Syntax highlighting requires downloading the highlight definition files for each programming language. Therefore, disabling the automatic loading of these files is expected to reduce the amount of communication data." + description: "If code highlighting notations are used in MFC, etc., they will not load until tapped. Syntax highlighting requires downloading the highlight definition files for each programming language. Therefore, disabling the automatic loading of these files is expected to reduce the amount of communication data." _hemisphere: N: "Northern Hemisphere" S: "Southern Hemisphere" @@ -2958,7 +2970,7 @@ _reversi: canPutEverywhere: "Tiles are placeable everywhere" timeLimitForEachTurn: "Time limit for turn" freeMatch: "Free Match" - lookingForPlayer: "Finding opponent..." + lookingForPlayer: "Finding opponent" gameCanceled: "The game has been cancelled." shareToTlTheGameWhenStart: "Share Game to timeline when started" iStartedAGame: "The game has begun! #MisskeyReversi" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 81b180ed1c..3adce64031 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -518,8 +518,8 @@ showNoteActionsOnlyHover: "Mostrar acciones de la nota sólo al pasar el cursor" showReactionsCount: "Mostrar el número de reacciones en las notas" noHistory: "No hay datos en el historial" signinHistory: "Historial de ingresos" -enableAdvancedMfm: "Habilitar CFM avanzado" -enableAnimatedMfm: "Habilitar CFM con movimiento" +enableAdvancedMfm: "Habilitar MFC avanzado" +enableAnimatedMfm: "Habilitar MFC con movimiento" doing: "Voy en camino" category: "Categoría" tags: "Etiqueta" @@ -1229,8 +1229,8 @@ remainingN: "Faltan: {n}" overwriteContentConfirm: "¿Quieres sustituir todo el contenido actual?" seasonalScreenEffect: "Efectos de pantalla asociados a estaciones" decorate: "Decorar" -addMfmFunction: "Añadir función CFM" -enableQuickAddMfmFunction: "Activar acceso rápido para añadir funciones CFM" +addMfmFunction: "Añadir función MFC" +enableQuickAddMfmFunction: "Activar acceso rápido para añadir funciones MFC" bubbleGame: "Bubble Game" sfx: "Efectos de sonido" soundWillBePlayed: "Se reproducirán efectos sonoros" @@ -1837,9 +1837,9 @@ _displayOfSensitiveMedia: respect: "Esconder medios marcados como sensibles" ignore: "Mostrar medios marcados como sensibles" force: "Esconder todala multimedia" -_cfm: - cheatSheet: "Hoja de referencia de CFM" - intro: "CFM es un lenguaje de marcado dedicado que se puede usar en varios lugares dentro de CherryPick. Aquí puede ver una lista de sintaxis disponibles en CFM." +_mfc: + cheatSheet: "Hoja de referencia de MFC" + intro: "MFC es un lenguaje de marcado dedicado que se puede usar en varios lugares dentro de CherryPick. Aquí puede ver una lista de sintaxis disponibles en MFC." dummy: "CherryPick expande el mundo de la Fediverso" mention: "Menciones" mentionDescription: "El signo @ seguido de un nombre de usuario se puede utilizar para notificar a un usuario en particular." @@ -1902,7 +1902,7 @@ _cfm: rotate: "Rotar" rotateDescription: "Rota el contenido a un ángulo especificado." plain: "Plano" - plainDescription: "Desactiva los efectos de todo el contenido CFM con este efecto CFM." + plainDescription: "Desactiva los efectos de todo el contenido MFC con este efecto MFC." _instanceTicker: none: "No mostrar" remote: "Mostrar a usuarios remotos" @@ -2593,7 +2593,7 @@ _dataSaver: description: "Desactiva la carga de vistas previas de las URLs." _code: title: "Resaltar código" - description: "Si se usa resaltado de código en CFM, etc., no se cargará hasta pulsar en ello. El resaltado de sintaxis requiere la descarga de archivos de definición para cada lenguaje de programación. Debido a esto, al deshabilitar la carga automática de estos archivos reducirás el consumo de datos." + description: "Si se usa resaltado de código en MFC, etc., no se cargará hasta pulsar en ello. El resaltado de sintaxis requiere la descarga de archivos de definición para cada lenguaje de programación. Debido a esto, al deshabilitar la carga automática de estos archivos reducirás el consumo de datos." _hemisphere: N: "Hemisferio norte" S: "Hemisferio sur" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 85dec114af..1cfabcd867 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -509,8 +509,8 @@ showNoteActionsOnlyHover: "Afficher les actions de note uniquement au survol" showReactionsCount: "Afficher le nombre de réactions des notes" noHistory: "Pas d'historique" signinHistory: "Historique de connexion" -enableAdvancedMfm: "Activer la CFM avancée" -enableAnimatedMfm: "Activer le CFM animé" +enableAdvancedMfm: "Activer la MFC avancée" +enableAnimatedMfm: "Activer le MFC animé" doing: "En cours..." category: "Catégorie" tags: "Étiquettes" @@ -1219,8 +1219,8 @@ remainingN: "Restants : {n}" overwriteContentConfirm: "Voulez-vous remplacer le contenu actuel ?" seasonalScreenEffect: "Effet d'écran saisonnier" decorate: "Décorer" -addMfmFunction: "Insérer CFM" -enableQuickAddMfmFunction: "Afficher le sélecteur de CFM avancé" +addMfmFunction: "Insérer MFC" +enableQuickAddMfmFunction: "Afficher le sélecteur de MFC avancé" bubbleGame: "Jeu de bulles" sfx: "Effets sonores" soundWillBePlayed: "Le son sera joué" @@ -1622,9 +1622,9 @@ _aboutMisskey: projectMembers: "Membres du projet" _displayOfSensitiveMedia: force: "Masquer tous les médias" -_cfm: - cheatSheet: "Antisèche CFM" - intro: "CFM est un langage Markdown spécifique utilisable ici et là dans CherryPick. Vous pouvez vérifier ici les structures utilisables avec CFM." +_mfc: + cheatSheet: "Antisèche MFC" + intro: "MFC est un langage Markdown spécifique utilisable ici et là dans CherryPick. Vous pouvez vérifier ici les structures utilisables avec MFC." dummy: "La Fédiverse s'agrandit avec CherryPick" mention: "Mentionner" mentionDescription: "Vous pouvez afficher un utilisateur spécifique en indiquant une arobase suivie d'un nom d'utilisateur" @@ -2264,7 +2264,7 @@ _dataSaver: description: "Les vignettes d'aperçu des URL ne seront plus chargées." _code: title: "Mise en évidence du code" - description: "Si la notation de mise en évidence du code est utilisée, par exemple dans la CFM, elle ne sera pas chargée tant qu'elle n'aura pas été tapée. La mise en évidence du code nécessite le chargement du fichier de définition de chaque langue à mettre en évidence, mais comme ces fichiers ne sont plus chargés automatiquement, on peut s'attendre à une réduction du trafic de données." + description: "Si la notation de mise en évidence du code est utilisée, par exemple dans la MFC, elle ne sera pas chargée tant qu'elle n'aura pas été tapée. La mise en évidence du code nécessite le chargement du fichier de définition de chaque langue à mettre en évidence, mais comme ces fichiers ne sont plus chargés automatiquement, on peut s'attendre à une réduction du trafic de données." _reversi: waitingBoth: "Préparez-vous" total: "Total" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 347fe345da..5de0443e92 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -520,8 +520,8 @@ showNoteActionsOnlyHover: "Hanya tampilkan aksi catatan saat ditunjuk" showReactionsCount: "Lihat jumlah reaksi dalam catatan" noHistory: "Tidak ada riwayat" signinHistory: "Riwayat masuk" -enableAdvancedMfm: "Nyalakan CFM tingkat lanjut" -enableAnimatedMfm: "Nyalakan animasi CFM" +enableAdvancedMfm: "Nyalakan MFC tingkat lanjut" +enableAnimatedMfm: "Nyalakan animasi MFC" doing: "Sedang berkerja..." category: "Kategori" tags: "Tandai" @@ -1230,7 +1230,7 @@ overwriteContentConfirm: "Apakah kamu yakin untuk menimpa konten saat ini?" seasonalScreenEffect: "Efek layar musiman" decorate: "Dekor" addMfmFunction: "Tambahkan dekorasi" -enableQuickAddMfmFunction: "Tampilkan pemilih CFM tingkat lanjut" +enableQuickAddMfmFunction: "Tampilkan pemilih MFC tingkat lanjut" bubbleGame: "Bubble Game" sfx: "Efek Suara" soundWillBePlayed: "Suara yang akan dimainkan" @@ -1846,9 +1846,9 @@ _displayOfSensitiveMedia: respect: "Sembunyikan media yang ditandai sensitif" ignore: "Tampilkan media yang ditandai sensitif" force: "Sembunyikan semua media" -_cfm: - cheatSheet: "Contekan CFM" - intro: "CFM adalah CherryPick-exclusive Markup Language yang dapat digunakan di banyak tempat. Berikut kamu bisa melihat daftar dari syntax CFM yang ada." +_mfc: + cheatSheet: "Contekan MFC" + intro: "MFC adalah CherryPick-exclusive Markup Language yang dapat digunakan di banyak tempat. Berikut kamu bisa melihat daftar dari syntax MFC yang ada." dummy: "CherryPick membentangkan dunia Fediverse" mention: "Sebut" mentionDescription: "Kamu dapat menentukan pengguna tertentu dengan menggunakan simbol-At dan nama engguna mereka." @@ -2603,7 +2603,7 @@ _dataSaver: description: "Gambar kecil URL pratinjau tidak akan dimuat lagi." _code: title: "Penyorotan kode" - description: "Jika notasi penyorotan kode digunakan di CFM, dll. Fungsi tersebut tidak akan dimuat apabila tidak diketuk. Penyorotan sintaks membutuhkan pengunduhan berkas definisi penyorotan untuk setiap bahasa pemrograman. Oleh sebab itu, menonaktifkan pemuatan otomatis dari berkas ini dilakukan untuk mengurangi jumlah komunikasi data." + description: "Jika notasi penyorotan kode digunakan di MFC, dll. Fungsi tersebut tidak akan dimuat apabila tidak diketuk. Penyorotan sintaks membutuhkan pengunduhan berkas definisi penyorotan untuk setiap bahasa pemrograman. Oleh sebab itu, menonaktifkan pemuatan otomatis dari berkas ini dilakukan untuk mengurangi jumlah komunikasi data." _hemisphere: N: "Bumi belahan utara" S: "Bumi belahan selatan" diff --git a/locales/index.d.ts b/locales/index.d.ts index 7ac337e7a1..c90549b1d0 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -13,6 +13,34 @@ export interface Locale extends ILocale { * 日本語 */ "_lang_": string; + /** + * ブロックされています + */ + "youBlocked": string; + /** + * {user}さんのフォローやポストの表示はできません。 + */ + "youBlockedDescription": ParameterizedString<"user">; + /** + * 予約投稿 + */ + "schedulePost": string; + /** + * 予約投稿一覧 + */ + "schedulePostList": string; + /** + * 一定時間が経過した後に接続したときに歓迎メッセージを表示 + */ + "welcomeBackToast": string; + /** + * 入力された文字数が多すぎます + */ + "invalidTextLengthError": string; + /** + * 文字数が{limitValue}文字に制限されています。現在入力された文字数は{value}文字です。 + */ + "invalidTextLengthDescription": ParameterizedString<"limitValue" | "value">; /** * 返信を自動でもっと見る */ @@ -699,6 +727,10 @@ export interface Locale extends ILocale { * あなた宛て */ "mentions": string; + /** + * 新規投稿 + */ + "newNotes": string; /** * ダイレクト投稿 */ @@ -2564,19 +2596,19 @@ export interface Locale extends ILocale { */ "signinHistory": string; /** - * 高度なCFMを有効にする + * 高度なMFCを有効にする */ "enableAdvancedMfm": string; /** - * 有効にすると、動きのあるCFMのようなさまざまなCFM機能が使用できます。 + * 有効にすると、動きのあるMFCのようなさまざまなMFC機能が使用できます。 */ "enableAdvancedMfmDescription": string; /** - * 動きのあるCFMを有効にする + * 動きのあるMFCを有効にする */ "enableAnimatedMfm": string; /** - * 有効にすると、CFM文法または絵文字を使用するテキストが動きます。 + * 有効にすると、MFC文法または絵文字を使用するテキストが動きます。 */ "enableAnimatedMfmDescription": string; /** @@ -4721,7 +4753,7 @@ export interface Locale extends ILocale { */ "collapseRenotesDescription": string; /** - * 特定のCFM構文を含むノートを省略して表示 + * 特定のMFC構文を含むノートを省略して表示 */ "collapseDefault": string; /** @@ -5509,7 +5541,7 @@ export interface Locale extends ILocale { */ "addMfmFunction": string; /** - * 高度なCFMのピッカーを表示する + * 高度なMFCのピッカーを表示する */ "enableQuickAddMfmFunction": string; /** @@ -6155,7 +6187,7 @@ export interface Locale extends ILocale { */ "blurEffectsSetting": string; /** - * CFMとアニメーション画像設定 + * MFCとアニメーション画像設定 */ "mfmAndAnimatedImagesSetting": string; /** @@ -7707,6 +7739,10 @@ export interface Locale extends ILocale { * ノートの編集 */ "canEditNote": string; + /** + * 予約投稿の最大数 + */ + "scheduleNoteMax": string; /** * ノート内の最大メンション数 */ @@ -8323,13 +8359,13 @@ export interface Locale extends ILocale { */ "force": string; }; - "_cfm": { + "_mfc": { /** - * CFMチートシート + * MFCチートシート */ "cheatSheet": string; /** - * CFMは、CherryPick内の様々な場所で使用できる専用のマークアップ言語です。ここでは、CFMで使用可能な構文一覧が確認できます。 + * MFCは、CherryPick内の様々な場所で使用できる専用のマークアップ言語です。ここでは、MFCで使用可能な構文一覧が確認できます。 */ "intro": string; /** @@ -9370,6 +9406,14 @@ export interface Locale extends ILocale { * ノートを作成・削除する */ "write:notes": string; + /** + * 予約投稿を見る + */ + "read:notes-schedule": string; + /** + * 予約投稿を作成・削除する + */ + "write:notes-schedule": string; /** * 通知を見る */ @@ -10656,6 +10700,10 @@ export interface Locale extends ILocale { * {n}人がリノートしました */ "renotedBySomeUsers": ParameterizedString<"n">; + /** + * {n}件の新しい投稿があります + */ + "notedBySomeUsers": ParameterizedString<"n">; /** * {n}人にフォローされました */ @@ -11430,7 +11478,7 @@ export interface Locale extends ILocale { */ "title": string; /** - * CFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。 + * MFCなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。 */ "description": string; }; diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 760df2809a..3004187a2d 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -530,8 +530,8 @@ showNoteActionsOnlyHover: "Mostra le azioni delle Note solo al passaggio del mou showReactionsCount: "Visualizza il numero di reazioni su una nota" noHistory: "Nessuna cronologia" signinHistory: "Storico degli accessi al profilo" -enableAdvancedMfm: "Attiva CFM avanzati" -enableAnimatedMfm: "Attiva CFM animati" +enableAdvancedMfm: "Attiva MFC avanzati" +enableAnimatedMfm: "Attiva MFC animati" doing: "In corso..." category: "Categoria" tags: "Tag" @@ -1249,7 +1249,7 @@ overwriteContentConfirm: "Vuoi davvero sostituire l'attuale contenuto?" seasonalScreenEffect: "Schermate in base alla stagione" decorate: "Decora" addMfmFunction: "Aggiungi decorazioni" -enableQuickAddMfmFunction: "Attiva il selettore di funzioni CFM" +enableQuickAddMfmFunction: "Attiva il selettore di funzioni MFC" bubbleGame: "Bubble Game" sfx: "Effetti sonori" soundWillBePlayed: "Con musica ed effetti sonori" @@ -1890,9 +1890,9 @@ _displayOfSensitiveMedia: respect: "Nascondere i media espliciti" ignore: "Non nascondere i media espliciti" force: "Nascondi tutti i media" -_cfm: - cheatSheet: "Bigliettino CFM" - intro: "CFM è un linguaggio Markdown particolare che si può usare in diverse parti di CherryPick. Qui puoi visualizzare a colpo d'occhio tutta la sintassi CFM utile." +_mfc: + cheatSheet: "Bigliettino MFC" + intro: "MFC è un linguaggio Markdown particolare che si può usare in diverse parti di CherryPick. Qui puoi visualizzare a colpo d'occhio tutta la sintassi MFC utile." dummy: "Il Fediverso si espande con CherryPick" mention: "Menzioni" mentionDescription: "Si può menzionare un utente specifico digitando il suo nome utente subito dopo il segno @." diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1a04e72f88..6c80c55e74 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1,5 +1,12 @@ _lang_: "日本語" +youBlocked: "ブロックされています" +youBlockedDescription: "{user}さんのフォローやポストの表示はできません。" +schedulePost: "予約投稿" +schedulePostList: "予約投稿一覧" +welcomeBackToast: "一定時間が経過した後に接続したときに歓迎メッセージを表示" +invalidTextLengthError: "入力された文字数が多すぎます" +invalidTextLengthDescription: "文字数が{limitValue}文字に制限されています。現在入力された文字数は{value}文字です。" autoLoadMoreReplies: "返信を自動でもっと見る" autoLoadMoreConversation: "会話を自動でもっと見る" useAutoTranslate: "自動翻訳" @@ -168,6 +175,7 @@ receiveFollowRequest: "フォローリクエストされました" followRequestAccepted: "フォローが承認されました" mention: "メンション" mentions: "あなた宛て" +newNotes: "新規投稿" directNotes: "ダイレクト投稿" importAndExport: "インポートとエクスポート" import: "インポート" @@ -634,10 +642,10 @@ showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表 showReactionsCount: "ノートのリアクション数を表示する" noHistory: "履歴はありません" signinHistory: "ログイン履歴" -enableAdvancedMfm: "高度なCFMを有効にする" -enableAdvancedMfmDescription: "有効にすると、動きのあるCFMのようなさまざまなCFM機能が使用できます。" -enableAnimatedMfm: "動きのあるCFMを有効にする" -enableAnimatedMfmDescription: "有効にすると、CFM文法または絵文字を使用するテキストが動きます。" +enableAdvancedMfm: "高度なMFCを有効にする" +enableAdvancedMfmDescription: "有効にすると、動きのあるMFCのようなさまざまなMFC機能が使用できます。" +enableAnimatedMfm: "動きのあるMFCを有効にする" +enableAnimatedMfmDescription: "有効にすると、MFC文法または絵文字を使用するテキストが動きます。" doing: "やっています" category: "カテゴリ" tags: "タグ" @@ -1173,7 +1181,7 @@ thisPostMayBeAnnoyingCancel: "やめる" thisPostMayBeAnnoyingIgnore: "このまま投稿" collapseRenotes: "リノートのスマート省略" collapseRenotesDescription: "リアクションやリノートをしたことがあるノートをたたんで表示します。" -collapseDefault: "特定のCFM構文を含むノートを省略して表示" +collapseDefault: "特定のMFC構文を含むノートを省略して表示" internalServerError: "サーバー内部エラー" internalServerErrorDescription: "サーバー内部で予期しないエラーが発生しました。" copyErrorInfo: "エラー情報をコピー" @@ -1370,7 +1378,7 @@ overwriteContentConfirm: "現在の内容に上書きされますがよろしい seasonalScreenEffect: "季節に応じた画面の演出" decorate: "デコる" addMfmFunction: "装飾を追加" -enableQuickAddMfmFunction: "高度なCFMのピッカーを表示する" +enableQuickAddMfmFunction: "高度なMFCのピッカーを表示する" bubbleGame: "バブルゲーム" sfx: "効果音" soundWillBePlayed: "サウンドが再生されます" @@ -1552,7 +1560,7 @@ _initialAccountSetting: privacySetting: "プライバシー設定" fontSizeSetting: "フォントサイズ設定" blurEffectsSetting: "ぼかし効果設定" - mfmAndAnimatedImagesSetting: "CFMとアニメーション画像設定" + mfmAndAnimatedImagesSetting: "MFCとアニメーション画像設定" theseSettingsCanEditLater: "これらの設定は後から変更できます。" youCanEditMoreSettingsInSettingsPageLater: "この他にも様々な設定を「設定」ページから行えます。ぜひ後で確認してみてください。" followUsers: "タイムラインを構築するため、気になるユーザーをフォローしてみましょう。" @@ -1995,6 +2003,7 @@ _role: ltlAvailable: "ローカルタイムラインの閲覧" canPublicNote: "パブリック投稿の許可" canEditNote: "ノートの編集" + scheduleNoteMax: "予約投稿の最大数" mentionMax: "ノート内の最大メンション数" canInvite: "サーバー招待コードの発行" inviteLimit: "招待コードの作成可能数" @@ -2173,9 +2182,9 @@ _displayOfSensitiveMedia: ignore: "センシティブ設定されたメディアを隠さない" force: "常にメディアを隠す" -_cfm: - cheatSheet: "CFMチートシート" - intro: "CFMは、CherryPick内の様々な場所で使用できる専用のマークアップ言語です。ここでは、CFMで使用可能な構文一覧が確認できます。" +_mfc: + cheatSheet: "MFCチートシート" + intro: "MFCは、CherryPick内の様々な場所で使用できる専用のマークアップ言語です。ここでは、MFCで使用可能な構文一覧が確認できます。" dummy: "CherryPickでFediverseの世界が広がります" mention: "メンション" mentionDescription: "アットマーク + ユーザー名で、特定のユーザーを示すことができます。" @@ -2458,6 +2467,8 @@ _permissions: "read:mutes": "ミュートを見る" "write:mutes": "ミュートを操作する" "write:notes": "ノートを作成・削除する" + "read:notes-schedule": "予約投稿を見る" + "write:notes-schedule": "予約投稿を作成・削除する" "read:notifications": "通知を見る" "write:notifications": "通知を操作する" "read:reactions": "リアクションを見る" @@ -2808,6 +2819,7 @@ _notification: reactedBySomeUsers: "{n}人がリアクションしました" likedBySomeUsers: "{n}人がいいねしました" renotedBySomeUsers: "{n}人がリノートしました" + notedBySomeUsers: "{n}件の新しい投稿があります" followedBySomeUsers: "{n}人にフォローされました" flushNotification: "通知の履歴をリセットする" exportOfXCompleted: "{x}のエクスポートが完了しました" @@ -3032,7 +3044,7 @@ _dataSaver: description: "URLプレビューのサムネイル画像が読み込まれなくなります。" _code: title: "コードハイライトを非表示" - description: "CFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。" + description: "MFCなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。" _hemisphere: N: "北半球" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 59c8f5e778..3db81019d2 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -525,8 +525,8 @@ showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示す showReactionsCount: "ノートのリアクション数を表示する" noHistory: "履歴はないわ。" signinHistory: "ログイン履歴" -enableAdvancedMfm: "ややこしいCFMもありにする" -enableAnimatedMfm: "動きがやかましいCFMも許したる" +enableAdvancedMfm: "ややこしいMFCもありにする" +enableAnimatedMfm: "動きがやかましいMFCも許したる" doing: "やっとるがな" category: "カテゴリ" tags: "タグ" @@ -1242,7 +1242,7 @@ overwriteContentConfirm: "今の内容に上書きされるけどいい?" seasonalScreenEffect: "季節にあった画面の動き" decorate: "デコる" addMfmFunction: "装飾つける" -enableQuickAddMfmFunction: "ややこしいCFMのピッカーを出す" +enableQuickAddMfmFunction: "ややこしいMFCのピッカーを出す" bubbleGame: "バブルゲーム" sfx: "効果音" soundWillBePlayed: "サウンドが再生されるで" @@ -1865,9 +1865,9 @@ _displayOfSensitiveMedia: respect: "きわどいのは見とうない" ignore: "きわどいのも見たい" force: "常にメディアを隠すで" -_cfm: - cheatSheet: "CFMチートシート" - intro: "CFMは、CherryPick内の色んな所で使える専用のマークアップ言語やで。このページでCFMで使える構文一覧が確認できるで。" +_mfc: + cheatSheet: "MFCチートシート" + intro: "MFCは、CherryPick内の色んな所で使える専用のマークアップ言語やで。このページでMFCで使える構文一覧が確認できるで。" dummy: "CherryPickでFediverseの世界が広がります" mention: "メンション" mentionDescription: "アットマーク + ユーザー名で、特定のユーザーを示すことができるで。" diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml index a178d481f3..e3adaa6b06 100644 --- a/locales/ko-GS.yml +++ b/locales/ko-GS.yml @@ -479,8 +479,8 @@ native: "기본" showNoteActionsOnlyHover: "마우스 올맀을 때만 노트 액션 버턴 보이기" noHistory: "기록이 없십니다" signinHistory: "로그인 기록" -enableAdvancedMfm: "복잡한 CFM 키기" -enableAnimatedMfm: "정신사나운 CFM 키기" +enableAdvancedMfm: "복잡한 MFC 키기" +enableAnimatedMfm: "정신사나운 MFC 키기" doing: "잠만예" category: "카테고리" tags: "태그" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index e100d158c3..ae20b9804f 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1,5 +1,12 @@ --- _lang_: "한국어" +youBlocked: "앗.. 차단당했어요.." +youBlockedDescription: "{user} 님을 팔로우하거나 해당 사용자의 게시물을 볼 수 없어요." +schedulePost: "노트 게시 예약" +schedulePostList: "노트 게시 예약 목록" +welcomeBackToast: "일정 시간이 지난 후 접속했을 때 환영 메시지 표시" +invalidTextLengthError: "입력된 문자수가 너무 많아요" +invalidTextLengthDescription: "문자수가 {limitValue}자로 제한되어 있어요. 현재 입력된 문자수는 {value}자예요." autoLoadMoreReplies: "답글을 자동으로 더 보기" autoLoadMoreConversation: "대화를 자동으로 더 보기" useAutoTranslate: "자동 번역" @@ -22,7 +29,7 @@ showReplyTargetNoteInSemiTransparent: "답글 대상 노트를 반투명하게 noteFooterButton: "노트 동작 버튼" collapseReplies: "답글로 작성된 노트 간략화하기" collapseRepliesDescription: "답글로 작성된 노트를 접어서 표시해요.\n리액션한 노트는 영향을 받지 않아요." -repliedBy: "{user}님이 답글을 작성했어요" +repliedBy: "{user} 님이 답글을 작성했어요" collapseLongNoteContent: "내용이 긴 노트 간략화하기" alwaysShowCw: "'내용 가리기'로 설정한 내용을 항상 보이기" forceRenoteVisibilitySelector: "리노트 공개 범위 지정" @@ -109,7 +116,7 @@ gotIt: "알겠어요" cancel: "취소" noThankYou: "나중에" enterUsername: "사용자 이름 입력" -renotedBy: "{user}님이 리노트 했어요" +renotedBy: "{user} 님이 리노트 했어요" noNotes: "노트가 없어요" noNotifications: "표시할 알림이 없어요" instance: "서버" @@ -168,6 +175,7 @@ receiveFollowRequest: "새로운 팔로우 요청이 있어요" followRequestAccepted: "팔로우가 수락되었어요" mention: "멘션" mentions: "받은 멘션" +newNotes: "새 노트" directNotes: "다이렉트 노트" importAndExport: "가져오기 및 내보내기" import: "가져오기" @@ -634,10 +642,10 @@ showNoteActionsOnlyHover: "노트에 커서를 올렸을 때에만 노트 동작 showReactionsCount: "노트의 리액션 수 표시하기" noHistory: "기록이 없어요" signinHistory: "로그인 기록" -enableAdvancedMfm: "고급 CFM 활성화" -enableAdvancedMfmDescription: "활성화하면 움직임이 있는 CFM과 같은 다양한 CFM 기능을 사용할 수 있게 돼요." -enableAnimatedMfm: "움직임이 있는 CFM 활성화" -enableAnimatedMfmDescription: "활성화하면 CFM 문법을 사용한 텍스트나 이모지가 움직이게 돼요." +enableAdvancedMfm: "고급 MFC 활성화" +enableAdvancedMfmDescription: "활성화하면 움직임이 있는 MFC과 같은 다양한 MFC 기능을 사용할 수 있게 돼요." +enableAnimatedMfm: "움직임이 있는 MFC 활성화" +enableAnimatedMfmDescription: "활성화하면 MFC 문법을 사용한 텍스트나 이모지가 움직이게 돼요." doing: "잠시만 기다려 주세요" category: "카테고리" tags: "태그" @@ -979,7 +987,7 @@ administration: "관리" accounts: "계정" switch: "전환" noMaintainerInformationWarning: "관리자 정보를 아직 설정하지 않았어요." -noInquiryUrlWarning: "문의처 주소를 아직 설정하지 않았어요." +noInquiryUrlWarning: "문의처 URL을 아직 설정하지 않았어요." noBotProtectionWarning: "봇 방어가 설정되지 않았어요." configure: "설정하기" postToGallery: "갤러리에 업로드" @@ -1173,7 +1181,7 @@ thisPostMayBeAnnoyingCancel: "그만두기" thisPostMayBeAnnoyingIgnore: "이대로 게시" collapseRenotes: "이미 본 리노트를 간략화하기" collapseRenotesDescription: "리액션이나 리노트한 노트를 접어서 표시해요." -collapseDefault: "특정 CFM 구문이 포함된 노트 간략화하기" +collapseDefault: "특정 MFC 구문이 포함된 노트 간략화하기" internalServerError: "내부 서버 오류" internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했어요." copyErrorInfo: "오류 정보 복사" @@ -1370,7 +1378,7 @@ overwriteContentConfirm: "현재 내용을 덮어쓰기 하게 돼요. 그래도 seasonalScreenEffect: "계절에 따른 화면 연출" decorate: "장식하기" addMfmFunction: "장식 추가" -enableQuickAddMfmFunction: "고급 CFM 선택기 표시" +enableQuickAddMfmFunction: "고급 MFC 선택기 표시" bubbleGame: "버블 게임" sfx: "효과음" soundWillBePlayed: "사운드가 재생돼요" @@ -1539,7 +1547,7 @@ _initialAccountSetting: privacySetting: "프라이버시 설정" fontSizeSetting: "글자 크기 설정" blurEffectsSetting: "흐림 효과 설정" - mfmAndAnimatedImagesSetting: "CFM 및 움직이는 이미지 설정" + mfmAndAnimatedImagesSetting: "MFC 및 움직이는 이미지 설정" theseSettingsCanEditLater: "이 설정들은 나중에도 변경할 수 있어요." youCanEditMoreSettingsInSettingsPageLater: "이 외에도 '설정' 페이지에서 다양한 설정을 나의 입맛에 맞게 조절할 수 있으니 꼭 확인해 보세요!" followUsers: "관심사가 맞는 사람을 팔로우하여 타임라인을 가꾸어 보세요." @@ -1974,6 +1982,7 @@ _role: ltlAvailable: "로컬 타임라인 보이기" canPublicNote: "공개 노트 허용" canEditNote: "노트 편집 허용" + scheduleNoteMax: "게시를 예약한 노트의 최대 수" mentionMax: "노트에서 언급할 수 있는 멘션 수" canInvite: "서버 초대 코드 발행" inviteLimit: "초대 한도" @@ -2136,9 +2145,9 @@ _displayOfSensitiveMedia: respect: "민감한 콘텐츠로 표시된 미디어 가리기" ignore: "민감한 콘텐츠로 표시된 미디어 보이기" force: "미디어 항상 가리기" -_cfm: - cheatSheet: "CFM 도움말" - intro: "CFM는 CherryPick 클라이언트의 다양한 곳에서 사용할 수 있는 전용 마크업 언어에요. 여기에서 CFM에서 사용할 수 있는 구문을 확인할 수 있어요." +_mfc: + cheatSheet: "MFC 도움말" + intro: "MFC는 CherryPick 클라이언트의 다양한 곳에서 사용할 수 있는 전용 마크업 언어에요. 여기에서 MFC에서 사용할 수 있는 구문을 확인할 수 있어요." dummy: "CherryPick으로 연합우주의 세계가 펼쳐집니다" mention: "멘션" mentionDescription: "골뱅이표(@) 뒤에 사용자 이름을 넣어 특정 사용자를 지정할 수 있어요." @@ -2211,7 +2220,7 @@ _cfm: bg: "배경색" bgDescription: "지정한 값으로 배경색을 지정해요." plain: "평문" - plainDescription: "안에 있는 CFM 구문을 모두 무시하고 평문으로 표시해요." + plainDescription: "안에 있는 MFC 구문을 모두 무시하고 평문으로 표시해요." ruby: "루비" rubyDescription: "글자 위에 루비를 표시해요." _instanceTicker: @@ -2406,6 +2415,8 @@ _permissions: "read:mutes": "뮤트 여부를 확인합니다" "write:mutes": "뮤트를 하거나 해제합니다" "write:notes": "노트를 작성하거나 삭제합니다" + "read:notes-schedule": "게시를 예약한 노트를 봅니다" + "write:notes-schedule": "노트 게시를 예약하거나 삭제합니다" "read:notifications": "알림을 확인합니다" "write:notifications": "알림을 모두 읽음 처리합니다" "read:reactions": "리액션을 확인합니다" @@ -2738,6 +2749,7 @@ _notification: reactedBySomeUsers: "{n}명이 리액션했어요" likedBySomeUsers: "{n}명이 좋아요를 눌렀어요" renotedBySomeUsers: "{n}명이 리노트했어요" + notedBySomeUsers: "{n}개의 새 노트가 있어요" followedBySomeUsers: "{n}명에게 팔로우됨" flushNotification: "모든 알림 지우기" exportOfXCompleted: "{x} 내보내기에 성공했어요." @@ -2949,7 +2961,7 @@ _dataSaver: description: "URL 미리보기의 썸네일 이미지를 불러오지 않아요." _code: title: "코드 문법 강조" - description: "CFM 등에서 코드 문법 강조 기법을 사용할 때, 클릭하기 전까지는 불러오지 않아요. 코드 문법 강조 기능은 강조할 언어마다 해당 정의 파일을 불러와야 하지만, 이를 자동으로 불러오지 않게 되어 데이터 사용량을 줄일 수 있어요." + description: "MFC 등에서 코드 문법 강조 기법을 사용할 때, 클릭하기 전까지는 불러오지 않아요. 코드 문법 강조 기능은 강조할 언어마다 해당 정의 파일을 불러와야 하지만, 이를 자동으로 불러오지 않게 되어 데이터 사용량을 줄일 수 있어요." _hemisphere: N: "북반구" S: "남반구" @@ -3026,7 +3038,7 @@ _contextMenu: appWithShift: "Shift 키로 애플리케이션" native: "브라우저의 UI" _embedCodeGen: - title: "임베디드 코드를 커스터마이즈" + title: "임베디드 코드 사용자화" header: "해더를 표시" autoload: "자동으로 다음 코드를 실행 (비권장)" maxHeight: "최대 높이" @@ -3036,7 +3048,7 @@ _embedCodeGen: rounded: "외곽선을 둥글게 하기" border: "외곽선에 테두리를 씌우기" applyToPreview: "미리보기에 반영" - generateCode: "임베디드 코드를 만들기" + generateCode: "임베디드 코드 만들기" codeGenerated: "코드를 만들었어요!" codeGeneratedDescription: "만들어진 코드를 웹 사이트에 붙여서 사용해 주세요." _abuse: diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 40e59ca37d..53b2076092 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -508,8 +508,8 @@ showNoteActionsOnlyHover: "Pokazuj akcje notatek tylko po najechaniu myszką" showReactionsCount: "Wyświetl liczbę reakcji na notatkę" noHistory: "Brak historii" signinHistory: "Historia logowania" -enableAdvancedMfm: "Włącz zaawansowane CFM" -enableAnimatedMfm: "Włącz animowane CFM" +enableAdvancedMfm: "Włącz zaawansowane MFC" +enableAnimatedMfm: "Włącz animowane MFC" doing: "Przetwarzanie..." category: "Kategoria" tags: "Tagi" @@ -1132,9 +1132,9 @@ _aboutMisskey: donate: "Przekaż darowiznę na Misskey" morePatrons: "Naprawdę doceniam wsparcie ze strony wielu niewymienionych tu osób. Dziękuję! 🥰" patrons: "Wspierający" -_cfm: - cheatSheet: "Ściąga CFM" - intro: "CFM to język składniowy wyjątkowy dla CherryPick, który może być użyty w wielu miejscach. Tu znajdziesz listę wszystkich możliwych elementów składni CFM." +_mfc: + cheatSheet: "Ściąga MFC" + intro: "MFC to język składniowy wyjątkowy dla CherryPick, który może być użyty w wielu miejscach. Tu znajdziesz listę wszystkich możliwych elementów składni MFC." dummy: "CherryPick rozszerza świat Fediwersum" mention: "Wspomnij" mentionDescription: "Używając znaku @ i nazwy użytkownika, możesz określić danego użytkownika." @@ -1193,7 +1193,7 @@ _cfm: rotate: "Obróć" rotateDescription: "Obraca zawartość o określony kąt." plain: "Zwyczajny" - plainDescription: "Wyłącza efekty wszystkich CFM zawartych w tym efekcie CFM." + plainDescription: "Wyłącza efekty wszystkich MFC zawartych w tym efekcie MFC." _instanceTicker: none: "Nigdy nie pokazuj" remote: "Pokaż dla zdalnych użytkowników" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 743ed96d1d..75b97f2bef 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -513,8 +513,8 @@ showNoteActionsOnlyHover: "Exibir as ações da nota somente ao passar o cursor showReactionsCount: "Ver o número de reações nas notas" noHistory: "Ainda não há histórico" signinHistory: "Histórico de acesso" -enableAdvancedMfm: "Habilitar CFM avançado" -enableAnimatedMfm: "Habilitar CFM animado" +enableAdvancedMfm: "Habilitar MFC avançado" +enableAnimatedMfm: "Habilitar MFC animado" doing: "Processando..." category: "Categoria" tags: "Etiquetas" @@ -1227,8 +1227,8 @@ remainingN: "Restante: {n}" overwriteContentConfirm: "Você tem certeza de que deseja sobrescrever o conteúdo atual?" seasonalScreenEffect: "Efeito de Tela Sazonal" decorate: "Decorar" -addMfmFunction: "Adicionar CFM" -enableQuickAddMfmFunction: "Exibir seleção avançada de CFM" +addMfmFunction: "Adicionar MFC" +enableQuickAddMfmFunction: "Exibir seleção avançada de MFC" bubbleGame: "Bubble Game" sfx: "Efeitos Sonoros" soundWillBePlayed: "Sons serão reproduzidos" @@ -2566,7 +2566,7 @@ _dataSaver: description: "Miniaturas na prévia de URLs não serão mais carregadas." _code: title: "Destaque de código" - description: "Se as notações de formatação de código forem utilizadas em CFM, elas não irão carregar até serem selecionadas. Destaque de código exige baixar arquivos de alta definição para cada linguagem de programação. Logo, desabilitar o carregamento automático desses arquivos diminui a quantidade de informação comunicada." + description: "Se as notações de formatação de código forem utilizadas em MFC, elas não irão carregar até serem selecionadas. Destaque de código exige baixar arquivos de alta definição para cada linguagem de programação. Logo, desabilitar o carregamento automático desses arquivos diminui a quantidade de informação comunicada." _hemisphere: N: "Hemisfério Norte" S: "Hemisfério Sul" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index b49e81f24e..0c45806b0c 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -519,8 +519,8 @@ showNoteActionsOnlyHover: "Показывать кнопки у заметок showReactionsCount: "Видеть количество реакций на заметках" noHistory: "История пока пуста" signinHistory: "Журнал посещений" -enableAdvancedMfm: "Включить расширенный CFM" -enableAnimatedMfm: "Включить анимированную разметку CFM" +enableAdvancedMfm: "Включить расширенный MFC" +enableAnimatedMfm: "Включить анимированную разметку MFC" doing: "В процессе" category: "Категория" tags: "Метки" @@ -1178,7 +1178,7 @@ code: "Код" remainingN: "Остаётся: {n}" seasonalScreenEffect: "Эффект времени года на экране" decorate: "Украсить" -addMfmFunction: "Добавить CFM" +addMfmFunction: "Добавить MFC" lastNDays: "Последние {n} сут" hemisphere: "Место проживания" enableHorizontalSwipe: "Смахните в сторону, чтобы сменить вкладки" @@ -1619,9 +1619,9 @@ _displayOfSensitiveMedia: respect: "Скрывать содержимое не для всех" ignore: "Показывать содержимое не для всех" force: "Скрывать всё содержимое" -_cfm: - cheatSheet: "Подсказка по разметке CFM" - intro: "CFM — язык оформления текста, который придуман специально для CherryPick и готов для применения во многих местах. На этой странице собраны и кратко изложены способы его использовать." +_mfc: + cheatSheet: "Подсказка по разметке MFC" + intro: "MFC — язык оформления текста, который придуман специально для CherryPick и готов для применения во многих местах. На этой странице собраны и кратко изложены способы его использовать." dummy: "CherryPick расширяет границы Федиверса." mention: "Упоминание" mentionDescription: "При помощи знака «собака» перед именем можно упомянуть какого-нибудь пользователя." diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index f85f922e08..35bd11fcac 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -469,8 +469,8 @@ joinOrCreateGroup: "Požiadajte o pozvanie do existujúcej skupiny alebo vytvort showNoteActionsOnlyHover: "Ovládacie prvky poznámky sa zobrazujú len po nabehnutí myši" noHistory: "Žiadna história" signinHistory: "História prihlásení" -enableAdvancedMfm: "Povolenie pokročilého CFM" -enableAnimatedMfm: "Povoliť animované CFM" +enableAdvancedMfm: "Povolenie pokročilého MFC" +enableAnimatedMfm: "Povoliť animované MFC" doing: "Pracujem..." category: "Kategórie" tags: "Značky" @@ -1030,9 +1030,9 @@ _aboutMisskey: donate: "Podporiť Misskey" morePatrons: "Takisto oceňujeme podporu mnoých ďalších, ktorí tu nie sú uvedení. Ďakujeme! 🥰" patrons: "Prispievatelia" -_cfm: +_mfc: cheatSheet: "MFM Cheatsheet" - intro: "CFM je CherryPick exkluzívny značkovací jazyk, ktorý sa dá používať na viacerých miestach. Tu môžete vidieť zoznam všetkej dostupnej CFM syntaxe." + intro: "MFC je CherryPick exkluzívny značkovací jazyk, ktorý sa dá používať na viacerých miestach. Tu môžete vidieť zoznam všetkej dostupnej MFC syntaxe." dummy: "CherryPick rozširuje svet Fediverza" mention: "Zmienka" mentionDescription: "Používateľa spomeniete použítím zavináča a mena používateľa" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index b91ca8d5b0..e95bcfa41b 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -525,8 +525,8 @@ showNoteActionsOnlyHover: "แสดงการดำเนินการโ showReactionsCount: "แสดงจำนวนรีแอกชั่นในโน้ต" noHistory: "ไม่มีประวัติ" signinHistory: "ประวัติการเข้าสู่ระบบ" -enableAdvancedMfm: "เปิดใช้งาน CFM ขั้นสูง" -enableAnimatedMfm: "เปิดการใช้งาน CFM แบบเคลื่อนไหว" +enableAdvancedMfm: "เปิดใช้งาน MFC ขั้นสูง" +enableAnimatedMfm: "เปิดการใช้งาน MFC แบบเคลื่อนไหว" doing: "กำลังประมวลผล......" category: "หมวดหมู่" tags: "นามแฝง" @@ -1242,7 +1242,7 @@ overwriteContentConfirm: "แน่ใจหรือไม่ว่าต้อ seasonalScreenEffect: "เอฟเฟกต์หน้าจอตามฤดูกาล" decorate: "ตกแต่ง" addMfmFunction: "เพิ่มการตกแต่ง" -enableQuickAddMfmFunction: "แสดงตัวจิ้มเลือก CFM ขั้นสูง" +enableQuickAddMfmFunction: "แสดงตัวจิ้มเลือก MFC ขั้นสูง" bubbleGame: "เกมบับเบิ้ล" sfx: "เสียงเอฟเฟ็กต์" soundWillBePlayed: "จะมีการเล่นเอฟเฟกต์เสียง" @@ -1866,9 +1866,9 @@ _displayOfSensitiveMedia: respect: "ซ่อนสื่อที่มีเนื้อหาละเอียดอ่อน" ignore: "แสดงสื่อที่มีเนื้อหาละเอียดอ่อน" force: "ซ่อนสื่อทั้งหมด" -_cfm: - cheatSheet: "โค้ด CFM Cheat Sheet" - intro: "CFM เป็นภาษามาร์กอัปพิเศษเฉพาะของ CherryPick ที่สามารถใช้ได้ในหลายที่ คุณยังสามารถดูรายการไวยากรณ์ CFM ที่มีอยู่ทั้งหมดได้ที่นี่นะ" +_mfc: + cheatSheet: "โค้ด MFC Cheat Sheet" + intro: "MFC เป็นภาษามาร์กอัปพิเศษเฉพาะของ CherryPick ที่สามารถใช้ได้ในหลายที่ คุณยังสามารถดูรายการไวยากรณ์ MFC ที่มีอยู่ทั้งหมดได้ที่นี่นะ" dummy: "CherryPick ขยายโลกของ Fediverse" mention: "กล่าวถึง" mentionDescription: "คุณสามารถระบุผู้ใช้โดยใช้ At-Symbol และชื่อผู้ใช้ได้นะ" @@ -1931,7 +1931,7 @@ _cfm: rotate: "หมุนหน้าจอ" rotateDescription: "เปลี่ยนเนื้อหาตามด้วยมุมที่ระบุไว้" plain: "เรียบง่าย" - plainDescription: "ปิดการใช้งานเอฟเฟกต์ของ CFM ทั้งหมดที่มีอยู่ในเอฟเฟกต์ CFM นี้" + plainDescription: "ปิดการใช้งานเอฟเฟกต์ของ MFC ทั้งหมดที่มีอยู่ในเอฟเฟกต์ MFC นี้" _instanceTicker: none: "ไม่ต้องแสดง" remote: "แสดงสำหรับผู้ใช้ระยะไกล" @@ -2648,7 +2648,7 @@ _dataSaver: description: "ธัมบ์เนลแสดงตัวอย่าง URL จะไม่โหลดโดยอัตโนมัติ" _code: title: "ไฮไลต์โค้ด" - description: "หากใช้สัญลักษณ์ไฮไลต์โค้ดใน CFM ฯลฯ สัญลักษณ์เหล่านั้นจะไม่โหลดจนกว่าจะแตะ การไฮไลต์ไวยากรณ์(syntax)จำเป็นต้องดาวน์โหลดไฟล์คำจำกัดความของไฮไลต์สำหรับแต่ละภาษา ดังนั้นการปิดใช้งานการโหลดไฟล์เหล่านี้โดยอัตโนมัติจึงคาดว่าจะช่วยลดปริมาณข้อมูลการสื่อสารได้" + description: "หากใช้สัญลักษณ์ไฮไลต์โค้ดใน MFC ฯลฯ สัญลักษณ์เหล่านั้นจะไม่โหลดจนกว่าจะแตะ การไฮไลต์ไวยากรณ์(syntax)จำเป็นต้องดาวน์โหลดไฟล์คำจำกัดความของไฮไลต์สำหรับแต่ละภาษา ดังนั้นการปิดใช้งานการโหลดไฟล์เหล่านี้โดยอัตโนมัติจึงคาดว่าจะช่วยลดปริมาณข้อมูลการสื่อสารได้" _hemisphere: N: "ซีกโลกเหนือ" S: "ซีกโลกใต้" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index bb518f8bf0..3c6b4a0b48 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -466,8 +466,8 @@ youHaveNoGroups: "Немає груп" joinOrCreateGroup: "Отримуйте запрошення до груп або створюйте свої власні групи." noHistory: "Історія порожня" signinHistory: "Історія входів" -enableAdvancedMfm: "Увімкнути розширений CFM" -enableAnimatedMfm: "Увімкнути анімований CFM" +enableAdvancedMfm: "Увімкнути розширений MFC" +enableAnimatedMfm: "Увімкнути анімований MFC" doing: "Виконується" category: "Категорія" tags: "Теги" @@ -1234,9 +1234,9 @@ _aboutMisskey: donate: "Пожертвувати Misskey" morePatrons: "Ми дуже цінуємо підтримку багатьох інших помічників, не перелічених тут. Дякуємо! 🥰" patrons: "Підтримали" -_cfm: - cheatSheet: " Довідка CFM" - intro: "CFM це ексклюзивна мова розмітки тексту в CherryPick, яку можна використовувати в багатьох місцях. Тут ви можете переглянути приклади її синтаксису." +_mfc: + cheatSheet: " Довідка MFC" + intro: "MFC це ексклюзивна мова розмітки тексту в CherryPick, яку можна використовувати в багатьох місцях. Тут ви можете переглянути приклади її синтаксису." dummy: "CherryPick розширює світ Федіверсу" mention: "Згадка" mentionDescription: "За допомогою знака \"@\" перед ім'ям можна згадати конкретного користувача." @@ -1292,7 +1292,7 @@ _cfm: fontDescription: "Встановлює шрифт для контенту." rotate: "Обертати" plain: "Звичайний" - plainDescription: "Деактивує всі ефекти CFM, що містяться в цьому ефекті CFM." + plainDescription: "Деактивує всі ефекти MFC, що містяться в цьому ефекті MFC." _instanceTicker: none: "Не відображати" remote: "Відображати для віддалених користувачів" diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml index a27d41a8c8..e1c522d745 100644 --- a/locales/uz-UZ.yml +++ b/locales/uz-UZ.yml @@ -474,7 +474,7 @@ native: "Mahalliy" showNoteActionsOnlyHover: "Eslatma amallarini faqat sichqonchani olib borganda ko‘rsatish" noHistory: "Tarix yo'q" signinHistory: "kirish tarixi" -enableAdvancedMfm: "CFMni faollashtirish" +enableAdvancedMfm: "MFCni faollashtirish" doing: "Bajarilmoqda..." category: "kategoriya" tags: "teg" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 6cf2a57134..802f177241 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -501,8 +501,8 @@ joinOrCreateGroup: "Tham gia hoặc tạo một nhóm mới." showNoteActionsOnlyHover: "Chỉ hiển thị các hành động ghi chú khi di chuột" noHistory: "Không có dữ liệu" signinHistory: "Lịch sử đăng nhập" -enableAdvancedMfm: "Xem bài CFM chất lượng cao." -enableAnimatedMfm: "Xem bài CFM có chuyển động" +enableAdvancedMfm: "Xem bài MFC chất lượng cao." +enableAnimatedMfm: "Xem bài MFC có chuyển động" doing: "Đang xử lý..." category: "Phân loại" tags: "Thẻ" @@ -1471,9 +1471,9 @@ _aboutMisskey: donate: "Ủng hộ Misskey" morePatrons: "Chúng tôi cũng trân trọng sự hỗ trợ của nhiều người đóng góp khác không được liệt kê ở đây. Cảm ơn! 🥰" patrons: "Người ủng hộ" -_cfm: - cheatSheet: "CFM Cheatsheet" - intro: "CFM là ngôn ngữ phát triển độc quyền của CherryPick có thể được sử dụng ở nhiều nơi. Tại đây bạn có thể xem danh sách tất cả các cú pháp CFM có sẵn." +_mfc: + cheatSheet: "MFC Cheatsheet" + intro: "MFC là ngôn ngữ phát triển độc quyền của CherryPick có thể được sử dụng ở nhiều nơi. Tại đây bạn có thể xem danh sách tất cả các cú pháp MFC có sẵn." dummy: "CherryPick mở rộng thế giới Fediverse" mention: "Nhắc đến" mentionDescription: "Bạn có thể nhắc đến ai đó bằng cách sử dụng @tên người dùng." @@ -1536,7 +1536,7 @@ _cfm: rotate: "Xoay" rotateDescription: "Xoay nội dung theo một góc cụ thể." plain: "Đơn giản" - plainDescription: "Vô hiệu hóa mọi hiệu ứng CFM chứa trong hiệu ứng CFM này." + plainDescription: "Vô hiệu hóa mọi hiệu ứng MFC chứa trong hiệu ứng MFC này." _instanceTicker: none: "Không hiển thị" remote: "Hiện cho người dùng từ máy chủ khác" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 3ae7b9f126..d1728d8c11 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -529,8 +529,8 @@ showNoteActionsOnlyHover: "仅在悬停时显示帖子操作" showReactionsCount: "显示帖子的回应数" noHistory: "没有历史记录" signinHistory: "登录历史" -enableAdvancedMfm: "启用扩展 CFM" -enableAnimatedMfm: "启用 CFM 动画" +enableAdvancedMfm: "启用扩展 MFC" +enableAnimatedMfm: "启用 MFC 动画" doing: "正在进行" category: "类别" tags: "标签" @@ -1248,7 +1248,7 @@ overwriteContentConfirm: "将覆盖现有内容。确定吗?" seasonalScreenEffect: "符合当前季节的画面效果" decorate: "装饰" addMfmFunction: "添加装饰" -enableQuickAddMfmFunction: "显示高级 CFM 选择器" +enableQuickAddMfmFunction: "显示高级 MFC 选择器" bubbleGame: "泡泡游戏" sfx: "音效" soundWillBePlayed: "声音将会播放" @@ -1887,9 +1887,9 @@ _displayOfSensitiveMedia: respect: "隐藏敏感媒体" ignore: "显示敏感媒体" force: "隐藏所有内容" -_cfm: - cheatSheet: "CFM代码速查表" - intro: "CFM是一种在CherryPick中的各个位置使用的专用标记语言。在这里您可以看到CFM中可用的语法列表。" +_mfc: + cheatSheet: "MFC代码速查表" + intro: "MFC是一种在CherryPick中的各个位置使用的专用标记语言。在这里您可以看到MFC中可用的语法列表。" dummy: "通过CherryPick扩展联邦宇宙的世界" mention: "提及" mentionDescription: "可以使用 @+用户名 来指示特定用户" @@ -2679,7 +2679,7 @@ _dataSaver: description: "将不再加载 URL 预览缩略图。" _code: title: "代码高亮" - description: "如果使用了代码高亮标记,例如在 CFM 中,则在点击之前不会加载。 代码高亮要求加载每种高亮语言的定义文件,由于这些文件不再自动加载,因此有望减少数据传输量。" + description: "如果使用了代码高亮标记,例如在 MFC 中,则在点击之前不会加载。 代码高亮要求加载每种高亮语言的定义文件,由于这些文件不再自动加载,因此有望减少数据传输量。" _hemisphere: N: "北半球" S: "南半球" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 198db97aea..8e45b79991 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -532,8 +532,8 @@ showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的操作選項" showReactionsCount: "顯示貼文的反應數目" noHistory: "沒有歷史紀錄" signinHistory: "登入歷史" -enableAdvancedMfm: "啟用進階 CFM" -enableAnimatedMfm: "啟用 CFM 動畫" +enableAdvancedMfm: "啟用進階 MFC" +enableAnimatedMfm: "啟用 MFC 動畫" doing: "正在進行" category: "類別" tags: "標籤" @@ -1250,8 +1250,8 @@ remainingN: "剩餘:{n}" overwriteContentConfirm: "確定要覆蓋目前的內容嗎?" seasonalScreenEffect: "隨季節變換畫面的呈現" decorate: "設置頭像裝飾" -addMfmFunction: "插入 CFM 功能語法" -enableQuickAddMfmFunction: "顯示高級 CFM 選擇器" +addMfmFunction: "插入 MFC 功能語法" +enableQuickAddMfmFunction: "顯示高級 MFC 選擇器" bubbleGame: "氣泡遊戲" sfx: "音效" soundWillBePlayed: "將播放音效" @@ -1893,9 +1893,9 @@ _displayOfSensitiveMedia: respect: "隱藏敏感檔案" ignore: "顯示敏感檔案" force: "隱藏所有檔案" -_cfm: - cheatSheet: "CFM代碼小抄" - intro: "CFM是CherryPick專用的標記語言,可以在CherryPick中的各個位置使用。 您可以這裏看到CFM可用語法列表。" +_mfc: + cheatSheet: "MFC代碼小抄" + intro: "MFC是CherryPick專用的標記語言,可以在CherryPick中的各個位置使用。 您可以這裏看到MFC可用語法列表。" dummy: "CherryPick拓展了Fediverse的世界" mention: "提及" mentionDescription: "透過 @+用戶名 來標示特定使用者。" @@ -2688,7 +2688,7 @@ _dataSaver: description: "將不再自動載入網址預覽縮圖。" _code: title: "程式碼突出顯示" - description: "如果使用了 CFM 的程式碼突顯標記,則在點擊之前不會載入。程式碼突顯要求加載每種程式語言的突顯定義檔案,但由於這些檔案不再自動載入,因此有望減少資料流量。" + description: "如果使用了 MFC 的程式碼突顯標記,則在點擊之前不會載入。程式碼突顯要求加載每種程式語言的突顯定義檔案,但由於這些檔案不再自動載入,因此有望減少資料流量。" _hemisphere: N: "北半球" S: "南半球" diff --git a/package.json b/package.json index 4e8c1c6422..ccc82c0c73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cherrypick", - "version": "4.12.0", + "version": "4.12.1", "basedMisskeyVersion": "2024.9.0", "codename": "nasubi", "repository": { diff --git a/packages/backend/migration/1699437894737-scheduleNote.js b/packages/backend/migration/1699437894737-scheduleNote.js new file mode 100644 index 0000000000..28dc290f25 --- /dev/null +++ b/packages/backend/migration/1699437894737-scheduleNote.js @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ScheduleNote1699437894737 { + name = 'ScheduleNote1699437894737' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "note_schedule" ("id" character varying(32) NOT NULL, "note" jsonb NOT NULL, "userId" character varying(260) NOT NULL, "scheduledAt" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_3a1ae2db41988f4994268218436" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_e798958c40009bf0cdef4f28b5" ON "note_schedule" ("userId") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP TABLE "note_schedule"`); + } +} diff --git a/packages/backend/migration/1729518620697-remoteObjectStorage.js b/packages/backend/migration/1729518620697-remoteObjectStorage.js new file mode 100644 index 0000000000..ee6ddda1b1 --- /dev/null +++ b/packages/backend/migration/1729518620697-remoteObjectStorage.js @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RemoteObjectStorage1729518620697 { + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "useRemoteObjectStorage" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "meta" ADD "remoteObjectStorageBucket" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "remoteObjectStoragePrefix" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "remoteObjectStorageBaseUrl" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "remoteObjectStorageEndpoint" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "remoteObjectStorageRegion" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "remoteObjectStorageAccessKey" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "remoteObjectStorageSecretKey" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "remoteObjectStoragePort" integer`); + await queryRunner.query(`ALTER TABLE "meta" ADD "remoteObjectStorageUseSSL" boolean DEFAULT true`); + await queryRunner.query(`ALTER TABLE "meta" ADD "remoteObjectStorageUseProxy" boolean DEFAULT true`); + await queryRunner.query(`ALTER TABLE "meta" ADD "remoteObjectStorageSetPublicRead" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "meta" ADD "remoteObjectStorageS3ForcePathStyle" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "remoteObjectStorageS3ForcePathStyle";`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "remoteObjectStorageSetPublicRead";`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "remoteObjectStorageUseProxy";`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "remoteObjectStorageUseSSL";`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "remoteObjectStoragePort";`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "remoteObjectStorageSecretKey";`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "remoteObjectStorageAccessKey";`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "remoteObjectStorageRegion";`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "remoteObjectStorageEndpoint";`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "remoteObjectStorageBaseUrl";`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "remoteObjectStoragePrefix";`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "remoteObjectStorageBucket";`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useRemoteObjectStorage";`); + } +} diff --git a/packages/backend/migration/1730162520000-dropObjectStorageRemoteSetting.js b/packages/backend/migration/1730162520000-dropObjectStorageRemoteSetting.js new file mode 100644 index 0000000000..776ca62f49 --- /dev/null +++ b/packages/backend/migration/1730162520000-dropObjectStorageRemoteSetting.js @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class DropObjectStorageRemoteSetting1730162520000 { + name = 'DropObjectStorageRemoteSetting1730162520000' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRemoteS3ForcePathStyle"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRemoteSetPublicRead"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRemoteUseProxy"`, undefined); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRemoteUseSSL"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRemotePort"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRemoteSecretKey"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRemoteAccessKey"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRemoteRegion"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRemoteEndpoint"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRemoteBaseUrl"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRemotePrefix"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRemoteBucket"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useObjectStorageRemote"`); + } + +} diff --git a/packages/backend/migration/1730364663000-youBlockedImageUrl.js b/packages/backend/migration/1730364663000-youBlockedImageUrl.js new file mode 100644 index 0000000000..c7ad9f5b13 --- /dev/null +++ b/packages/backend/migration/1730364663000-youBlockedImageUrl.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class YouBlockedImageUrl1730364663000 { + name = 'YouBlockedImageUrl1730364663000' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "youBlockedImageUrl" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "youBlockedImageUrl"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 3b5dea1e96..326b0aaade 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -112,7 +112,7 @@ "bullmq": "5.13.2", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", - "cfm-js": "0.24.0-cherrypick.8", + "mfc-js": "0.24.0-cherrypick.9", "chalk": "5.3.0", "chalk-template": "1.1.0", "cherrypick-js": "workspace:*", diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index e453b99aad..cc68c9b203 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -19,6 +19,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { query } from '@/misc/prelude/url.js'; import type { Serialized } from '@/types.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { DriveService } from '@/core/DriveService.js'; const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/; @@ -39,6 +40,7 @@ export class CustomEmojiService implements OnApplicationShutdown { private emojiEntityService: EmojiEntityService, private moderationLogService: ModerationLogService, private globalEventService: GlobalEventService, + private driveService: DriveService, ) { this.emojisCache = new MemoryKVCache(1000 * 60 * 60 * 12); // 12h @@ -68,6 +70,15 @@ export class CustomEmojiService implements OnApplicationShutdown { localOnly: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][]; }, moderator?: MiUser): Promise { + // システムユーザーとして再アップロード + if (!data.driveFile.user?.isRoot) { + data.driveFile = await this.driveService.uploadFromUrl({ + url: data.driveFile.url, + user: null, + force: true, + }); + } + const emoji = await this.emojisRepository.insertOne({ id: this.idService.gen(), updatedAt: new Date(), diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index d55b3e334a..203f3c718a 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -147,7 +147,7 @@ export class DriveService { * @param isRemote If true, file is remote file */ @bindThis - private async save(file: MiDriveFile, path: string, name: string, type: string, hash: string, size: number, isRemote: boolean): Promise { + private async save(file: MiDriveFile, path: string, name: string, type: string, hash: string, size: number, isRemote = false): Promise { // thunbnail, webpublic を必要なら生成 const alts = await this.generateAlts(path, type, !file.uri); @@ -170,16 +170,34 @@ export class DriveService { ext = ''; } - const useObjectStorageRemote = isRemote && this.meta.useObjectStorageRemote; - const objectStorageBaseUrl = useObjectStorageRemote ? this.meta.objectStorageRemoteBaseUrl : this.meta.objectStorageBaseUrl; - const objectStorageUseSSL = useObjectStorageRemote ? this.meta.objectStorageRemoteUseSSL : this.meta.objectStorageUseSSL; - const objectStorageEndpoint = useObjectStorageRemote ? this.meta.objectStorageRemoteEndpoint : this.meta.objectStorageEndpoint; - const objectStoragePort = useObjectStorageRemote ? this.meta.objectStorageRemotePort : this.meta.objectStoragePort; - const objectStorageBucket = useObjectStorageRemote ? this.meta.objectStorageRemoteBucket : this.meta.objectStorageBucket; - const objectStoragePrefix = useObjectStorageRemote ? this.meta.objectStorageRemotePrefix : this.meta.objectStoragePrefix; + const useRemoteObjectStorage = isRemote && this.meta.useRemoteObjectStorage; + + const objectStorageBaseUrl = useRemoteObjectStorage + ? this.meta.remoteObjectStorageBaseUrl + : this.meta.objectStorageBaseUrl; + + const objectStorageUseSSL = useRemoteObjectStorage + ? this.meta.remoteObjectStorageUseSSL + : this.meta.objectStorageUseSSL; + + const objectStorageEndpoint = useRemoteObjectStorage + ? this.meta.remoteObjectStorageEndpoint + : this.meta.objectStorageEndpoint; + + const objectStoragePort = useRemoteObjectStorage + ? this.meta.remoteObjectStoragePort + : this.meta.objectStoragePort; + + const objectStorageBucket = useRemoteObjectStorage + ? this.meta.remoteObjectStorageBucket + : this.meta.objectStorageBucket; + + const objectStoragePrefix = useRemoteObjectStorage + ? this.meta.remoteObjectStoragePrefix + : this.meta.objectStoragePrefix; const baseUrl = objectStorageBaseUrl - ?? `${ objectStorageUseSSL ? 'https' : 'http' }://${ objectStorageEndpoint }${ objectStoragePort ? `:${objectStoragePort}` : '' }/${ objectStorageBucket }`; + ?? `${ objectStorageUseSSL ? 'https' : 'http' }://${ objectStorageEndpoint }${ objectStoragePort ? `:${ objectStoragePort }` : '' }/${ objectStorageBucket }`; // for original const key = `${objectStoragePrefix}/${randomUUID()}${ext}`; @@ -380,13 +398,19 @@ export class DriveService { * Upload to ObjectStorage */ @bindThis - private async upload(key: string, stream: fs.ReadStream | Buffer, type: string, isRemote: boolean, ext?: string | null, filename?: string) { + private async upload(key: string, stream: fs.ReadStream | Buffer, type: string, isRemote = false, ext?: string | null, filename?: string) { if (type === 'image/apng') type = 'image/png'; if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream'; - const useObjectStorageRemote = isRemote && this.meta.useObjectStorageRemote; - const objectStorageBucket = useObjectStorageRemote ? this.meta.objectStorageRemoteBucket : this.meta.objectStorageBucket; - const objectStorageSetPublicRead = useObjectStorageRemote ? this.meta.objectStorageRemoteSetPublicRead : this.meta.objectStorageSetPublicRead; + const useRemoteObjectStorage = isRemote && this.meta.useRemoteObjectStorage; + + const objectStorageBucket = useRemoteObjectStorage + ? this.meta.remoteObjectStorageBucket + : this.meta.objectStorageBucket; + + const objectStorageSetPublicRead = useRemoteObjectStorage + ? this.meta.remoteObjectStorageSetPublicRead + : this.meta.objectStorageSetPublicRead; const params = { Bucket: objectStorageBucket, @@ -754,7 +778,7 @@ export class DriveService { } @bindThis - public async deleteFileSync(file: MiDriveFile, isExpired = false, isRemote: boolean, deleter?: MiUser) { + public async deleteFileSync(file: MiDriveFile, isExpired = false, isRemote = false, deleter?: MiUser) { if (file.storedInternal) { this.internalStorageService.del(file.accessKey!); @@ -829,9 +853,12 @@ export class DriveService { } @bindThis - public async deleteObjectStorageFile(key: string, isRemote: boolean) { - const useObjectStorageRemote = isRemote && this.meta.useObjectStorageRemote; - const objectStorageBucket = useObjectStorageRemote ? this.meta.objectStorageRemoteBucket : this.meta.objectStorageBucket; + public async deleteObjectStorageFile(key: string, isRemote = false) { + const useRemoteObjectStorage = isRemote && this.meta.useRemoteObjectStorage; + const objectStorageBucket = useRemoteObjectStorage + ? this.meta.remoteObjectStorageBucket + : this.meta.objectStorageBucket; + try { const param = { Bucket: objectStorageBucket, diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index c489a5c4ca..8725fe5352 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -14,7 +14,7 @@ import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; import type { DefaultTreeAdapterMap } from 'parse5'; -import type * as mfm from 'cfm-js'; +import type * as mfm from 'mfc-js'; const treeAdapter = parse5.defaultTreeAdapter; type Node = DefaultTreeAdapterMap['node']; diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 2212b6f793..bf305f587a 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -4,7 +4,7 @@ */ import { setImmediate } from 'node:timers/promises'; -import * as mfm from 'cfm-js'; +import * as mfm from 'mfc-js'; import { In, DataSource, IsNull, LessThan } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; diff --git a/packages/backend/src/core/NoteUpdateService.ts b/packages/backend/src/core/NoteUpdateService.ts index 8c5c76e82b..6e85e701b1 100644 --- a/packages/backend/src/core/NoteUpdateService.ts +++ b/packages/backend/src/core/NoteUpdateService.ts @@ -7,7 +7,7 @@ import { setImmediate } from 'node:timers/promises'; import util from 'util'; import { In, DataSource } from 'typeorm'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import * as mfm from 'cfm-js'; +import * as mfm from 'mfc-js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; import type { NotesRepository, UsersRepository } from '@/models/_.js'; @@ -218,7 +218,7 @@ export class NoteUpdateService implements OnApplicationShutdown { this.globalEventService.publishNoteStream(note.id, 'updated', { cw: note.cw, text: note.text }); //#region AP deliver - if (this.userEntityService.isLocalUser(user)) { + if (this.userEntityService.isLocalUser(user) && !note.localOnly) { await (async () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index e4395792e1..a10949f361 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -18,6 +18,7 @@ import { UserWebhookDeliverJobData, SystemWebhookDeliverJobData, ScheduledNoteDeleteJobData, + ScheduleNotePostJobData, } from '../queue/types.js'; import type { Provider } from '@nestjs/common'; @@ -31,6 +32,7 @@ export type ObjectStorageQueue = Bull.Queue; export type UserWebhookDeliverQueue = Bull.Queue; export type SystemWebhookDeliverQueue = Bull.Queue; export type ScheduledNoteDeleteQueue = Bull.Queue; +export type ScheduleNotePostQueue = Bull.Queue; const $system: Provider = { provide: 'queue:system', @@ -92,6 +94,12 @@ const $scheduledNoteDelete: Provider = { inject: [DI.config, DI.redisForJobQueue], }; +const $scheduleNotePost: Provider = { + provide: 'queue:scheduleNotePost', + useFactory: (config: Config, redisForJobQueue: Redis.Redis) => new Bull.Queue(QUEUE.SCHEDULE_NOTE_POST, baseQueueOptions(config, QUEUE.SCHEDULE_NOTE_POST, redisForJobQueue)), + inject: [DI.config, DI.redisForJobQueue], +}; + @Module({ imports: [ ], @@ -106,6 +114,7 @@ const $scheduledNoteDelete: Provider = { $userWebhookDeliver, $systemWebhookDeliver, $scheduledNoteDelete, + $scheduleNotePost, ], exports: [ $system, @@ -118,6 +127,7 @@ const $scheduledNoteDelete: Provider = { $userWebhookDeliver, $systemWebhookDeliver, $scheduledNoteDelete, + $scheduleNotePost, ], }) export class QueueModule implements OnApplicationShutdown { @@ -132,6 +142,7 @@ export class QueueModule implements OnApplicationShutdown { @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, @Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue, + @Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue, ) {} public async dispose(): Promise { @@ -149,6 +160,7 @@ export class QueueModule implements OnApplicationShutdown { this.userWebhookDeliverQueue.close(), this.systemWebhookDeliverQueue.close(), this.scheduledNoteDeleteQueue.close(), + this.scheduleNotePostQueue.close(), ]); } diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 497fb54349..6fce806f19 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -34,6 +34,7 @@ import type { UserWebhookDeliverQueue, SystemWebhookDeliverQueue, ScheduledNoteDeleteQueue, + ScheduleNotePostQueue, } from './QueueModule.js'; import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; @@ -54,6 +55,7 @@ export class QueueService { @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, @Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue, + @Inject('queue:scheduleNotePost') public ScheduleNotePostQueue: ScheduleNotePostQueue, ) { this.systemQueue.add('tickCharts', { }, { diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index c8c8a64c4b..860f7a142d 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -36,6 +36,7 @@ export type RolePolicies = { ltlAvailable: boolean; canPublicNote: boolean; canEditNote: boolean; + scheduleNoteMax: number; mentionLimit: number; canInvite: boolean; inviteLimit: number; @@ -72,6 +73,7 @@ export const DEFAULT_POLICIES: RolePolicies = { ltlAvailable: true, canPublicNote: true, canEditNote: true, + scheduleNoteMax: 5, mentionLimit: 20, canInvite: false, inviteLimit: 0, @@ -379,6 +381,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), canEditNote: calc('canEditNote', vs => vs.some(v => v === true)), + scheduleNoteMax: calc('scheduleNoteMax', vs => Math.max(...vs)), mentionLimit: calc('mentionLimit', vs => Math.max(...vs)), canInvite: calc('canInvite', vs => vs.some(v => v === true)), inviteLimit: calc('inviteLimit', vs => Math.max(...vs)), diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts index 8150153f0d..de6b6c562a 100644 --- a/packages/backend/src/core/S3Service.ts +++ b/packages/backend/src/core/S3Service.ts @@ -19,26 +19,41 @@ import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/c export class S3Service { constructor( private httpRequestService: HttpRequestService, - ) { - } + ) {} @bindThis - public getS3Client(meta: MiMeta, isRemote: boolean): S3Client { - const useObjectStorageRemote = isRemote && meta.useObjectStorageRemote; + public getS3Client(meta: MiMeta, isRemote = false): S3Client { + const useRemoteObjectStorage = isRemote && meta.useRemoteObjectStorage; - const objectStorageEndpoint = useObjectStorageRemote ? meta.objectStorageRemoteEndpoint : meta.objectStorageEndpoint; - const objectStorageUseSSL = useObjectStorageRemote ? meta.objectStorageRemoteUseSSL : meta.objectStorageUseSSL; - const objectStorageUseProxy = useObjectStorageRemote ? meta.objectStorageRemoteUseProxy : meta.objectStorageUseProxy; - const objectStorageAccessKey = useObjectStorageRemote ? meta.objectStorageRemoteAccessKey : meta.objectStorageAccessKey; - const objectStorageSecretKey = useObjectStorageRemote ? meta.objectStorageRemoteSecretKey : meta.objectStorageSecretKey; - const objectStorageRegion = useObjectStorageRemote ? meta.objectStorageRemoteRegion : meta.objectStorageRegion; - const objectStorageS3ForcePathStyle = useObjectStorageRemote ? meta.objectStorageRemoteS3ForcePathStyle : meta.objectStorageS3ForcePathStyle; + const objectStorageEndpoint = useRemoteObjectStorage + ? meta.remoteObjectStorageEndpoint + : meta.objectStorageEndpoint; + + const objectStorageUseSSL = useRemoteObjectStorage + ? meta.remoteObjectStorageUseSSL + : meta.objectStorageUseSSL; + + const objectStorageAccessKey = useRemoteObjectStorage + ? meta.remoteObjectStorageAccessKey + : meta.objectStorageAccessKey; + + const objectStorageSecretKey = useRemoteObjectStorage + ? meta.remoteObjectStorageSecretKey + : meta.objectStorageSecretKey; + + const objectStorageRegion = useRemoteObjectStorage + ? meta.remoteObjectStorageRegion + : meta.objectStorageRegion; + + const objectStorageS3ForcePathStyle = useRemoteObjectStorage + ? meta.remoteObjectStorageS3ForcePathStyle + : meta.objectStorageS3ForcePathStyle; const u = objectStorageEndpoint - ? `${objectStorageUseSSL ? 'https' : 'http'}://${objectStorageEndpoint}` - : `${objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent + ? `${ objectStorageUseSSL ? 'https' : 'http' }://${ objectStorageEndpoint }` + : `${ objectStorageUseSSL ? 'https' : 'http' }://example.com`; // dummy url to select http(s) agent - const agent = this.httpRequestService.getAgentByUrl(new URL(u), !objectStorageUseProxy); + const agent = this.httpRequestService.getAgentByUrl(new URL(u), !objectStorageUseSSL); const handlerOption: NodeHttpHandlerOptions = {}; if (objectStorageUseSSL) { handlerOption.httpsAgent = agent as https.Agent; @@ -52,7 +67,7 @@ export class S3Service { accessKeyId: objectStorageAccessKey, secretAccessKey: objectStorageSecretKey, } : undefined, - region: objectStorageRegion ? objectStorageRegion : undefined, // empty string is converted to undefined + region: objectStorageRegion || undefined, // 空文字列もundefinedにするため ?? は使わない tls: objectStorageUseSSL, forcePathStyle: objectStorageEndpoint ? objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted requestHandler: new NodeHttpHandler(handlerOption), @@ -60,7 +75,7 @@ export class S3Service { } @bindThis - public async upload(meta: MiMeta, input: PutObjectCommandInput, isRemote: boolean) { + public async upload(meta: MiMeta, input: PutObjectCommandInput, isRemote = false) { const client = this.getS3Client(meta, isRemote); return new Upload({ client, @@ -72,7 +87,7 @@ export class S3Service { } @bindThis - public delete(meta: MiMeta, input: DeleteObjectCommandInput, isRemote: boolean) { + public delete(meta: MiMeta, input: DeleteObjectCommandInput, isRemote = false) { const client = this.getS3Client(meta, isRemote); return client.send(new DeleteObjectCommand(input)); } diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts index c195416eb2..a108805771 100644 --- a/packages/backend/src/core/activitypub/ApMfmService.ts +++ b/packages/backend/src/core/activitypub/ApMfmService.ts @@ -4,7 +4,7 @@ */ import { Injectable } from '@nestjs/common'; -import * as mfm from 'cfm-js'; +import * as mfm from 'mfc-js'; import { MfmService } from '@/core/MfmService.js'; import type { MiNote } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 2a9947aea4..3913e5dd14 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -6,7 +6,7 @@ import { createPublicKey, randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; -import * as mfm from 'cfm-js'; +import * as mfm from 'mfc-js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index bdcf57f2db..6b2e4164a1 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -104,6 +104,7 @@ export class MetaEntityService { infoImageUrl: instance.infoImageUrl, serverErrorImageUrl: instance.serverErrorImageUrl, notFoundImageUrl: instance.notFoundImageUrl, + youBlockedImageUrl: instance.youBlockedImageUrl, iconUrl: instance.iconUrl, backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 3bcd358270..4fd399d285 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -142,6 +142,27 @@ export class NotificationEntityService implements OnModuleInit { note: noteIfNeed, users, }); + } else if (notification.type === 'note:grouped') { + const users = (await Promise.all(notification.notifierIds.map(notifier => { + const packedUser = hint?.packedUsers != null ? hint.packedUsers.get(notifier) : null; + if (packedUser) { + return packedUser; + } + + return this.userEntityService.pack(notifier, { id: meId }); + }))).filter(x => x != null); + // if all users have been deleted, don't show this notification + if (users.length === 0) { + return null; + } + + return await awaitAll({ + id: notification.id, + createdAt: new Date(notification.createdAt).toISOString(), + type: notification.type, + noteIds: notification.noteIds, + users, + }); } // #endregion @@ -213,6 +234,7 @@ export class NotificationEntityService implements OnModuleInit { if ('notifierId' in notification) userIds.push(notification.notifierId); if (notification.type === 'reaction:grouped') userIds.push(...notification.reactions.map(x => x.userId)); if (notification.type === 'renote:grouped') userIds.push(...notification.userIds); + if (notification.type === 'note:grouped') userIds.push(...notification.notifierIds); } const users = userIds.length > 0 ? await this.usersRepository.find({ where: { id: In(userIds) }, diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 52fcd5c60c..1fe4008839 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -92,5 +92,6 @@ export const DI = { userMemosRepository: Symbol('userMemosRepository'), bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), reversiGamesRepository: Symbol('reversiGamesRepository'), + noteScheduleRepository: Symbol('noteScheduleRepository'), //#endregion }; diff --git a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts index 28ecba8b3b..5b73525838 100644 --- a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts +++ b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import * as mfm from 'cfm-js'; +import * as mfm from 'mfc-js'; import { unique } from '@/misc/prelude/array.js'; export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] { diff --git a/packages/backend/src/misc/extract-hashtags.ts b/packages/backend/src/misc/extract-hashtags.ts index a0c7f7ad3b..0311243e20 100644 --- a/packages/backend/src/misc/extract-hashtags.ts +++ b/packages/backend/src/misc/extract-hashtags.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import * as mfm from 'cfm-js'; +import * as mfm from 'mfc-js'; import { unique } from '@/misc/prelude/array.js'; export function extractHashtags(nodes: mfm.MfmNode[]): string[] { diff --git a/packages/backend/src/misc/extract-mentions.ts b/packages/backend/src/misc/extract-mentions.ts index 72e0730068..0e9dc842db 100644 --- a/packages/backend/src/misc/extract-mentions.ts +++ b/packages/backend/src/misc/extract-mentions.ts @@ -5,7 +5,7 @@ // test is located in test/extract-mentions -import * as mfm from 'cfm-js'; +import * as mfm from 'mfc-js'; export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] { // TODO: 重複を削除 diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 00773f0715..30565a7b3a 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -157,6 +157,12 @@ export class MiMeta { }) public infoImageUrl: string | null; + @Column('varchar', { + length: 1024, + nullable: true, + }) + public youBlockedImageUrl: string | null; + @Column('boolean', { default: false, }) @@ -510,74 +516,74 @@ export class MiMeta { @Column('boolean', { default: false, }) - public useObjectStorageRemote: boolean; + public useRemoteObjectStorage: boolean; @Column('varchar', { length: 1024, nullable: true, }) - public objectStorageRemoteBucket: string | null; + public remoteObjectStorageBucket: string | null; @Column('varchar', { length: 1024, nullable: true, }) - public objectStorageRemotePrefix: string | null; + public remoteObjectStoragePrefix: string | null; @Column('varchar', { length: 1024, nullable: true, }) - public objectStorageRemoteBaseUrl: string | null; + public remoteObjectStorageBaseUrl: string | null; @Column('varchar', { length: 1024, nullable: true, }) - public objectStorageRemoteEndpoint: string | null; + public remoteObjectStorageEndpoint: string | null; @Column('varchar', { length: 1024, nullable: true, }) - public objectStorageRemoteRegion: string | null; + public remoteObjectStorageRegion: string | null; @Column('varchar', { length: 1024, nullable: true, }) - public objectStorageRemoteAccessKey: string | null; + public remoteObjectStorageAccessKey: string | null; @Column('varchar', { length: 1024, nullable: true, }) - public objectStorageRemoteSecretKey: string | null; + public remoteObjectStorageSecretKey: string | null; @Column('integer', { nullable: true, }) - public objectStorageRemotePort: number | null; + public remoteObjectStoragePort: number | null; @Column('boolean', { default: true, }) - public objectStorageRemoteUseSSL: boolean; + public remoteObjectStorageUseSSL: boolean; @Column('boolean', { default: true, }) - public objectStorageRemoteUseProxy: boolean; + public remoteObjectStorageUseProxy: boolean; @Column('boolean', { default: false, }) - public objectStorageRemoteSetPublicRead: boolean; + public remoteObjectStorageSetPublicRead: boolean; @Column('boolean', { default: true, }) - public objectStorageRemoteS3ForcePathStyle: boolean; + public remoteObjectStorageS3ForcePathStyle: boolean; @Column('boolean', { default: false, diff --git a/packages/backend/src/models/NoteSchedule.ts b/packages/backend/src/models/NoteSchedule.ts new file mode 100644 index 0000000000..25b8efb765 --- /dev/null +++ b/packages/backend/src/models/NoteSchedule.ts @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; +import { MiNote } from '@/models/Note.js'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiChannel } from './Channel.js'; +import { EventSchema } from './Event.js'; +import type { MiDriveFile } from './DriveFile.js'; + +type MinimumUser = { + id: MiUser['id']; + host: MiUser['host']; + username: MiUser['username']; + uri: MiUser['uri']; +}; + +export type MiScheduleNoteType={ + /** Date.toISOString() */ + createdAt: string; + visibility: 'public' | 'home' | 'followers' | 'specified'; + visibleUsers: MinimumUser[]; + channel?: MiChannel['id']; + poll: { + multiple: boolean; + choices: string[]; + /** Date.toISOString() */ + expiresAt: string | null + } | undefined; + renote?: MiNote['id']; + localOnly: boolean; + cw?: string | null; + reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null; + files: MiDriveFile['id'][]; + text?: string | null; + reply?: MiNote['id']; + event?: { + /** Date.toISOString() */ + start: string; + /** Date.toISOString() */ + end: string | null; + title: string; + metadata: EventSchema; + } | null; + disableRightClick: boolean, + apMentions?: MinimumUser[] | null; + apHashtags?: string[] | null; + apEmojis?: string[] | null; +} + +@Entity('note_schedule') +export class MiNoteSchedule { + @PrimaryColumn(id()) + public id: string; + + @Column('jsonb') + public note: MiScheduleNoteType; + + @Index() + @Column('varchar', { + length: 260, + }) + public userId: MiUser['id']; + + @Column('timestamp with time zone') + public scheduledAt: Date; +} diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 647cac7397..5b846cdfb2 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -140,4 +140,10 @@ export type MiGroupedNotification = MiNotification | { createdAt: string; noteId: MiNote['id']; userIds: string[]; +} | { + type: 'note:grouped'; + id: string; + createdAt: string; + notifierIds: MiUser['id'][]; + noteIds: string[]; }; diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 325645cbd6..c976e902c1 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -44,6 +44,7 @@ import { MiNote, MiNoteFavorite, MiNoteReaction, + MiNoteSchedule, MiNoteThreadMuting, MiNoteUnread, MiPage, @@ -537,6 +538,12 @@ const $abuseReportResolversRepository: Provider = { inject: [DI.db], }; +const $noteScheduleRepository: Provider = { + provide: DI.noteScheduleRepository, + useFactory: (db: DataSource) => db.getRepository(MiNoteSchedule).extend(miRepository as MiRepository), + inject: [DI.db], +}; + @Module({ imports: [], providers: [ @@ -615,6 +622,7 @@ const $abuseReportResolversRepository: Provider = { $abuseReportResolversRepository, $bubbleGameRecordsRepository, $reversiGamesRepository, + $noteScheduleRepository, ], exports: [ $usersRepository, @@ -692,6 +700,7 @@ const $abuseReportResolversRepository: Provider = { $abuseReportResolversRepository, $bubbleGameRecordsRepository, $reversiGamesRepository, + $noteScheduleRepository, ], }) export class RepositoryModule { diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index 92f9dbf21e..0889179a97 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -85,6 +85,7 @@ import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserListFavorite } from '@/models/UserListFavorite.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; +import { MiNoteSchedule } from '@/models/NoteSchedule.js'; import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; export interface MiRepository { @@ -166,6 +167,7 @@ export { MiNote, MiNoteFavorite, MiNoteReaction, + MiNoteSchedule, MiNoteThreadMuting, MiNoteUnread, MiPage, @@ -283,3 +285,4 @@ export type FlashLikesRepository = Repository & MiRepository & MiRepository; export type BubbleGameRecordsRepository = Repository & MiRepository; export type ReversiGamesRepository = Repository & MiRepository; +export type NoteScheduleRepository = Repository; diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 5f0bfd0488..0942279b52 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -144,6 +144,10 @@ export const packedMetaLiteSchema = { type: 'string', optional: false, nullable: true, }, + youBlockedImageUrl: { + type: 'string', + optional: false, nullable: true, + }, iconUrl: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 84d8e2a79c..3b11ade641 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -300,6 +300,10 @@ export const packedRolePoliciesSchema = { type: 'boolean', optional: false, nullable: false, }, + scheduleNoteMax: { + type: 'integer', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index ee9ecafacc..2083fb5741 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -84,6 +84,7 @@ import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserMemo } from '@/models/UserMemo.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; +import { MiNoteSchedule } from '@/models/NoteSchedule.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; @@ -165,6 +166,7 @@ export const entities = [ MiNote, MiNoteFavorite, MiNoteReaction, + MiNoteSchedule, MiNoteThreadMuting, MiNoteUnread, MiPage, diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 8e604909ae..7ca8d3a779 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -42,6 +42,7 @@ import { AggregateRetentionProcessorService } from './processors/AggregateRetent import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js'; import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js'; import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteDeleteProcessorService.js'; +import { ScheduleNotePostProcessorService } from './processors/ScheduleNotePostProcessorService.js'; @Module({ imports: [ @@ -85,6 +86,7 @@ import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteD AggregateRetentionProcessorService, QueueProcessorService, ScheduledNoteDeleteProcessorService, + ScheduleNotePostProcessorService, ], exports: [ QueueProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index bc082dec5f..126723557b 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -45,6 +45,7 @@ import { BakeBufferedReactionsProcessorService } from './processors/BakeBuffered import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteDeleteProcessorService.js'; +import { ScheduleNotePostProcessorService } from './processors/ScheduleNotePostProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; import { QUEUE, baseQueueOptions } from './const.js'; @@ -87,6 +88,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private objectStorageQueueWorker: Bull.Worker; private endedPollNotificationQueueWorker: Bull.Worker; private scheduledNoteDeleteQueueWorker: Bull.Worker; + private schedulerNotePostQueueWorker: Bull.Worker; constructor( @Inject(DI.config) @@ -130,6 +132,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService, private cleanProcessorService: CleanProcessorService, private scheduledNoteDeleteProcessorService: ScheduledNoteDeleteProcessorService, + private scheduleNotePostProcessorService: ScheduleNotePostProcessorService, ) { this.logger = this.queueLoggerService.logger; @@ -529,6 +532,15 @@ export class QueueProcessorService implements OnApplicationShutdown { }); } //#endregion + + //#region schedule note post + { + this.schedulerNotePostQueueWorker = new Bull.Worker(QUEUE.SCHEDULE_NOTE_POST, (job) => this.scheduleNotePostProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.SCHEDULE_NOTE_POST, this.redisForJobQueue), + autorun: false, + }); + } + //#endregion } @bindThis @@ -544,6 +556,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.objectStorageQueueWorker.run(), this.endedPollNotificationQueueWorker.run(), this.scheduledNoteDeleteQueueWorker.run(), + this.schedulerNotePostQueueWorker.run(), ]); } @@ -560,6 +573,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.objectStorageQueueWorker.close(), this.endedPollNotificationQueueWorker.close(), this.scheduledNoteDeleteQueueWorker.close(), + this.schedulerNotePostQueueWorker.close(), ]); } diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts index 2dc0a08e1c..860ec07f27 100644 --- a/packages/backend/src/queue/const.ts +++ b/packages/backend/src/queue/const.ts @@ -18,6 +18,7 @@ export const QUEUE = { USER_WEBHOOK_DELIVER: 'userWebhookDeliver', SYSTEM_WEBHOOK_DELIVER: 'systemWebhookDeliver', SCHEDULED_NOTE_DELETE: 'scheduledNoteDelete', + SCHEDULE_NOTE_POST: 'scheduleNotePost', }; export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE], redisConnection: Redis.Redis): Bull.QueueOptions { diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index f0e3e9abba..9a284902fd 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -108,7 +108,7 @@ export class DeleteAccountProcessorService { cursor = files.at(-1)?.id ?? null; for (const file of files) { - await this.driveService.deleteFileSync(file, false, isRemote); + await this.driveService.deleteFileSync(file, undefined, isRemote); } } diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts index 61e64c18f6..1ca44b56e4 100644 --- a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts @@ -38,6 +38,7 @@ export class DeleteDriveFilesProcessorService { this.logger.info(`Deleting drive files of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + const isRemote = user ? this.userEntityService.isRemoteUser(user) : false; if (user == null) { return; } @@ -65,8 +66,7 @@ export class DeleteDriveFilesProcessorService { cursor = files.at(-1)?.id ?? null; for (const file of files) { - const isRemote = file.user ? this.userEntityService.isRemoteUser(file.user) : false; - await this.driveService.deleteFileSync(file, false, isRemote); + await this.driveService.deleteFileSync(file, undefined, isRemote); deletedCount++; } diff --git a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts new file mode 100644 index 0000000000..6b5b9db71b --- /dev/null +++ b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts @@ -0,0 +1,100 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; +import { NoteCreateService } from '@/core/NoteCreateService.js'; +import type { ChannelsRepository, DriveFilesRepository, MiDriveFile, NoteScheduleRepository, NotesRepository, UsersRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { ScheduleNotePostJobData } from '../types.js'; + +@Injectable() +export class ScheduleNotePostProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.noteScheduleRepository) + private noteScheduleRepository: NoteScheduleRepository, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + private noteCreateService: NoteCreateService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('schedule-note-post'); + } + + @bindThis + public async process(job: Bull.Job): Promise { + this.noteScheduleRepository.findOneBy({ id: job.data.scheduleNoteId }).then(async (data) => { + if (!data) { + this.logger.warn(`Schedule note ${job.data.scheduleNoteId} not found`); + } else { + const me = await this.usersRepository.findOneBy({ id: data.userId }); + const note = data.note; + + //idの形式でキューに積んであったのをDBから取り寄せる + const reply = note.reply ? await this.notesRepository.findOneBy({ id: note.reply }) : undefined; + const renote = note.reply ? await this.notesRepository.findOneBy({ id: note.renote }) : undefined; + const channel = note.channel ? await this.channelsRepository.findOneBy({ id: note.channel, isArchived: false }) : undefined; + let files: MiDriveFile[] = []; + const fileIds = note.files ?? null; + if (fileIds != null && fileIds.length > 0 && me) { + files = await this.driveFilesRepository.createQueryBuilder('file') + .where('file.userId = :userId AND file.id IN (:...fileIds)', { + userId: me.id, + fileIds, + }) + .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') + .setParameters({ fileIds }) + .getMany(); + } + if ( + !data.userId || + !me || + (note.reply && !reply) || + (note.renote && !renote) || + (note.channel && !channel) || + (note.files.length !== files.length) + ) { + //キューに積んだときは有った物が消滅してたら予約投稿をキャンセルする + this.logger.warn('cancel schedule note'); + await this.noteScheduleRepository.remove(data); + return; + } + await this.noteCreateService.create(me, { + ...note, + createdAt: new Date(note.createdAt), //typeORMのjsonbで何故かstringにされるから戻す + files, + poll: note.poll ? { + choices: note.poll.choices, + multiple: note.poll.multiple, + expiresAt: note.poll.expiresAt ? new Date(note.poll.expiresAt) : null, + } : undefined, + event: note.event ? { + start: new Date(note.event.start), + end: note.event.end ? new Date(note.event.end) : null, + title: note.event.title, + metadata: note.event.metadata, + } : undefined, + reply, + renote, + channel, + }); + await this.noteScheduleRepository.remove(data); + } + }); + } +} diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 0a5da0d3bc..b9afc7ecc4 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -137,3 +137,7 @@ export type ThinUser = { export type ScheduledNoteDeleteJobData = { noteId: MiNote['id']; }; + +export type ScheduleNotePostJobData = { + scheduleNoteId: MiNote['id']; +} diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index dcf2ce59de..4643153d39 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -312,6 +312,9 @@ import * as ep___notes_reactions_create from './endpoints/notes/reactions/create import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js'; import * as ep___notes_renotes from './endpoints/notes/renotes.js'; import * as ep___notes_replies from './endpoints/notes/replies.js'; +import * as ep___notes_schedule_create from './endpoints/notes/schedule/create.js'; +import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js'; +import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js'; import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js'; import * as ep___notes_search from './endpoints/notes/search.js'; import * as ep___notes_show from './endpoints/notes/show.js'; @@ -728,6 +731,9 @@ const $notes_reactions_create: Provider = { provide: 'ep:notes/reactions/create' const $notes_reactions_delete: Provider = { provide: 'ep:notes/reactions/delete', useClass: ep___notes_reactions_delete.default }; const $notes_renotes: Provider = { provide: 'ep:notes/renotes', useClass: ep___notes_renotes.default }; const $notes_replies: Provider = { provide: 'ep:notes/replies', useClass: ep___notes_replies.default }; +const $notes_schedule_create: Provider = { provide: 'ep:notes/schedule/create', useClass: ep___notes_schedule_create.default }; +const $notes_schedule_delete: Provider = { provide: 'ep:notes/schedule/delete', useClass: ep___notes_schedule_delete.default }; +const $notes_schedule_list: Provider = { provide: 'ep:notes/schedule/list', useClass: ep___notes_schedule_list.default }; const $notes_searchByTag: Provider = { provide: 'ep:notes/search-by-tag', useClass: ep___notes_searchByTag.default }; const $notes_search: Provider = { provide: 'ep:notes/search', useClass: ep___notes_search.default }; const $notes_show: Provider = { provide: 'ep:notes/show', useClass: ep___notes_show.default }; @@ -1149,6 +1155,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_reactions_delete, $notes_renotes, $notes_replies, + $notes_schedule_create, + $notes_schedule_delete, + $notes_schedule_list, $notes_searchByTag, $notes_search, $notes_show, @@ -1562,6 +1571,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_reactions_delete, $notes_renotes, $notes_replies, + $notes_schedule_create, + $notes_schedule_delete, + $notes_schedule_list, $notes_searchByTag, $notes_search, $notes_show, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index a29da92138..f02da4145b 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -317,6 +317,9 @@ import * as ep___notes_reactions_create from './endpoints/notes/reactions/create import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js'; import * as ep___notes_renotes from './endpoints/notes/renotes.js'; import * as ep___notes_replies from './endpoints/notes/replies.js'; +import * as ep___notes_schedule_create from './endpoints/notes/schedule/create.js'; +import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js'; +import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js'; import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js'; import * as ep___notes_search from './endpoints/notes/search.js'; import * as ep___notes_show from './endpoints/notes/show.js'; @@ -731,6 +734,9 @@ const eps = [ ['notes/reactions/delete', ep___notes_reactions_delete], ['notes/renotes', ep___notes_renotes], ['notes/replies', ep___notes_replies], + ['notes/schedule/create', ep___notes_schedule_create], + ['notes/schedule/delete', ep___notes_schedule_delete], + ['notes/schedule/list', ep___notes_schedule_list], ['notes/search-by-tag', ep___notes_searchByTag], ['notes/search', ep___notes_search], ['notes/show', ep___notes_show], diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 127c431304..0fd911917b 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -94,6 +94,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + youBlockedImageUrl: { + type: 'string', + optional: false, nullable: true, + }, iconUrl: { type: 'string', optional: false, nullable: true, @@ -305,53 +309,53 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - useObjectStorageRemote: { + useRemoteObjectStorage: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, - objectStorageRemoteBaseUrl: { + remoteObjectStorageBaseUrl: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, - objectStorageRemoteBucket: { + remoteObjectStorageBucket: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, - objectStorageRemotePrefix: { + remoteObjectStoragePrefix: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, - objectStorageRemoteEndpoint: { + remoteObjectStorageEndpoint: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, - objectStorageRemoteRegion: { + remoteObjectStorageRegion: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, - objectStorageRemotePort: { + remoteObjectStoragePort: { type: 'number', - optional: true, nullable: true, + optional: false, nullable: true, }, - objectStorageRemoteAccessKey: { + remoteObjectStorageAccessKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, - objectStorageRemoteSecretKey: { + remoteObjectStorageSecretKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, - objectStorageRemoteUseSSL: { + remoteObjectStorageUseSSL: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, - objectStorageRemoteUseProxy: { + remoteObjectStorageUseProxy: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, - objectStorageRemoteSetPublicRead: { + remoteObjectStorageSetPublicRead: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, enableIpLogging: { type: 'boolean', @@ -489,7 +493,7 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - objectStorageRemoteS3ForcePathStyle: { + remoteObjectStorageS3ForcePathStyle: { type: 'boolean', optional: false, nullable: false, }, @@ -654,6 +658,7 @@ export default class extends Endpoint { // eslint- serverErrorImageUrl: instance.serverErrorImageUrl, notFoundImageUrl: instance.notFoundImageUrl, infoImageUrl: instance.infoImageUrl, + youBlockedImageUrl: instance.youBlockedImageUrl, iconUrl: instance.iconUrl, app192IconUrl: instance.app192IconUrl, app512IconUrl: instance.app512IconUrl, @@ -707,19 +712,19 @@ export default class extends Endpoint { // eslint- objectStorageUseProxy: instance.objectStorageUseProxy, objectStorageSetPublicRead: instance.objectStorageSetPublicRead, objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle, - useObjectStorageRemote: instance.useObjectStorageRemote, - objectStorageRemoteBaseUrl: instance.objectStorageRemoteBaseUrl, - objectStorageRemoteBucket: instance.objectStorageRemoteBucket, - objectStorageRemotePrefix: instance.objectStorageRemotePrefix, - objectStorageRemoteEndpoint: instance.objectStorageRemoteEndpoint, - objectStorageRemoteRegion: instance.objectStorageRemoteRegion, - objectStorageRemotePort: instance.objectStorageRemotePort, - objectStorageRemoteAccessKey: instance.objectStorageRemoteAccessKey, - objectStorageRemoteSecretKey: instance.objectStorageRemoteSecretKey, - objectStorageRemoteUseSSL: instance.objectStorageRemoteUseSSL, - objectStorageRemoteUseProxy: instance.objectStorageRemoteUseProxy, - objectStorageRemoteSetPublicRead: instance.objectStorageRemoteSetPublicRead, - objectStorageRemoteS3ForcePathStyle: instance.objectStorageRemoteS3ForcePathStyle, + useRemoteObjectStorage: instance.useRemoteObjectStorage, + remoteObjectStorageBaseUrl: instance.remoteObjectStorageBaseUrl, + remoteObjectStorageBucket: instance.remoteObjectStorageBucket, + remoteObjectStoragePrefix: instance.remoteObjectStoragePrefix, + remoteObjectStorageEndpoint: instance.remoteObjectStorageEndpoint, + remoteObjectStorageRegion: instance.remoteObjectStorageRegion, + remoteObjectStoragePort: instance.remoteObjectStoragePort, + remoteObjectStorageAccessKey: instance.remoteObjectStorageAccessKey, + remoteObjectStorageSecretKey: instance.remoteObjectStorageSecretKey, + remoteObjectStorageUseSSL: instance.remoteObjectStorageUseSSL, + remoteObjectStorageUseProxy: instance.remoteObjectStorageUseProxy, + remoteObjectStorageSetPublicRead: instance.remoteObjectStorageSetPublicRead, + remoteObjectStorageS3ForcePathStyle: instance.remoteObjectStorageS3ForcePathStyle, deeplAuthKey: instance.deeplAuthKey, deeplIsPro: instance.deeplIsPro, ctav3SaKey: instance.ctav3SaKey, diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index d7f9e4eaa3..e2bd38aac6 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js'; +import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue, ScheduleNotePostQueue } from '@/core/QueueModule.js'; export const meta = { tags: ['admin'], @@ -55,6 +55,7 @@ export default class extends Endpoint { // eslint- @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, + @Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue, ) { super(meta, paramDef, async (ps, me) => { const deliverJobCounts = await this.deliverQueue.getJobCounts(); diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index ad7c604006..264551bdd3 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -54,6 +54,7 @@ export const paramDef = { serverErrorImageUrl: { type: 'string', nullable: true }, infoImageUrl: { type: 'string', nullable: true }, notFoundImageUrl: { type: 'string', nullable: true }, + youBlockedImageUrl: { type: 'string', nullable: true }, iconUrl: { type: 'string', nullable: true }, app192IconUrl: { type: 'string', nullable: true }, app512IconUrl: { type: 'string', nullable: true }, @@ -129,19 +130,19 @@ export const paramDef = { objectStorageUseProxy: { type: 'boolean' }, objectStorageSetPublicRead: { type: 'boolean' }, objectStorageS3ForcePathStyle: { type: 'boolean' }, - useObjectStorageRemote: { type: 'boolean' }, - objectStorageRemoteBaseUrl: { type: 'string', nullable: true }, - objectStorageRemoteBucket: { type: 'string', nullable: true }, - objectStorageRemotePrefix: { type: 'string', nullable: true }, - objectStorageRemoteEndpoint: { type: 'string', nullable: true }, - objectStorageRemoteRegion: { type: 'string', nullable: true }, - objectStorageRemotePort: { type: 'integer', nullable: true }, - objectStorageRemoteAccessKey: { type: 'string', nullable: true }, - objectStorageRemoteSecretKey: { type: 'string', nullable: true }, - objectStorageRemoteUseSSL: { type: 'boolean' }, - objectStorageRemoteUseProxy: { type: 'boolean' }, - objectStorageRemoteSetPublicRead: { type: 'boolean' }, - objectStorageRemoteS3ForcePathStyle: { type: 'boolean' }, + useRemoteObjectStorage: { type: 'boolean' }, + remoteObjectStorageBaseUrl: { type: 'string', nullable: true }, + remoteObjectStorageBucket: { type: 'string', nullable: true }, + remoteObjectStoragePrefix: { type: 'string', nullable: true }, + remoteObjectStorageEndpoint: { type: 'string', nullable: true }, + remoteObjectStorageRegion: { type: 'string', nullable: true }, + remoteObjectStoragePort: { type: 'integer', nullable: true }, + remoteObjectStorageAccessKey: { type: 'string', nullable: true }, + remoteObjectStorageSecretKey: { type: 'string', nullable: true }, + remoteObjectStorageUseSSL: { type: 'boolean' }, + remoteObjectStorageUseProxy: { type: 'boolean' }, + remoteObjectStorageSetPublicRead: { type: 'boolean' }, + remoteObjectStorageS3ForcePathStyle: { type: 'boolean' }, enableIpLogging: { type: 'boolean' }, enableActiveEmailValidation: { type: 'boolean' }, enableVerifymailApi: { type: 'boolean' }, @@ -221,7 +222,7 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - private moduleRef: ModuleRef, + private moduleRef: ModuleRef, private metaService: MetaService, private moderationLogService: ModerationLogService, ) { @@ -302,6 +303,10 @@ export default class extends Endpoint { // eslint- set.notFoundImageUrl = ps.notFoundImageUrl; } + if (ps.youBlockedImageUrl !== undefined) { + set.youBlockedImageUrl = ps.youBlockedImageUrl; + } + if (ps.backgroundImageUrl !== undefined) { set.backgroundImageUrl = ps.backgroundImageUrl; } @@ -542,56 +547,56 @@ export default class extends Endpoint { // eslint- set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle; } - if (ps.useObjectStorageRemote !== undefined) { - set.useObjectStorageRemote = ps.useObjectStorageRemote; + if (ps.useRemoteObjectStorage !== undefined) { + set.useRemoteObjectStorage = ps.useRemoteObjectStorage; } - if (ps.objectStorageRemoteBaseUrl !== undefined) { - set.objectStorageRemoteBaseUrl = ps.objectStorageRemoteBaseUrl; + if (ps.remoteObjectStorageBaseUrl !== undefined) { + set.remoteObjectStorageBaseUrl = ps.remoteObjectStorageBaseUrl; } - if (ps.objectStorageRemoteBucket !== undefined) { - set.objectStorageRemoteBucket = ps.objectStorageRemoteBucket; + if (ps.remoteObjectStorageBucket !== undefined) { + set.remoteObjectStorageBucket = ps.remoteObjectStorageBucket; } - if (ps.objectStorageRemotePrefix !== undefined) { - set.objectStorageRemotePrefix = ps.objectStorageRemotePrefix; + if (ps.remoteObjectStoragePrefix !== undefined) { + set.remoteObjectStoragePrefix = ps.remoteObjectStoragePrefix; } - if (ps.objectStorageRemoteEndpoint !== undefined) { - set.objectStorageRemoteEndpoint = ps.objectStorageRemoteEndpoint; + if (ps.remoteObjectStorageEndpoint !== undefined) { + set.remoteObjectStorageEndpoint = ps.remoteObjectStorageEndpoint; } - if (ps.objectStorageRemoteRegion !== undefined) { - set.objectStorageRemoteRegion = ps.objectStorageRemoteRegion; + if (ps.remoteObjectStorageRegion !== undefined) { + set.remoteObjectStorageRegion = ps.remoteObjectStorageRegion; } - if (ps.objectStorageRemotePort !== undefined) { - set.objectStorageRemotePort = ps.objectStorageRemotePort; + if (ps.remoteObjectStoragePort !== undefined) { + set.remoteObjectStoragePort = ps.remoteObjectStoragePort; } - if (ps.objectStorageRemoteAccessKey !== undefined) { - set.objectStorageRemoteAccessKey = ps.objectStorageRemoteAccessKey; + if (ps.remoteObjectStorageAccessKey !== undefined) { + set.remoteObjectStorageAccessKey = ps.remoteObjectStorageAccessKey; } - if (ps.objectStorageRemoteSecretKey !== undefined) { - set.objectStorageRemoteSecretKey = ps.objectStorageRemoteSecretKey; + if (ps.remoteObjectStorageSecretKey !== undefined) { + set.remoteObjectStorageSecretKey = ps.remoteObjectStorageSecretKey; } - if (ps.objectStorageRemoteUseSSL !== undefined) { - set.objectStorageRemoteUseSSL = ps.objectStorageRemoteUseSSL; + if (ps.remoteObjectStorageUseSSL !== undefined) { + set.remoteObjectStorageUseSSL = ps.remoteObjectStorageUseSSL; } - if (ps.objectStorageRemoteUseProxy !== undefined) { - set.objectStorageRemoteUseProxy = ps.objectStorageRemoteUseProxy; + if (ps.remoteObjectStorageUseProxy !== undefined) { + set.remoteObjectStorageUseProxy = ps.remoteObjectStorageUseProxy; } - if (ps.objectStorageRemoteSetPublicRead !== undefined) { - set.objectStorageRemoteSetPublicRead = ps.objectStorageRemoteSetPublicRead; + if (ps.remoteObjectStorageSetPublicRead !== undefined) { + set.remoteObjectStorageSetPublicRead = ps.remoteObjectStorageSetPublicRead; } - if (ps.objectStorageRemoteS3ForcePathStyle !== undefined) { - set.objectStorageRemoteS3ForcePathStyle = ps.objectStorageRemoteS3ForcePathStyle; + if (ps.remoteObjectStorageS3ForcePathStyle !== undefined) { + set.remoteObjectStorageS3ForcePathStyle = ps.remoteObjectStorageS3ForcePathStyle; } if (ps.translatorType !== undefined) { diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts index dc6ffd3e02..833a74fe12 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts @@ -157,6 +157,24 @@ export default class extends Endpoint { // eslint- prevGroupedNotification.id = notification.id; continue; } + if (prev.type === 'note' && notification.type === 'note') { + if (prevGroupedNotification.type !== 'note:grouped') { + groupedNotifications[groupedNotifications.length - 1] = { + type: 'note:grouped', + id: '', + createdAt: notification.createdAt, + noteIds: [notification.noteId], + notifierIds: [prev.notifierId!], + }; + prevGroupedNotification = groupedNotifications.at(-1)!; + } + if (!(prevGroupedNotification as FilterUnionByProperty).notifierIds.includes(notification.notifierId)) { + (prevGroupedNotification as FilterUnionByProperty).notifierIds.push(notification.notifierId!); + } + (prevGroupedNotification as FilterUnionByProperty).noteIds.push(notification.noteId!); + prevGroupedNotification.id = notification.id; + continue; + } groupedNotifications.push(notification); } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index ffe44c0507..6703fcdfba 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -4,7 +4,7 @@ */ import RE2 from 're2'; -import * as mfm from 'cfm-js'; +import * as mfm from 'mfc-js'; import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { JSDOM } from 'jsdom'; diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts new file mode 100644 index 0000000000..ecdfa4bf2e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts @@ -0,0 +1,393 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { In } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import { isPureRenote } from 'cherrypick-js/note.js'; +import type { MiUser } from '@/models/User.js'; +import type { + UsersRepository, + NotesRepository, + BlockingsRepository, + DriveFilesRepository, + ChannelsRepository, + NoteScheduleRepository, +} from '@/models/_.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { MiNote } from '@/models/Note.js'; +import type { MiChannel } from '@/models/Channel.js'; +import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { QueueService } from '@/core/QueueService.js'; +import { IdService } from '@/core/IdService.js'; +import { MiScheduleNoteType } from '@/models/NoteSchedule.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + + prohibitMoved: true, + + limit: { + duration: ms('1hour'), + max: 300, + }, + + kind: 'write:notes-schedule', + + errors: { + scheduleNoteMax: { + message: 'Schedule note max.', + code: 'SCHEDULE_NOTE_MAX', + id: '168707c3-e7da-4031-989e-f42aa3a274b2', + }, + noSuchRenoteTarget: { + message: 'No such renote target.', + code: 'NO_SUCH_RENOTE_TARGET', + id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4', + }, + + cannotReRenote: { + message: 'You can not Renote a pure Renote.', + code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE', + id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a', + }, + + cannotRenoteDueToVisibility: { + message: 'You can not Renote due to target visibility.', + code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY', + id: 'be9529e9-fe72-4de0-ae43-0b363c4938af', + }, + + noSuchReplyTarget: { + message: 'No such reply target.', + code: 'NO_SUCH_REPLY_TARGET', + id: '749ee0f6-d3da-459a-bf02-282e2da4292c', + }, + + cannotReplyToPureRenote: { + message: 'You can not reply to a pure Renote.', + code: 'CANNOT_REPLY_TO_A_PURE_RENOTE', + id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15', + }, + + cannotCreateAlreadyExpiredPoll: { + message: 'Poll is already expired.', + code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', + id: '04da457d-b083-4055-9082-955525eda5a5', + }, + + cannotCreateAlreadyExpiredSchedule: { + message: 'Schedule is already expired.', + code: 'CANNOT_CREATE_ALREADY_EXPIRED_SCHEDULE', + id: '8a9bfb90-fc7e-4878-a3e8-d97faaf5fb07', + }, + + noSuchChannel: { + message: 'No such channel.', + code: 'NO_SUCH_CHANNEL', + id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb', + }, + noSuchSchedule: { + message: 'No such schedule.', + code: 'NO_SUCH_SCHEDULE', + id: '44dee229-8da1-4a61-856d-e3a4bbc12032', + }, + youHaveBeenBlocked: { + message: 'You have been blocked by this user.', + code: 'YOU_HAVE_BEEN_BLOCKED', + id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3', + }, + + noSuchFile: { + message: 'Some files are not found.', + code: 'NO_SUCH_FILE', + id: 'b6992544-63e7-67f0-fa7f-32444b1b5306', + }, + + cannotRenoteOutsideOfChannel: { + message: 'Cannot renote outside of channel.', + code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL', + id: '33510210-8452-094c-6227-4a6c05d99f00', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' }, + visibleUserIds: { type: 'array', uniqueItems: true, items: { + type: 'string', format: 'misskey:id', + } }, + cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, + disableRightClick: { type: 'boolean', default: false }, + noExtractMentions: { type: 'boolean', default: false }, + noExtractHashtags: { type: 'boolean', default: false }, + noExtractEmojis: { type: 'boolean', default: false }, + replyId: { type: 'string', format: 'misskey:id', nullable: true }, + renoteId: { type: 'string', format: 'misskey:id', nullable: true }, + + // anyOf内にバリデーションを書いても最初の一つしかチェックされない + // See https://github.com/misskey-dev/misskey/pull/10082 + text: { + type: 'string', + minLength: 1, + maxLength: MAX_NOTE_TEXT_LENGTH, + nullable: true, + }, + fileIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + mediaIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + poll: { + type: 'object', + nullable: true, + properties: { + choices: { + type: 'array', + uniqueItems: true, + minItems: 2, + maxItems: 10, + items: { type: 'string', minLength: 1, maxLength: 50 }, + }, + multiple: { type: 'boolean' }, + expiresAt: { type: 'integer', nullable: true }, + expiredAfter: { type: 'integer', nullable: true, minimum: 1 }, + }, + required: ['choices'], + }, + event: { + type: 'object', + nullable: true, + properties: { + title: { type: 'string', minLength: 1, maxLength: 128, nullable: false }, + start: { type: 'integer', nullable: false }, + end: { type: 'integer', nullable: true }, + metadata: { type: 'object' }, + }, + }, + scheduleNote: { + type: 'object', + nullable: false, + properties: { + scheduledAt: { type: 'integer', nullable: false }, + }, + }, + }, + // (re)note with text, files and poll are optional + anyOf: [ + { required: ['text'] }, + { required: ['renoteId'] }, + { required: ['fileIds'] }, + { required: ['mediaIds'] }, + { required: ['poll'] }, + ], + required: ['scheduleNote'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.noteScheduleRepository) + private noteScheduleRepository: NoteScheduleRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + private queueService: QueueService, + private roleService: RoleService, + private idService: IdService, + ) { + super({ + ...meta, + }, paramDef, async (ps, me) => { + const scheduleNoteCount = await this.noteScheduleRepository.countBy({ userId: me.id }); + const scheduleNoteMax = (await this.roleService.getUserPolicies(me.id)).scheduleNoteMax; + if (scheduleNoteCount >= scheduleNoteMax) { + throw new ApiError(meta.errors.scheduleNoteMax); + } + let visibleUsers: MiUser[] = []; + if (ps.visibleUserIds) { + visibleUsers = await this.usersRepository.findBy({ + id: In(ps.visibleUserIds), + }); + } + + let files: MiDriveFile[] = []; + const fileIds = ps.fileIds ?? ps.mediaIds ?? null; + if (fileIds != null) { + files = await this.driveFilesRepository.createQueryBuilder('file') + .where('file.userId = :userId AND file.id IN (:...fileIds)', { + userId: me.id, + fileIds, + }) + .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') + .setParameters({ fileIds }) + .getMany(); + + if (files.length !== fileIds.length) { + throw new ApiError(meta.errors.noSuchFile); + } + } + + let renote: MiNote | null = null; + if (ps.renoteId != null) { + // Fetch renote to note + renote = await this.notesRepository.findOneBy({ id: ps.renoteId }); + + if (renote == null) { + throw new ApiError(meta.errors.noSuchRenoteTarget); + } else if (isPureRenote(renote)) { + throw new ApiError(meta.errors.cannotReRenote); + } + + // Check blocking + if (renote.userId !== me.id) { + const blockExist = await this.blockingsRepository.exist({ + where: { + blockerId: renote.userId, + blockeeId: me.id, + }, + }); + if (blockExist) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } + + if (renote.visibility === 'followers' && renote.userId !== me.id) { + // 他人のfollowers noteはreject + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } else if (renote.visibility === 'specified') { + // specified / direct noteはreject + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } + } + + let reply: MiNote | null = null; + if (ps.replyId != null) { + // Fetch reply + reply = await this.notesRepository.findOneBy({ id: ps.replyId }); + + if (reply == null) { + throw new ApiError(meta.errors.noSuchReplyTarget); + } else if (isPureRenote(reply)) { + throw new ApiError(meta.errors.cannotReplyToPureRenote); + } + + // Check blocking + if (reply.userId !== me.id) { + const blockExist = await this.blockingsRepository.exist({ + where: { + blockerId: reply.userId, + blockeeId: me.id, + }, + }); + if (blockExist) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } + } + + if (ps.poll) { + let scheduleNote_scheduledAt = Date.now(); + if (typeof ps.scheduleNote.scheduledAt === 'number') { + scheduleNote_scheduledAt = ps.scheduleNote.scheduledAt; + } + if (typeof ps.poll.expiresAt === 'number') { + if (ps.poll.expiresAt < scheduleNote_scheduledAt) { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); + } + } else if (typeof ps.poll.expiredAfter === 'number') { + ps.poll.expiresAt = scheduleNote_scheduledAt + ps.poll.expiredAfter; + } + } + if (typeof ps.scheduleNote.scheduledAt === 'number') { + if (ps.scheduleNote.scheduledAt < Date.now()) { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredSchedule); + } + } else { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredSchedule); + } + const note:MiScheduleNoteType = { + createdAt: new Date(ps.scheduleNote.scheduledAt!).toISOString(), + files: files.map(f => f.id), + poll: ps.poll ? { + choices: ps.poll.choices, + multiple: ps.poll.multiple ?? false, + expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt).toISOString() : null, + } : undefined, + text: ps.text ?? undefined, + reply: reply?.id, + renote: renote?.id, + cw: ps.cw, + localOnly: false, + reactionAcceptance: ps.reactionAcceptance, + visibility: ps.visibility, + visibleUsers, + apMentions: ps.noExtractMentions ? [] : undefined, + apHashtags: ps.noExtractHashtags ? [] : undefined, + apEmojis: ps.noExtractEmojis ? [] : undefined, + event: ps.event ? { + start: new Date(ps.event.start!).toISOString(), + end: ps.event.end ? new Date(ps.event.end).toISOString() : null, + title: ps.event.title!, + metadata: ps.event.metadata ?? {}, + } : undefined, + disableRightClick: ps.disableRightClick, + }; + + if (ps.scheduleNote.scheduledAt) { + me.token = null; + const noteId = this.idService.gen(new Date().getTime()); + await this.noteScheduleRepository.insert({ + id: noteId, + note: note, + userId: me.id, + scheduledAt: new Date(ps.scheduleNote.scheduledAt), + }); + + const delay = new Date(ps.scheduleNote.scheduledAt).getTime() - Date.now(); + await this.queueService.ScheduleNotePostQueue.add(String(delay), { + scheduleNoteId: noteId, + }, { + delay, + removeOnComplete: true, + jobId: noteId, + }); + } + + return ''; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts b/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts new file mode 100644 index 0000000000..df406f99f0 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { Inject, Injectable } from '@nestjs/common'; +import type { NoteScheduleRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { QueueService } from '@/core/QueueService.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + kind: 'write:notes-schedule', + + limit: { + duration: ms('1hour'), + max: 300, + }, + + errors: { + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: 'a58056ba-8ba1-4323-8ebf-e0b585bc244f', + }, + permissionDenied: { + message: 'Permission denied.', + code: 'PERMISSION_DENIED', + id: 'c0da2fed-8f61-4c47-a41d-431992607b5c', + httpStatusCode: 403, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + noteId: { type: 'string', format: 'misskey:id' }, + }, + required: ['noteId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.noteScheduleRepository) + private noteScheduleRepository: NoteScheduleRepository, + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.noteScheduleRepository.findOneBy({ id: ps.noteId }); + if (note === null) { + throw new ApiError(meta.errors.noSuchNote); + } + if (note.userId !== me.id) { + throw new ApiError(meta.errors.permissionDenied); + } + await this.noteScheduleRepository.delete({ id: ps.noteId }); + await this.queueService.ScheduleNotePostQueue.remove(ps.noteId); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts new file mode 100644 index 0000000000..88da4f4043 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts @@ -0,0 +1,128 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import type { MiNote, MiNoteSchedule, NoteScheduleRepository } from '@/models/_.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { QueryService } from '@/core/QueryService.js'; +import { Packed } from '@/misc/json-schema.js'; +import { noteVisibilities } from '@/types.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + kind: 'read:notes-schedule', + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + id: { type: 'string', format: 'misskey:id', optional: false, nullable: false }, + note: { + type: 'object', + optional: false, nullable: false, + properties: { + createdAt: { type: 'string', optional: false, nullable: false }, + text: { type: 'string', optional: true, nullable: false }, + cw: { type: 'string', optional: true, nullable: true }, + fileIds: { type: 'array', optional: false, nullable: false, items: { type: 'string', format: 'misskey:id', optional: false, nullable: false } }, + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], optional: false, nullable: false }, + visibleUsers: { + type: 'array', optional: false, nullable: false, items: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, + }, + user: { + type: 'object', + optional: false, nullable: false, + ref: 'User', + }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, + isSchedule: { type: 'boolean', optional: false, nullable: false }, + }, + }, + userId: { type: 'string', optional: false, nullable: false }, + scheduledAt: { type: 'string', optional: false, nullable: false }, + }, + }, + }, + limit: { + duration: ms('1hour'), + max: 300, + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.noteScheduleRepository) + private noteScheduleRepository: NoteScheduleRepository, + + private userEntityService: UserEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.noteScheduleRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.userId = :userId', { userId: me.id }); + const scheduleNotes = await query.limit(ps.limit).getMany(); + const user = await this.userEntityService.pack(me, me); + const scheduleNotesPack: { + id: string; + note: { + text?: string; + cw?: string|null; + fileIds: string[]; + visibility: typeof noteVisibilities[number]; + visibleUsers: Packed<'UserLite'>[]; + reactionAcceptance: MiNote['reactionAcceptance']; + user: Packed<'User'>; + createdAt: string; + isSchedule: boolean; + }; + userId: string; + scheduledAt: string; + }[] = await Promise.all(scheduleNotes.map(async (item: MiNoteSchedule) => { + return { + ...item, + scheduledAt: item.scheduledAt.toISOString(), + note: { + ...item.note, + text: item.note.text ?? '', + user: user, + visibility: item.note.visibility ?? 'public', + reactionAcceptance: item.note.reactionAcceptance ?? null, + visibleUsers: item.note.visibleUsers ? await userEntityService.packMany(item.note.visibleUsers.map(u => u.id), me) : [], + fileIds: item.note.files ? item.note.files : [], + createdAt: item.scheduledAt.toISOString(), + isSchedule: true, + id: item.id, + }, + }; + })); + + return scheduleNotesPack; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts index fecd0baf21..38f10685ac 100644 --- a/packages/backend/src/server/api/endpoints/notes/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/update.ts @@ -23,7 +23,7 @@ export const meta = { kind: 'write:notes', limit: { - duration: ms('1hour'), + duration: ms('5min'), max: 10, minInterval: ms('1sec'), }, diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index bb6d9bf163..fdbb2c0169 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -34,6 +34,7 @@ import type { UserWebhookDeliverQueue, SystemWebhookDeliverQueue, ScheduledNoteDeleteQueue, + ScheduleNotePostQueue, } from '@/core/QueueModule.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -126,6 +127,7 @@ export class ClientServerService { @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, @Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue, + @Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue, ) { //this.createServer = this.createServer.bind(this); } @@ -196,6 +198,7 @@ export class ClientServerService { serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg', infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg', notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg', + youBlockedImageUrl: meta.youBlockedImageUrl ?? 'https://xn--931a.moe/assets/error.jpg', instanceUrl: this.config.url, metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)), now: Date.now(), @@ -255,6 +258,7 @@ export class ClientServerService { this.userWebhookDeliverQueue, this.systemWebhookDeliverQueue, this.scheduledNoteDeleteQueue, + this.scheduleNotePostQueue, ].map(q => new BullMQAdapter(q)), serverAdapter: bullBoardServerAdapter, }); diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts index 086dda04d6..93cf8d3fe9 100644 --- a/packages/backend/src/server/web/FeedService.ts +++ b/packages/backend/src/server/web/FeedService.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, IsNull } from 'typeorm'; import { Feed } from 'feed'; -import { parse as mfmParse } from 'cfm-js'; +import { parse as mfmParse } from 'mfc-js'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, NotesRepository, UserProfilesRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 805d060919..e7c9e3b3ad 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -44,6 +44,7 @@ export const groupedNotificationTypes = [ ...notificationTypes, 'reaction:grouped', 'renote:grouped', + 'note:grouped', ] as const; export const obsoleteNotificationTypes = ['pollVote'/*, 'groupInvited'*/] as const; diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts index a36a5cfcf1..26270a9c1c 100644 --- a/packages/backend/test/unit/MfmService.ts +++ b/packages/backend/test/unit/MfmService.ts @@ -4,7 +4,7 @@ */ import * as assert from 'assert'; -import * as mfm from 'cfm-js'; +import * as mfm from 'mfc-js'; import { Test } from '@nestjs/testing'; import { CoreModule } from '@/core/CoreModule.js'; diff --git a/packages/backend/test/unit/extract-mentions.ts b/packages/backend/test/unit/extract-mentions.ts index 63b91afbc1..1d6d2d2e16 100644 --- a/packages/backend/test/unit/extract-mentions.ts +++ b/packages/backend/test/unit/extract-mentions.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; -import { parse } from 'cfm-js'; +import { parse } from 'mfc-js'; import { extractMentions } from '@/misc/extract-mentions.js'; describe('Extract mentions', () => { diff --git a/packages/cherrypick-js/etc/cherrypick-js.api.md b/packages/cherrypick-js/etc/cherrypick-js.api.md index af0637b38e..3ee013adeb 100644 --- a/packages/cherrypick-js/etc/cherrypick-js.api.md +++ b/packages/cherrypick-js/etc/cherrypick-js.api.md @@ -1717,6 +1717,10 @@ declare namespace entities { NotesRenotesResponse, NotesRepliesRequest, NotesRepliesResponse, + NotesScheduleCreateRequest, + NotesScheduleDeleteRequest, + NotesScheduleListRequest, + NotesScheduleListResponse, NotesSearchByTagRequest, NotesSearchByTagResponse, NotesSearchRequest, @@ -2862,6 +2866,18 @@ type NotesRequest = operations['notes']['requestBody']['content']['application/j // @public (undocumented) type NotesResponse = operations['notes']['responses']['200']['content']['application/json']; +// @public (undocumented) +type NotesScheduleCreateRequest = operations['notes___schedule___create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesScheduleDeleteRequest = operations['notes___schedule___delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesScheduleListRequest = operations['notes___schedule___list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesScheduleListResponse = operations['notes___schedule___list']['responses']['200']['content']['application/json']; + // @public (undocumented) type NotesSearchByTagRequest = operations['notes___search-by-tag']['requestBody']['content']['application/json']; @@ -2989,7 +3005,7 @@ type PartialRolePolicyOverride = Partial<{ }>; // @public (undocumented) -export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "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", "read:admin:meta", "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", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "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:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; +export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notes-schedule", "write:notes-schedule", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "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", "read:admin:meta", "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", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "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:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; // @public (undocumented) type PingResponse = operations['ping']['responses']['200']['content']['application/json']; diff --git a/packages/cherrypick-js/package.json b/packages/cherrypick-js/package.json index 91f83894af..d438d48ad9 100644 --- a/packages/cherrypick-js/package.json +++ b/packages/cherrypick-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "cherrypick-js", - "version": "4.12.0", + "version": "4.12.1", "basedMisskeyVersion": "2024.9.0", "description": "CherryPick SDK for JavaScript", "license": "MIT", diff --git a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts index 6bb89d432f..b496cdff06 100644 --- a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts +++ b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts @@ -3409,6 +3409,39 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes-schedule* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes-schedule* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:notes-schedule* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/cherrypick-js/src/autogen/endpoint.ts b/packages/cherrypick-js/src/autogen/endpoint.ts index 18a2ec0f72..5571be9fbe 100644 --- a/packages/cherrypick-js/src/autogen/endpoint.ts +++ b/packages/cherrypick-js/src/autogen/endpoint.ts @@ -457,6 +457,10 @@ import type { NotesRenotesResponse, NotesRepliesRequest, NotesRepliesResponse, + NotesScheduleCreateRequest, + NotesScheduleDeleteRequest, + NotesScheduleListRequest, + NotesScheduleListResponse, NotesSearchByTagRequest, NotesSearchByTagResponse, NotesSearchRequest, @@ -926,6 +930,9 @@ export type Endpoints = { 'notes/reactions/delete': { req: NotesReactionsDeleteRequest; res: EmptyResponse }; 'notes/renotes': { req: NotesRenotesRequest; res: NotesRenotesResponse }; 'notes/replies': { req: NotesRepliesRequest; res: NotesRepliesResponse }; + 'notes/schedule/create': { req: NotesScheduleCreateRequest; res: EmptyResponse }; + 'notes/schedule/delete': { req: NotesScheduleDeleteRequest; res: EmptyResponse }; + 'notes/schedule/list': { req: NotesScheduleListRequest; res: NotesScheduleListResponse }; 'notes/search-by-tag': { req: NotesSearchByTagRequest; res: NotesSearchByTagResponse }; 'notes/search': { req: NotesSearchRequest; res: NotesSearchResponse }; 'notes/show': { req: NotesShowRequest; res: NotesShowResponse }; diff --git a/packages/cherrypick-js/src/autogen/entities.ts b/packages/cherrypick-js/src/autogen/entities.ts index 16f6073c0d..f8fb57377f 100644 --- a/packages/cherrypick-js/src/autogen/entities.ts +++ b/packages/cherrypick-js/src/autogen/entities.ts @@ -460,6 +460,10 @@ export type NotesRenotesRequest = operations['notes___renotes']['requestBody'][' export type NotesRenotesResponse = operations['notes___renotes']['responses']['200']['content']['application/json']; export type NotesRepliesRequest = operations['notes___replies']['requestBody']['content']['application/json']; export type NotesRepliesResponse = operations['notes___replies']['responses']['200']['content']['application/json']; +export type NotesScheduleCreateRequest = operations['notes___schedule___create']['requestBody']['content']['application/json']; +export type NotesScheduleDeleteRequest = operations['notes___schedule___delete']['requestBody']['content']['application/json']; +export type NotesScheduleListRequest = operations['notes___schedule___list']['requestBody']['content']['application/json']; +export type NotesScheduleListResponse = operations['notes___schedule___list']['responses']['200']['content']['application/json']; export type NotesSearchByTagRequest = operations['notes___search-by-tag']['requestBody']['content']['application/json']; export type NotesSearchByTagResponse = operations['notes___search-by-tag']['responses']['200']['content']['application/json']; export type NotesSearchRequest = operations['notes___search']['requestBody']['content']['application/json']; diff --git a/packages/cherrypick-js/src/autogen/types.ts b/packages/cherrypick-js/src/autogen/types.ts index 3dd0e714c7..3b7ad95849 100644 --- a/packages/cherrypick-js/src/autogen/types.ts +++ b/packages/cherrypick-js/src/autogen/types.ts @@ -2948,6 +2948,33 @@ export type paths = { */ post: operations['notes___replies']; }; + '/notes/schedule/create': { + /** + * notes/schedule/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes-schedule* + */ + post: operations['notes___schedule___create']; + }; + '/notes/schedule/delete': { + /** + * notes/schedule/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes-schedule* + */ + post: operations['notes___schedule___delete']; + }; + '/notes/schedule/list': { + /** + * notes/schedule/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:notes-schedule* + */ + post: operations['notes___schedule___list']; + }; '/notes/search-by-tag': { /** * notes/search-by-tag @@ -5195,6 +5222,7 @@ export type components = { canImportMuting: boolean; canImportUserLists: boolean; canEditNote: boolean; + scheduleNoteMax: number; }; ReversiGameLite: { /** Format: id */ @@ -5306,6 +5334,7 @@ export type components = { serverErrorImageUrl: string | null; infoImageUrl: string | null; notFoundImageUrl: string | null; + youBlockedImageUrl: string | null; iconUrl: string | null; maxNoteTextLength: number; ads: { @@ -5437,6 +5466,7 @@ export type operations = { serverErrorImageUrl: string | null; infoImageUrl: string | null; notFoundImageUrl: string | null; + youBlockedImageUrl: string | null; iconUrl: string | null; app192IconUrl: string | null; app512IconUrl: string | null; @@ -5482,18 +5512,18 @@ export type operations = { objectStorageUseSSL: boolean; objectStorageUseProxy: boolean; objectStorageSetPublicRead: boolean; - useObjectStorageRemote?: boolean; - objectStorageRemoteBaseUrl?: string | null; - objectStorageRemoteBucket?: string | null; - objectStorageRemotePrefix?: string | null; - objectStorageRemoteEndpoint?: string | null; - objectStorageRemoteRegion?: string | null; - objectStorageRemotePort?: number | null; - objectStorageRemoteAccessKey?: string | null; - objectStorageRemoteSecretKey?: string | null; - objectStorageRemoteUseSSL?: boolean; - objectStorageRemoteUseProxy?: boolean; - objectStorageRemoteSetPublicRead?: boolean; + useRemoteObjectStorage: boolean; + remoteObjectStorageBaseUrl: string | null; + remoteObjectStorageBucket: string | null; + remoteObjectStoragePrefix: string | null; + remoteObjectStorageEndpoint: string | null; + remoteObjectStorageRegion: string | null; + remoteObjectStoragePort: number | null; + remoteObjectStorageAccessKey: string | null; + remoteObjectStorageSecretKey: string | null; + remoteObjectStorageUseSSL: boolean; + remoteObjectStorageUseProxy: boolean; + remoteObjectStorageSetPublicRead: boolean; enableIpLogging: boolean; enableActiveEmailValidation: boolean; enableVerifymailApi: boolean; @@ -5528,7 +5558,7 @@ export type operations = { name: string | null; shortName: string | null; objectStorageS3ForcePathStyle: boolean; - objectStorageRemoteS3ForcePathStyle: boolean; + remoteObjectStorageS3ForcePathStyle: boolean; privacyPolicyUrl: string | null; inquiryUrl: string | null; repositoryUrl: string | null; @@ -10124,6 +10154,7 @@ export type operations = { serverErrorImageUrl?: string | null; infoImageUrl?: string | null; notFoundImageUrl?: string | null; + youBlockedImageUrl?: string | null; iconUrl?: string | null; app192IconUrl?: string | null; app512IconUrl?: string | null; @@ -10198,19 +10229,19 @@ export type operations = { objectStorageUseProxy?: boolean; objectStorageSetPublicRead?: boolean; objectStorageS3ForcePathStyle?: boolean; - useObjectStorageRemote?: boolean; - objectStorageRemoteBaseUrl?: string | null; - objectStorageRemoteBucket?: string | null; - objectStorageRemotePrefix?: string | null; - objectStorageRemoteEndpoint?: string | null; - objectStorageRemoteRegion?: string | null; - objectStorageRemotePort?: number | null; - objectStorageRemoteAccessKey?: string | null; - objectStorageRemoteSecretKey?: string | null; - objectStorageRemoteUseSSL?: boolean; - objectStorageRemoteUseProxy?: boolean; - objectStorageRemoteSetPublicRead?: boolean; - objectStorageRemoteS3ForcePathStyle?: boolean; + useRemoteObjectStorage?: boolean; + remoteObjectStorageBaseUrl?: string | null; + remoteObjectStorageBucket?: string | null; + remoteObjectStoragePrefix?: string | null; + remoteObjectStorageEndpoint?: string | null; + remoteObjectStorageRegion?: string | null; + remoteObjectStoragePort?: number | null; + remoteObjectStorageAccessKey?: string | null; + remoteObjectStorageSecretKey?: string | null; + remoteObjectStorageUseSSL?: boolean; + remoteObjectStorageUseProxy?: boolean; + remoteObjectStorageSetPublicRead?: boolean; + remoteObjectStorageS3ForcePathStyle?: boolean; enableIpLogging?: boolean; enableActiveEmailValidation?: boolean; enableVerifymailApi?: boolean; @@ -19443,8 +19474,8 @@ export type operations = { untilId?: string; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'note:grouped' | 'pollVote')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'note:grouped' | 'pollVote')[]; }; }; }; @@ -23771,6 +23802,247 @@ export type operations = { }; }; }; + /** + * notes/schedule/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes-schedule* + */ + notes___schedule___create: { + requestBody: { + content: { + 'application/json': { + /** + * @default public + * @enum {string} + */ + visibility?: 'public' | 'home' | 'followers' | 'specified'; + visibleUserIds?: string[]; + cw?: string | null; + /** + * @default null + * @enum {string|null} + */ + reactionAcceptance?: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote'; + /** @default false */ + disableRightClick?: boolean; + /** @default false */ + noExtractMentions?: boolean; + /** @default false */ + noExtractHashtags?: boolean; + /** @default false */ + noExtractEmojis?: boolean; + /** Format: misskey:id */ + replyId?: string | null; + /** Format: misskey:id */ + renoteId?: string | null; + text?: string | null; + fileIds?: string[]; + mediaIds?: string[]; + poll?: ({ + choices: string[]; + multiple?: boolean; + expiresAt?: number | null; + expiredAfter?: number | null; + }) | null; + event?: ({ + title?: string; + start?: number; + end?: number | null; + metadata?: Record; + }) | null; + scheduleNote: { + scheduledAt?: number; + }; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/schedule/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes-schedule* + */ + notes___schedule___delete: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/schedule/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:notes-schedule* + */ + notes___schedule___list: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': ({ + /** Format: misskey:id */ + id: string; + note: { + createdAt: string; + text?: string; + cw?: string | null; + fileIds: string[]; + /** @enum {string} */ + visibility: 'public' | 'home' | 'followers' | 'specified'; + visibleUsers: components['schemas']['UserLite'][]; + user: components['schemas']['User']; + /** + * @default null + * @enum {string|null} + */ + reactionAcceptance: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote'; + isSchedule: boolean; + }; + userId: string; + scheduledAt: string; + })[]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * notes/search-by-tag * @description No description provided. diff --git a/packages/cherrypick-js/src/consts.ts b/packages/cherrypick-js/src/consts.ts index b4fbcffa97..fe64319128 100644 --- a/packages/cherrypick-js/src/consts.ts +++ b/packages/cherrypick-js/src/consts.ts @@ -42,6 +42,8 @@ export const permissions = [ 'read:mutes', 'write:mutes', 'write:notes', + 'read:notes-schedule', + 'write:notes-schedule', 'read:notifications', 'write:notifications', 'read:reactions', diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index 86fcdf0d81..ef7345cd49 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -24,7 +24,7 @@ "@vue/compiler-sfc": "3.5.10", "astring": "1.9.0", "buraha": "0.0.1", - "cfm-js": "0.24.0-cherrypick.8", + "mfc-js": "0.24.0-cherrypick.9", "cherrypick-js": "workspace:*", "estree-walker": "3.0.3", "frontend-shared": "workspace:*", @@ -32,6 +32,7 @@ "rollup": "4.22.5", "sass": "1.79.3", "shiki": "1.12.0", + "temml": "0.10.29", "tinycolor2": "1.6.0", "tsc-alias": "1.8.10", "tsconfig-paths": "4.2.0", diff --git a/packages/frontend-embed/src/components/EmInstanceTicker.vue b/packages/frontend-embed/src/components/EmInstanceTicker.vue index 6f72908079..6367f37577 100644 --- a/packages/frontend-embed/src/components/EmInstanceTicker.vue +++ b/packages/frontend-embed/src/components/EmInstanceTicker.vue @@ -48,11 +48,11 @@ $height: 2ex; display: flex; align-items: center; height: $height; - border-radius: .3rem; + border-radius: .5rem; overflow: clip; color: #000; margin-top: 5px; - padding: 1px 3px 1px 0; + padding: 1px 5px 1px 0; text-shadow: /* .866 ≈ sin(60deg) */ 1px 0 1px #000, .866px .5px 1px #000, diff --git a/packages/frontend-embed/src/components/EmMfm.ts b/packages/frontend-embed/src/components/EmMfm.ts index 7544f66d19..01f3df3988 100644 --- a/packages/frontend-embed/src/components/EmMfm.ts +++ b/packages/frontend-embed/src/components/EmMfm.ts @@ -4,8 +4,9 @@ */ import { VNode, h, SetupContext, provide } from 'vue'; -import * as mfm from 'cfm-js'; +import * as mfm from 'mfc-js'; import * as Misskey from 'cherrypick-js'; +import temml from 'temml/dist/temml.mjs'; import { host } from '@@/js/config.js'; import EmUrl from '@/components/EmUrl.vue'; import EmTime from '@/components/EmTime.vue'; @@ -224,6 +225,9 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext= 2.5, + menu: props.enableEmojiMenu, + menuReaction: props.enableEmojiMenuReaction, })]; } } @@ -428,11 +434,15 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext import { computed, inject, ref, shallowRef } from 'vue'; -import * as mfm from 'cfm-js'; +import * as mfm from 'mfc-js'; import * as Misskey from 'cherrypick-js'; import { shouldCollapsed, shouldMfmCollapsed } from '@@/js/collapsed.js'; import { url } from '@@/js/config.js'; diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue index dd521e9b1e..89f03cb20f 100644 --- a/packages/frontend-embed/src/components/EmNoteDetailed.vue +++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue @@ -155,7 +155,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index 9839182d91..ccf4e5018e 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue index fdc1d7d217..61b0399e96 100644 --- a/packages/frontend/src/components/MkUpdated.vue +++ b/packages/frontend/src/components/MkUpdated.vue @@ -17,10 +17,11 @@ SPDX-License-Identifier: AGPL-3.0-only
✨{{ version }}🚀
{{ basedMisskeyVersion }}
- {{ i18n.ts.whatIsNew }} + {{ i18n.ts.whatIsNew }} {{ i18n.ts.gotIt }} + diff --git a/packages/frontend/src/pages/messaging/messaging-room.message.vue b/packages/frontend/src/pages/messaging/messaging-room.message.vue index b3b3e701bc..5ecd6a3b76 100644 --- a/packages/frontend/src/pages/messaging/messaging-room.message.vue +++ b/packages/frontend/src/pages/messaging/messaging-room.message.vue @@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index 0f87d9617a..7b396ee275 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -11,6 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ +
@@ -37,6 +40,7 @@ import { globalEvents } from '@/events.js'; const tab = ref('all'); const includeTypes = ref(null); const excludeTypes = computed(() => includeTypes.value ? notificationTypes.filter(t => !includeTypes.value.includes(t)) : undefined); +const newNoteExcludeTypes = computed(() => notificationTypes.filter(t => !['note'].includes(t))); const props = defineProps<{ disableRefreshButton?: boolean; @@ -96,6 +100,10 @@ const headerTabs = computed(() => [{ key: 'all', title: i18n.ts.all, icon: 'ti ti-point', +}, { + key: 'newNote', + title: i18n.ts.newNotes, + icon: 'ti ti-pencil', }, { key: 'mentions', title: i18n.ts.mentions, diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index 04e420e1bf..5c07c2b9e7 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -176,7 +176,7 @@ import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { deepClone } from '@/scripts/clone.js'; -import { signinRequired } from '@/account.js'; +import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { userPage } from '@/filters/user.js'; @@ -187,8 +187,6 @@ import { confetti } from '@/scripts/confetti.js'; import { defaultStore } from '@/store.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; -const $i = signinRequired(); - const props = defineProps<{ game: Misskey.entities.ReversiGameDetailed; connection?: Misskey.ChannelConnection | null; @@ -210,14 +208,14 @@ const engine = shallowRef(Reversi.Serializer.restoreGame({ })); const iAmPlayer = computed(() => { - return game.value.user1Id === $i.id || game.value.user2Id === $i.id; + return game.value.user1Id === $i?.id || game.value.user2Id === $i?.id; }); // true: 黒, false: 白 const myColor = computed(() => { if (!iAmPlayer.value) return null; - if (game.value.user1Id === $i.id && game.value.black === 1) return true; - if (game.value.user2Id === $i.id && game.value.black === 2) return true; + if (game.value.user1Id === $i?.id && game.value.black === 1) return true; + if (game.value.user2Id === $i?.id && game.value.black === 2) return true; return false; }); @@ -248,7 +246,7 @@ const isMyTurn = computed(() => { if (!iAmPlayer.value) return false; const u = turnUser.value; if (u == null) return false; - return u.id === $i.id; + return u.id === $i?.id; }); const cellsStyle = computed(() => { @@ -394,7 +392,7 @@ async function onStreamLog(log) { function onStreamEnded(x) { game.value = deepClone(x.game); - if (game.value.winnerId === $i.id) { + if (game.value.winnerId === $i?.id) { confetti({ duration: 1000 * 3, }); @@ -513,7 +511,7 @@ function getKey(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef): // 既にでている絵文字をクリックした際の挙動 function onReactionEmojiClick(emoji: string, ev?: MouseEvent) { - console.log('emoji click'); + if (_DEV_) console.log('emoji click'); const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); @@ -535,7 +533,7 @@ function onReactionEmojiClick(emoji: string, ev?: MouseEvent) { const reactButton = ref(null); function onReactionPickerClick() { - reactionPicker.show(reactButton.value ?? null, reaction => { + reactionPicker.show(reactButton.value ?? null, null, reaction => { const key = getKey(reaction); sendReaction(key); @@ -547,7 +545,7 @@ function onReactionPickerClick() { // #endregion function sendReaction(emojiKey: string) { - console.log('send emoji'); + if (_DEV_) console.log('send emoji'); if (!canReact.value) return; canReact.value = false; @@ -568,25 +566,25 @@ function sendReaction(emojiKey: string) { function onReacted(payload: Parameters['0']) { const { userId, reaction } = payload; - if (showReaction.value || userId === $i.id) { + if (showReaction.value || userId === $i?.id) { sound.playMisskeySfx('reaction'); const el = (userId === blackUser.value.id) ? blackUserEl.value : whiteUserEl.value; if (el) { const rect = el.getBoundingClientRect(); - const x = rect.right; + const x = (userId === blackUser.value.id) ? rect.left - (el.offsetWidth * 1.8) : rect.right; const y = rect.bottom; os.popup(XEmojiBalloon, { reaction, - tail: 'left', + tail: (userId === blackUser.value.id) ? 'right' : 'left', x, y, }, {}, 'end'); } } - if (userId === $i.id) { + if (userId === $i?.id) { // リアクションが可能になるまでのタイマー window.setTimeout(() => { if (canReactFallbackTimer != null) { diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue index e76d97f8db..aab709f4c5 100644 --- a/packages/frontend/src/pages/reversi/game.setting.vue +++ b/packages/frontend/src/pages/reversi/game.setting.vue @@ -115,7 +115,7 @@ import * as Misskey from 'cherrypick-js'; import * as Reversi from 'misskey-reversi'; import type { MenuItem } from '@/types/menu.js'; import { i18n } from '@/i18n.js'; -import { signinRequired } from '@/account.js'; +import { $i } from '@/account.js'; import { deepClone } from '@/scripts/clone.js'; import MkButton from '@/components/MkButton.vue'; import MkRadios from '@/components/MkRadios.vue'; @@ -124,8 +124,6 @@ import MkFolder from '@/components/MkFolder.vue'; import * as os from '@/os.js'; import { useRouter } from '@/router/supplier.js'; -const $i = signinRequired(); - const router = useRouter(); const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.category))); @@ -145,13 +143,13 @@ const mapName = computed(() => { return found ? found.name! : '-Custom-'; }); const isReady = computed(() => { - if (game.value.user1Id === $i.id && game.value.user1Ready) return true; - if (game.value.user2Id === $i.id && game.value.user2Ready) return true; + if (game.value.user1Id === $i?.id && game.value.user1Ready) return true; + if (game.value.user2Id === $i?.id && game.value.user2Ready) return true; return false; }); const isOpReady = computed(() => { - if (game.value.user1Id !== $i.id && game.value.user1Ready) return true; - if (game.value.user2Id !== $i.id && game.value.user2Ready) return true; + if (game.value.user1Id !== $i?.id && game.value.user1Ready) return true; + if (game.value.user2Id !== $i?.id && game.value.user2Ready) return true; return false; }); @@ -225,7 +223,7 @@ function updateSettings(key: keyof Misskey.entities.ReversiGameDetailed) { } function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof Misskey.entities.ReversiGameDetailed; value: any; }) { - if (userId === $i.id) return; + if (userId === $i?.id) return; if (game.value[key] === value) return; game.value[key] = value; if (isReady.value) { diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue index 6e106b8dc2..ee4a0af2f7 100644 --- a/packages/frontend/src/pages/reversi/game.vue +++ b/packages/frontend/src/pages/reversi/game.vue @@ -19,13 +19,11 @@ import GameBoard from './game.board.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { useStream } from '@/stream.js'; -import { signinRequired } from '@/account.js'; +import { $i } from '@/account.js'; import { useRouter } from '@/router/supplier.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -const $i = signinRequired(); - const router = useRouter(); const props = defineProps<{ @@ -74,7 +72,7 @@ async function fetchGame() { connection.value.on('canceled', x => { connection.value?.dispose(); - if (x.userId !== $i.id) { + if (x.userId !== $i?.id) { os.alert({ type: 'warning', text: i18n.ts._reversi.gameCanceled, diff --git a/packages/frontend/src/pages/search.event.vue b/packages/frontend/src/pages/search.event.vue index b662fd8340..e4ea887dc9 100644 --- a/packages/frontend/src/pages/search.event.vue +++ b/packages/frontend/src/pages/search.event.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only