diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml index f04888e98..d2cf4bf62 100644 --- a/.github/workflows/docker-develop.yml +++ b/.github/workflows/docker-develop.yml @@ -16,16 +16,16 @@ jobs: uses: actions/checkout@v3.3.0 - name: Docker meta id: meta - uses: docker/metadata-action@v3 + uses: docker/metadata-action@v4 with: images: misskey/misskey - name: Log in to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and Push to Docker Hub - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . push: true diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 84d36f846..d7803ce3e 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v3.3.0 - name: Docker meta id: meta - uses: docker/metadata-action@v3 + uses: docker/metadata-action@v4 with: images: misskey/misskey tags: | @@ -26,12 +26,12 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - name: Log in to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and Push to Docker Hub - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . push: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ef711d57..ca8379bcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,23 @@ You should also include the user name that made the change. --> +## 13.1.0 (2023/01/21) + +### Improvements +- 実績機能 +- Playのプリセットを追加 +- Playのscriptの文字数制限を緩和 +- AiScript GUIの強化 +- リアクション一覧詳細ダイアログを表示できるように +- 存在しないカスタム絵文字をテキストで表示するように +- Alt text in image viewer +- ジョブキューのプロセスとWebサーバーのプロセスを分離 + +### Bugfixes +- playを削除する手段がなかったのを修正 +- The … button on notes does nothing when not logged in +- twitterと連携するときに autwh is not a function になるのを修正 + ## 13.0.0 (2023/01/16) ### TL;DR @@ -32,13 +49,16 @@ You should also include the user name that made the change. - Elasticsearchのサポートが削除されました - 代わりに今後任意の検索プロバイダを設定できる仕組みを構想しています。その仕組みを使えば今まで通りElasticsearchも利用できます - Yarnからpnpmに移行されました + corepackの有効化を推奨します: `sudo corepack enable` - インスタンスブロックはサブドメインにも適用されるようになります - ロールの導入に伴い、いくつかの機能がロールと統合されました - モデレーターはロールに統合されました。今までのモデレーター情報は失われるため、予めモデレーター一覧を記録しておき、アップデート後にモデレーターロールを作りアサインし直してください。 - サイレンスはロールに統合されました。今までのユーザーは恩赦されるため、予めサイレンス一覧を記録しておくのをおすすめします。 - ユーザーごとのドライブ容量設定はロールに統合されました。 - - インスタンスデフォルトのドライブ容量設定はロールに統合されました。アップデート後、ベースロールのドライブ容量を編集してください。 + - インスタンスデフォルトのドライブ容量設定はロールに統合されました。アップデート後、ベースロールもしくはコンディショナルロールでドライブ容量を編集してください。 - LTL/GTLの解放状態はロールに統合されました。 +- Dockerの実行をrootで行わないようにしました。Dockerかつオブジェクトストレージを使用していない場合は`chown -hR 991.991 ./files`を実行してください。 + https://github.com/misskey-dev/misskey/pull/9560 #### For users - ノートのウォッチ機能が削除されました diff --git a/Dockerfile b/Dockerfile index 175be0fdb..47fe31bca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,8 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends \ build-essential +RUN corepack enable + WORKDIR /misskey COPY ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] @@ -14,7 +16,6 @@ COPY ["packages/backend/package.json", "./packages/backend/"] COPY ["packages/frontend/package.json", "./packages/frontend/"] COPY ["packages/sw/package.json", "./packages/sw/"] -RUN npm i -g pnpm RUN pnpm i --frozen-lockfile COPY . ./ @@ -34,10 +35,10 @@ RUN apt-get update \ ffmpeg tini \ && apt-get -y clean \ && rm -rf /var/lib/apt/lists/* \ + && corepack enable \ && groupadd -g "${GID}" misskey \ && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey -RUN npm i -g pnpm USER misskey WORKDIR /misskey diff --git a/docker-compose.yml b/docker-compose.yml index 01bd1b1e6..b0c4a914d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,7 +24,7 @@ services: redis: restart: always - image: redis:4.0-alpine + image: redis:7-alpine networks: - internal_network volumes: @@ -36,7 +36,7 @@ services: db: restart: always - image: postgres:12.2-alpine + image: postgres:15-alpine networks: - internal_network env_file: diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 7f465c61b..1ff72668e 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -108,6 +108,7 @@ clickToShow: "اضغط للعرض" sensitive: "محتوى حساس" add: "إضافة" reaction: "التفاعلات" +reactions: "التفاعلات" reactionSetting: "التفاعلات المراد عرضها في منتقي التفاعلات." reactionSettingDescription2: "اسحب لترتيب ، انقر للحذف ، استخدم \"+\" للإضافة." rememberNoteVisibility: "تذكر إعدادت مدى رؤية الملاحظات" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 28a01e657..c6e94896e 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -107,6 +107,7 @@ clickToShow: "দেখার জন্য ক্লিক করুন" sensitive: "সংবেদনশীল বিষয়বস্তু" add: "যুক্ত করুন" reaction: "প্রতিক্রিয়া" +reactions: "প্রতিক্রিয়া" reactionSetting: "রিঅ্যাকশন পিকারে যেসকল প্রতিক্রিয়া দেখানো হবে" reactionSettingDescription2: "পুনরায় সাজাতে টেনে আনুন, মুছতে ক্লিক করুন, যোগ করতে + টিপুন।" rememberNoteVisibility: "নোটের দৃশ্যমান্যতার সেটিংস মনে রাখুন" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index be97b7f60..8bc5bf036 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -108,6 +108,7 @@ clickToShow: "Fes clic per mostrar" sensitive: "NSFW" add: "Afegir" reaction: "Reaccions" +reactions: "Reaccions" reactionSetting: "Reaccions a mostrar al selector de reaccions" reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir." rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 4d7c0168f..eb9ae6f87 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -105,6 +105,7 @@ clickToShow: "Klikněte pro zobrazení" sensitive: "NSFW" add: "Přidat" reaction: "Reakce" +reactions: "Reakce" reactionSettingDescription2: "Přetažením změníte pořadí, kliknutím smažete, zmáčkněte \"+\" k přidání" rememberNoteVisibility: "Zapamatovat nastavení zobrazení poznámky" attachCancel: "Odstranit přílohu" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index ea4a72dc0..f6095e4db 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -110,6 +110,7 @@ clickToShow: "Zum Anzeigen anklicken" sensitive: "NSFW" add: "Hinzufügen" reaction: "Reaktionen" +reactions: "Reaktionen" reactionSetting: "In der Reaktionsauswahl anzuzeigende Reaktionen" reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen" rememberNoteVisibility: "Notizsichtbarkeit merken" @@ -931,10 +932,12 @@ undefined: "Undefiniert" assign: "Zuweisen" unassign: "Entfernen" color: "Farbe" -manageCustomEmojis: "Benutzerdefinierte Emojis verwalten" +manageCustomEmojis: "Kann benutzerdefinierte Emojis verwalten" youCannotCreateAnymore: "Du hast das Erstellungslimit erreicht." cannotPerformTemporary: "Vorübergehend nicht verfügbar" cannotPerformTemporaryDescription: "Diese Aktion ist wegen des Überschreitenes des Ausführungslimits temporär nicht verfügbar. Bitte versuche es nach einiger Zeit erneut." +preset: "Vorlage" +selectFromPresets: "Aus Vorlagen wählen" _role: new: "Rolle erstellen" edit: "Rolle bearbeiten" @@ -943,7 +946,7 @@ _role: permission: "Rollenberechtigungen" descriptionOfPermission: "Moderatoren können grundlegende Verwaltungsaufgaben erledigen.\nAdministratoren können alle Einstellungen der Instanz verwalten." assignTarget: "Zuweisungsart" - descriptionOfAssignTarget: "Manuell bedeutet, dass die Liste der Benutzer einer Rolle manuell verwaltet wird.\nKonditionell bedeutet, dass die Liste der Benutzer einer Rolle durch eine Bedingung automatisch verwaltet wird." + descriptionOfAssignTarget: "Manuell bedeutet, dass die Liste der Benutzer einer Rolle manuell verwaltet wird.\nKonditional bedeutet, dass die Liste der Benutzer einer Rolle durch eine Bedingung automatisch verwaltet wird." manual: "Manuell" conditional: "Konditional" condition: "Bedingung" @@ -966,7 +969,7 @@ _role: gtlAvailable: "Kann auf die globale Chronik zugreifen" ltlAvailable: "Kann auf die lokale Chronik zugreifen" canPublicNote: "Kann öffentliche Notizen erstellen" - canInvite: "Einladungscodes für diese Instanz erstellen" + canInvite: "Kann Einladungscodes für diese Instanz erstellen" canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten" driveCapacity: "Drive-Kapazität" pinMax: "Maximale Anzahl an angehefteten Notizen" @@ -979,6 +982,7 @@ _role: userEachUserListsMax: "Maximale Anzahl an Benutzerlisten" rateLimitFactor: "Versuchsanzahl" descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver." + canHideAds: "Kann Werbung ausblenden" _condition: isLocal: "Lokaler Benutzer" isRemote: "Benutzer fremder Instanz" @@ -1023,7 +1027,7 @@ _accountDelete: _ad: back: "Zurück" reduceFrequencyOfThisAd: "Diese Werbung weniger anzeigen" - hide: "Nie anzeigen" + hide: "Ausblenden" _forgotPassword: enterEmail: "Gib die Email-Adresse ein, mit der du dich registriert hast. An diese wird ein Link gesendet, mit dem du dein Passwort zurücksetzen kannst." ifNoEmail: "Solltest du bei der Registrierung keine Email-Adresse angegeben haben, wende dich bitte an den Administrator." diff --git a/locales/el-GR.yml b/locales/el-GR.yml index 974e66c03..c711683ff 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -103,6 +103,7 @@ you: "Εσύ" clickToShow: "Κάντε κλικ για εμφάνιση" add: "Προσθέστε" reaction: "Αντιδράσεις" +reactions: "Αντιδράσεις" reactionSetting: "Αντιδράσεις για εμφάνιση στην επιλογή αντίδρασης" reactionSettingDescription2: "Σύρετε για να αλλάξετε τη σειρά, κάντε κλικ για να διαγράψετε, πατήστε \"+\" για να προσθέσετε." rememberNoteVisibility: "Θυμήσου τις ρυθμίσεις ορατότητας σημειώματος" diff --git a/locales/en-US.yml b/locales/en-US.yml index b92ea24f2..b9f1603d2 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -110,6 +110,7 @@ clickToShow: "Click to show" sensitive: "NSFW" add: "Add" reaction: "Reactions" +reactions: "Reactions" reactionSetting: "Reactions to show in the reaction picker" reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add." rememberNoteVisibility: "Remember note visibility settings" @@ -935,8 +936,41 @@ manageCustomEmojis: "Manage Custom Emojis" youCannotCreateAnymore: "You've hit the creation limit." cannotPerformTemporary: "Temporarily unavailable" cannotPerformTemporaryDescription: "This action cannot be performed temporarily due to exceeding the execution limit. Please wait for a while and then try again." -preset: "Presets" +preset: "Preset" selectFromPresets: "Choose from presets" +_achievements: + _types: + _reactWithoutRead: + title: "Did you really read that?" + description: "React on a note that's over 100 characters long within 3 seconds of it being posted" + _clickedClickHere: + title: "Click here" + description: "You've clicked here" + _justPlainLucky: + title: "Just Plain Lucky" + description: "Has a chance to be obtained with a probability of 0.01% every 10 seconds" + _setNameToSyuilo: + title: "God Complex" + description: "Set your name to \"syuilo\"" + _passedSinceAccountCreated1: + title: "One Year Anniversary" + description: "One year has passed since your account was created" + _passedSinceAccountCreated2: + title: "Two Year Anniversary" + description: "Two years have passed since your account was created" + _passedSinceAccountCreated3: + title: "Three Year Anniversary" + description: "Three years have passed since your account was created" + _loggedInOnBirthday: + title: "Happy Birthday" + description: "Logged in on your birthday" + _cookieClicked: + title: "A game in which you click cookies" + description: "Clicked the cookie" + _brainDiver: + title: "Brain Diver" + description: "Post the link to Brain Diver" + flavor: "Misskey-Misskey La-Tu-Ma" _role: new: "New role" edit: "Edit role" @@ -954,10 +988,10 @@ _role: descriptionOfIsPublic: "Anyone will be able to view a list of users assigned to this role. In addition, this role will be displayed in the profiles of assigned users." options: "Role options" policies: "Policies" - baseRole: "Base role" - useBaseValue: "Use base role value" + baseRole: "Role template" + useBaseValue: "Use role template value" chooseRoleToAssign: "Select the role to assign" - canEditMembersByModerator: "Allow moderators to edit the list members of this role" + canEditMembersByModerator: "Allow moderators to edit the list of members for this role" descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users." priority: "Priority" _priority: @@ -965,11 +999,11 @@ _role: middle: "Medium" high: "High" _options: - gtlAvailable: "Viewing the global timeline" - ltlAvailable: "Viewing the local timeline" + gtlAvailable: "Can view the global timeline" + ltlAvailable: "Can view the local timeline" canPublicNote: "Can send public notes" - canInvite: "Create instance invite codes" - canManageCustomEmojis: "Manage Custom Emojis" + canInvite: "Can create instance invite codes" + canManageCustomEmojis: "Can manage custom emojis" driveCapacity: "Drive capacity" pinMax: "Maximum number of pinned notes" antennaMax: "Maximum number of antennas" @@ -981,7 +1015,7 @@ _role: userEachUserListsMax: "Maximum number of users within a user list" rateLimitFactor: "Rate limit" descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. " - canHideAds: "Remove ads" + canHideAds: "Can hide ads" _condition: isLocal: "Local user" isRemote: "Remote user" @@ -1026,7 +1060,7 @@ _accountDelete: _ad: back: "Back" reduceFrequencyOfThisAd: "Show this ad less" - hide: "Never show" + hide: "Hide" _forgotPassword: enterEmail: "Enter the email address you used to register. A link with which you can reset your password will then be sent to it." ifNoEmail: "If you did not use an email during registration, please contact the instance administrator instead." @@ -1586,6 +1620,7 @@ _notification: pollEnded: "Poll results have become available" unreadAntennaNote: "Antenna {name}" emptyPushNotificationMessage: "Push notifications have been updated" + achievementEarned: "Achievement unlocked" _types: all: "All" follow: "New followers" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 8be0edc29..47799a091 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -110,6 +110,7 @@ clickToShow: "Click para ver" sensitive: "Marcado como sensible" add: "Agregar" reaction: "Reacción" +reactions: "Reacción" reactionSetting: "Reacciones para mostrar en el menú de reacciones" reactionSettingDescription2: "Arrastre para reordenar, click para borrar, apriete la tecla + para añadir." rememberNoteVisibility: "Recordar visibilidad" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index eacff8c43..462b561e4 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -110,6 +110,7 @@ clickToShow: "Cliquer pour afficher" sensitive: "Contenu sensible" add: "Ajouter" reaction: "Réactions" +reactions: "Réactions" reactionSetting: "Réactions à afficher dans le sélecteur de réactions" reactionSettingDescription2: "Déplacer pour réorganiser, cliquer pour effacer, utiliser « + » pour ajouter." rememberNoteVisibility: "Activer l'option \" se souvenir de la visibilité des notes \" vous permet de réutiliser automatiquement la visibilité utilisée lors de la publication de votre note précédente." diff --git a/locales/id-ID.yml b/locales/id-ID.yml index e3fa177f3..87e37518f 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -2,6 +2,7 @@ _lang_: "Bahasa Indonesia" headlineMisskey: "Jaringan terhubung melalui catatan" introMisskey: "Selamat datang! Misskey adalah perangkat mikroblog tercatu bersifat sumber terbuka.\nMulailah menuliskan catatan, bagikan peristiwa terkini, serta ceritakan segala tentangmu.📡\nTunjukkan juga reaksimu pada catatan pengguna lain.👍\nMari jelajahi dunia baru🚀" +poweredByMisskeyDescription: "{name} adalah sebuah layanan (instance) yang menggunakan platform sumber terbuka Misskey." monthAndDay: "{day} {month}" search: "Penelusuran" notifications: "Pemberitahuan" @@ -47,6 +48,7 @@ deleteAndEdit: "Hapus dan sunting" deleteAndEditConfirm: "Apakah kamu yakin ingin menghapus note ini dan menyuntingnya? Kamu akan kehilangan semua reaksi, renote dan balasan di note ini." addToList: "Tambahkan ke daftar" sendMessage: "Kirim pesan" +copyRSS: "Salin RSS" copyUsername: "Salin nama pengguna" searchUser: "Cari pengguna" reply: "Balas" @@ -107,6 +109,7 @@ clickToShow: "Klik untuk melihat" sensitive: "Konten sensitif" add: "Tambahkan" reaction: "Reaksi" +reactions: "Reaksi" reactionSetting: "Reaksi untuk dimunculkan di bilah reaksi" reactionSettingDescription2: "Geser untuk memindah urutkan, klik untuk menghapus, tekan \"+\" untuk menambahkan" rememberNoteVisibility: "Ingat pengaturan visibilitas catatan" @@ -383,6 +386,7 @@ administrator: "Admin" token: "Token" twoStepAuthentication: "Otentikasi dua faktor" moderator: "Moderator" +moderation: "Moderasi" nUsersMentioned: "{n} pengguna disebut" securityKey: "Kunci keamanan" securityKeyName: "Nama kunci" @@ -449,6 +453,7 @@ language: "Bahasa" uiLanguage: "Bahasa antarmuka pengguna" groupInvited: "Telah diundang ke grup" aboutX: "Tentang {x}" +emojiStyle: "Gaya emoji" disableDrawer: "Jangan gunakan menu bergaya laci" youHaveNoGroups: "Kamu tidak memiliki grup" joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri." @@ -561,6 +566,7 @@ author: "Pembuat" leaveConfirm: "Ada perubahan yang belum disimpan. Apakah kamu ingin membuangnya?" manage: "Manajemen" plugins: "Plugin" +preferencesBackups: "Aturan pencadangan" deck: "Dek" undeck: "Keluar dari dek" useBlurEffectForModal: "Gunakan efek buram untuk modal" @@ -706,6 +712,7 @@ accentColor: "Aksen" textColor: "Teks" saveAs: "Simpan sebagai…" advanced: "Tingkat lanjut" +advancedSettings: "Pengaturan Lanjut" value: "Nilai" createdAt: "Dibuat pada" updatedAt: "Diperbarui pada" @@ -850,10 +857,21 @@ rateLimitExceeded: "Batas sudah terlampaui" cropImage: "potong gambar" cropImageAsk: "Ingin memotong gambar?" file: "Berkas" +noEmailServerWarning: "Mail Server tidak disetel." +recommended: "Disarankan" +check: "Cek" +deleteAccount: "Hapus Akun" +logoutConfirm: "Anda yakin ingin keluar?" +lastActiveDate: "Terakhir digunakan" +statusbar: "Bilah status" +pleaseSelect: "Pilih opsi..." reverse: "Balik" colored: "Diwarnai" +refreshInterval: "Jeda pembaharuan" label: "Label" +type: "Tipe" localOnly: "Hanya lokal" +shuffle: "Acak" account: "Akun" like: "Suka" unlike: "Tidak Suka" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index bedeb9474..e215b9f79 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -110,6 +110,7 @@ clickToShow: "Clicca per visualizzare" sensitive: "Contenuto sensibile" add: "Aggiungi" reaction: "Reazioni" +reactions: "Reazioni" reactionSetting: "Reazioni visualizzate sul pannello" reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere." rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note" @@ -836,7 +837,7 @@ hide: "Nascondere" leaveGroup: "Esci dal gruppo" leaveGroupConfirm: "Uscire da「{name}」?" useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile" -welcomeBackWithName: "Eccoti di nuovo, {name}! Ciao!" +welcomeBackWithName: "Ciao, {name}! Eccoti di nuovo!" clickToFinishEmailVerification: "Fai click su [{ok}] per completare la verifica dell'indirizzo email." overridedDeviceKind: "Tipo di dispositivo" smartphone: "Smartphone" @@ -935,6 +936,8 @@ manageCustomEmojis: "Gestisci le emoji personalizzate" youCannotCreateAnymore: "Non puoi creare, hai raggiunto il limite." cannotPerformTemporary: "Indisponibilità temporanea" cannotPerformTemporaryDescription: "L'attività non può essere svolta, poiché si è raggiunto il limite di esecuzioni possibili. Per favore, riprova più tardi." +preset: "Preimpostato" +selectFromPresets: "Seleziona preimpostato" _role: new: "Nuovo ruolo" edit: "Modifica ruolo" @@ -979,6 +982,7 @@ _role: userEachUserListsMax: "Quantità massima di profili per lista" rateLimitFactor: "Limite del rapporto" descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più." + canHideAds: "Può nascondere i banner" _condition: isLocal: "Profilo locale" isRemote: "Profilo remoto" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f0e53cb81..898ae01e7 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -110,6 +110,7 @@ clickToShow: "クリックして表示" sensitive: "閲覧注意" add: "追加" reaction: "リアクション" +reactions: "リアクション" reactionSetting: "ピッカーに表示するリアクション" reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。" rememberNoteVisibility: "公開範囲を記憶する" @@ -937,6 +938,235 @@ cannotPerformTemporary: "一時的に利用できません" cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。" preset: "プリセット" selectFromPresets: "プリセットから選択" +achievements: "実績" + +_achievements: + earnedAt: "獲得日時" + _types: + _notes1: + title: "just setting up my msky" + description: "初めてノートを投稿した" + flavor: "良いMisskeyライフを!" + _notes10: + title: "いくつかのノート" + description: "ノートを10回投稿した" + _notes100: + title: "たくさんのノート" + description: "ノートを100回投稿した" + _notes500: + title: "ノートまみれ" + description: "ノートを500回投稿した" + _notes1000: + title: "ノートの山" + description: "ノートを1,000回投稿した" + _notes5000: + title: "湧き出るノート" + description: "ノートを5,000回投稿した" + _notes10000: + title: "スーパーノート" + description: "ノートを10,000回投稿した" + _notes20000: + title: "ニードモアノート" + description: "ノートを20,000回投稿した" + _notes30000: + title: "ノートノートノート" + description: "ノートを30,000回投稿した" + _notes40000: + title: "ノート工場" + description: "ノートを40,000回投稿した" + _notes50000: + title: "ノートの惑星" + description: "ノートを50,000回投稿した" + _notes60000: + title: "ノートクエーサー" + description: "ノートを60,000回投稿した" + _notes70000: + title: "ブラックノートホール" + description: "ノートを70,000回投稿した" + _notes80000: + title: "ノートギャラクシー" + description: "ノートを80,000回投稿した" + _notes90000: + title: "ノートバース" + description: "ノートを90,000回投稿した" + _notes100000: + title: "ALL YOUR NOTE ARE BELONG TO US" + description: "ノートを100,000回投稿した" + flavor: "そんなに書くことある?" + _login3: + title: "ビギナーⅠ" + description: "通算ログイン日数が3日" + flavor: "今日からね僕は ミスキストってことで" + _login7: + title: "ビギナーⅡ" + description: "通算ログイン日数が7日" + flavor: "慣れてきましたか?" + _login15: + title: "ビギナーⅢ" + description: "通算ログイン日数が15日" + _login30: + title: "ミスキストⅠ" + description: "通算ログイン日数が30日" + _login60: + title: "ミスキストⅡ" + description: "通算ログイン日数が60日" + _login100: + title: "ミスキストⅢ" + description: "通算ログイン日数が100日" + flavor: "そのユーザー、ミスキストにつき" + _login200: + title: "常連Ⅰ" + description: "通算ログイン日数が200日" + _login300: + title: "常連Ⅱ" + description: "通算ログイン日数が300日" + _login400: + title: "常連Ⅲ" + description: "通算ログイン日数が400日" + _login500: + title: "ベテランⅠ" + description: "通算ログイン日数が500日" + flavor: "諸君、私はノートが好きだ" + _login600: + title: "ベテランⅡ" + description: "通算ログイン日数が600日" + _login700: + title: "ベテランⅢ" + description: "通算ログイン日数が700日" + _login800: + title: "ノートマスターⅠ" + description: "通算ログイン日数が800日" + _login900: + title: "ノートマスターⅡ" + description: "通算ログイン日数が900日" + _login1000: + title: "ノートマスターⅢ" + description: "通算ログイン日数が1,000日" + flavor: "Misskeyを使ってくれてありがとう!" + _noteClipped1: + title: "クリップせずにはいられないな" + description: "初めてノートをクリップした" + _noteFavorited1: + title: "星をみるひと" + description: "初めてノートをお気に入りに登録した" + _profileFilled: + title: "準備万端" + description: "プロフィール設定を行った" + _markedAsCat: + title: "吾輩は猫である" + description: "アカウントをCatとして設定した" + flavor: "名前はまだない。" + _following1: + title: "はじめてのフォロー" + description: "初めてフォローした" + _following10: + title: "ついてく、ついてく" + description: "フォローが10人を超した" + _following50: + title: "友達たくさん" + description: "フォローが50人を超した" + _following100: + title: "友達100人" + description: "フォローが100人を超した" + _following300: + title: "友達過多" + description: "フォローが300人を超した" + _followers1: + title: "はじめてのフォロワー" + description: "初めてフォローされた" + _followers10: + title: "フォローミー!" + description: "フォロワーが10人を超した" + _followers50: + title: "ぞろぞろ" + description: "フォロワーが50人を超した" + _followers100: + title: "人気者" + description: "フォロワーが100人を超した" + _followers300: + title: "一列でお並びください" + description: "フォロワーが300人を超した" + _followers500: + title: "基地局" + description: "フォロワーが500人を超した" + _followers1000: + title: "インフルエンサー" + description: "フォロワーが1,000人を超した" + _collectAchievements30: + title: "実績コレクター" + description: "実績を30個以上獲得した" + _viewAchievements3min: + title: "実績好き" + description: "実績一覧を3分以上眺め続けた" + _iLoveMisskey: + title: "I Love Misskey" + description: "\"I ❤ #Misskey\"を投稿した" + flavor: "Misskeyを使ってくださりありがとうございます! by 開発チーム" + _client30min: + title: "ひとやすみ" + description: "クライアントを起動してから30分以上経過した" + _noteDeletedWithin1min: + title: "いまのなし" + description: "投稿してから1分以内にその投稿を削除した" + _postedAtLateNight: + title: "夜行性" + description: "深夜にノートを投稿した" + flavor: "そろそろ寝よう。" + _postedAt0min0sec: + title: "時報" + description: "0分0秒にノートを投稿した" + flavor: "ポッ ポッ ポッ ピーン" + _selfQuote: + title: "自己言及" + description: "自分のノートを引用した" + _htl20npm: + title: "流れるTL" + description: "ホームタイムラインの流速が20npmを越す" + _outputHelloWorldOnScratchpad: + title: "Hello, world!" + description: "スクラッチパッドで hello world を出力した" + _open3windows: + title: "マルチウィンドウ" + description: "ウィンドウを3つ以上開いた状態にした" + _driveFolderCircularReference: + title: "循環参照" + description: "ドライブのフォルダを再帰的な入れ子にしようとした" + _reactWithoutRead: + title: "ちゃんと読んだ?" + description: "100文字以上のテキストを含むノートに投稿されてから3秒以内にリアクションした" + _clickedClickHere: + title: "ここをクリック" + description: "ここをクリックした" + _justPlainLucky: + title: "単なるラッキー" + description: "10秒ごとに0.01%の確率で獲得" + _setNameToSyuilo: + title: "神様コンプレックス" + description: "名前を syuilo に設定した" + _passedSinceAccountCreated1: + title: "一周年" + description: "アカウント作成から1年経過した" + _passedSinceAccountCreated2: + title: "二周年" + description: "アカウント作成から2年経過した" + _passedSinceAccountCreated3: + title: "三周年" + description: "アカウント作成から3年経過した" + _loggedInOnBirthday: + title: "ハッピーバースデー" + description: "誕生日にログインした" + _loggedInOnNewYearsDay: + title: "あけましておめでとうございます" + description: "元日にログインした" + flavor: "今年も弊インスタンスをよろしくお願いします" + _cookieClicked: + title: "クッキーをクリックするゲーム" + description: "クッキーをクリックした" + flavor: "ソフト間違ってない?" + _brainDiver: + title: "Brain Diver" + description: "Brain Diverへのリンクを投稿した" + flavor: "Misskey-Misskey La-Tu-Ma" _role: new: "ロールの作成" @@ -1634,6 +1864,7 @@ _notification: pollEnded: "アンケートの結果が出ました" unreadAntennaNote: "アンテナ {name}" emptyPushNotificationMessage: "プッシュ通知の更新をしました" + achievementEarned: "実績を獲得" _types: all: "すべて" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index e95b068b0..40d28b196 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -8,9 +8,9 @@ search: "探す" notifications: "通知" username: "ユーザー名" password: "パスワード" -forgotPassword: "パスワード忘れてん" +forgotPassword: "パスワード忘れてもうた" fetchingAsApObject: "今ちと連合に照会しとるで" -ok: "OKや" +ok: "ええで" gotIt: "ほい" cancel: "やめとく" noThankYou: "やめとく" @@ -110,6 +110,7 @@ clickToShow: "押したら見えるで" sensitive: "ちょっとアカンやつやで" add: "増やす" reaction: "リアクション" +reactions: "リアクション" reactionSetting: "Reaction that will be displayed in Picker. " reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。" rememberNoteVisibility: "公開範囲覚えといて" @@ -607,7 +608,7 @@ wordMute: "ワードミュート" regexpError: "正規表現エラー" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが出てきたで:" instanceMute: "インスタンスミュート" -userSaysSomething: "{name}が何か言ったようやで" +userSaysSomething: "{name}が何か言うとるわ" makeActive: "使うで" display: "表示" copy: "コピー" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 628a631d2..3e6ab5a2f 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -110,6 +110,7 @@ clickToShow: "클릭하여 보기" sensitive: "열람주의" add: "추가" reaction: "리액션" +reactions: "리액션" reactionSetting: "선택기에 표시할 리액션" reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다." rememberNoteVisibility: "공개 범위를 기억하기" @@ -935,12 +936,229 @@ manageCustomEmojis: "커스텀 이모지 관리" youCannotCreateAnymore: "더 이상 생성할 수 없습니다." cannotPerformTemporary: "일시적으로 사용할 수 없음" cannotPerformTemporaryDescription: "조작 횟수 제한을 초과하여 일시적으로 사용이 불가합니다. 잠시 후 다시 시도해 주세요." +preset: "프리셋" +selectFromPresets: "프리셋에서 선택" +achievements: "도전과제" +_achievements: + earnedAt: "달성 일시" + _types: + _notes1: + title: "미스키 설정하고 있었는데요" + description: "첫 노트를 포스트했습니다" + flavor: "Misskey에 오신 것을 환영합니다!" + _notes10: + title: "노트 조금" + description: "10개의 노트를 작성했습니다" + _notes100: + title: "노트 많이" + description: "100개의 노트를 작성했습니다" + _notes500: + title: "노트로 뒤덮여버렸어" + description: "500개의 노트를 작성했습니다" + _notes1000: + title: "노트만 산더미" + description: "1,000개의 노트를 작성했습니다" + _notes5000: + title: "노트가 어디서 솟아?" + description: "5,000개의 노트를 작성했습니다" + _notes10000: + title: "슈퍼-노트" + description: "10,000개의 노트를 작성했습니다" + _notes20000: + title: "노트 더 없어?" + description: "20,000개의 노트를 작성했습니다" + _notes30000: + title: "노트노트노트" + description: "30,000개의 노트를 작성했습니다" + _notes40000: + title: "노트 공장" + description: "40,000개의 노트를 작성했습니다" + _notes50000: + title: "노트 행성" + description: "50,000개의 노트를 작성했습니다" + _notes60000: + title: "노트 퀘이사" + description: "60,000개의 노트를 작성했습니다" + _notes70000: + title: "노트 블랙홀" + description: "70,000개의 노트를 작성했습니다" + _notes80000: + title: "노트 은하" + description: "80,000개의 노트를 작성했습니다" + _notes90000: + title: "노트 우주" + description: "90,000개의 노트를 작성했습니다" + _notes100000: + title: "네 모든 노트는 내 거야" + description: "100,000개의 노트를 작성했습니다" + flavor: "이만큼 쓸 일도 없겠지만... 다른 할 일이 있진 않으신가요?" + _login3: + title: "비기너 I" + description: "총 3일간 로그인했습니다" + flavor: "오늘부터 여러분도 미스키스트에요!" + _login7: + title: "비기너 II" + description: "총 7일간 로그인했습니다" + flavor: "슬슬 익숙해지셨나요?" + _login15: + title: "비기너 III" + description: "총 15일간 로그인했습니다" + _login30: + title: "미스키스트 I" + description: "총 30일간 로그인했습니다" + _login60: + title: "미스키스트 II" + description: "총 60일간 로그인했습니다" + _login100: + title: "미스키스트 III" + description: "총 100일간 로그인했습니다" + flavor: "그 유저, 미스키스트를 위하여" + _login200: + title: "단골 I" + description: "총 200일간 로그인했습니다" + _login300: + title: "단골 II" + description: "총 300일간 로그인했습니다" + _login400: + title: "단골 III" + description: "총 400일간 로그인했습니다" + _login500: + title: "베테랑 I" + description: "총 500일간 로그인했습니다" + flavor: "여러분, 저 이 노트들 좋아해요" + _login600: + title: "베테랑 II" + description: "총 600일간 로그인했습니다" + _login700: + title: "베테랑 III" + description: "총 700일간 로그인했습니다" + _login800: + title: "노트 마스터 I" + description: "총 800일간 로그인했습니다" + _login900: + title: "노트 마스터 II" + description: "총 900일간 로그인했습니다" + _login1000: + title: "노트 마스터 III" + description: "총 1,000일간 로그인했습니다" + flavor: "미스키를 사용해 주셔서 감사합니다!" + _noteClipped1: + title: "클립할 수밖에 없었어" + description: "처음으로 노트를 클립했습니다" + _noteFavorited1: + title: "별을 바라보는 자" + description: "처음으로 노트를 즐겨찾기했습니다" + _profileFilled: + title: "준비 완료" + description: "프로필 설정을 완료했습니다" + _markedAsCat: + title: "나는 고양이다냥!" + description: "계정을 고양이로 설정했습니다냥" + flavor: "냐냐냐냐냐냐아아아아앙!" + _following1: + title: "첫 팔로우" + description: "사용자를 처음으로 팔로우했습니다" + _following10: + title: "팔로우, 팔로우" + description: "10명의 사용자를 팔로우했습니다" + _following50: + title: "친구 잔뜩" + description: "50명의 사용자를 팔로우했습니다" + _following100: + title: "주소록 한 권으론 부족해" + description: "100명의 사용자를 팔로우했습니다" + _following300: + title: "친구가 넘쳐나" + description: "300명의 사용자를 팔로우했습니다" + _followers1: + title: "첫 팔로워" + description: "사용자가 처음으로 팔로잉했습니다" + _followers10: + title: "날 따라와!" + description: "10명의 사용자가 팔로우했습니다" + _followers50: + title: "이곳저곳" + description: "50명의 사용자가 팔로우했습니다" + _followers100: + title: "인기왕" + description: "100명의 사용자가 팔로우했습니다" + _followers300: + title: "줄 좀 서봐요" + description: "100명의 사용자가 팔로우했습니다" + _followers500: + title: "기지국" + description: "500명의 사용자가 팔로우했습니다" + _followers1000: + title: "유명인사" + description: "1,000명의 사용자가 팔로우했습니다" + _collectAchievements30: + title: "도전과제 콜렉터" + description: "30개의 도전과제를 획득했습니다" + _iLoveMisskey: + title: "I Love Misskey" + description: "\"I ❤ #Misskey\"를 포스트했습니다" + flavor: "Misskey를 이용해주셔서 감사합니다! - 개발팀 일동" + _client30min: + title: "잠깐 쉬어" + description: "클라이언트를 시작하고 30분이 경과하였습니다" + _noteDeletedWithin1min: + title: "있었는데요 없었습니다" + description: "노트를 포스트한 후 1분 이내에 삭제했습니다" + _postedAtLateNight: + title: "올빼미" + description: "한밤중에 노트를 포스트했습니다" + flavor: "잠 좀 자세요. 걱정돼요." + _postedAt0min0sec: + title: "정각" + description: "1초도 어긋나지 않은 정각에 노트를 포스트했습니다" + flavor: "째깍 째깍 째깍 땡!" + _selfQuote: + title: "혼잣말" + description: "자기 노트를 인용했습니다" + _htl20npm: + title: "타임라인 폭주 중" + description: "1분 사이에 홈 타임라인에 노트가 20개 넘게 생성되었습니다" + _driveFolderCircularReference: + title: "순환 참조" + description: "드라이브 폴더를 자신을 가리키도록 만드려 시도했습니다" + _reactWithoutRead: + title: "읽고 답하긴 하시는 건가요?" + description: "100자가 넘는 포스트에 3초 안에 포스트했습니다" + _clickedClickHere: + title: "여길 눌러보세요" + description: "이 곳을 눌러봤습니다" + _justPlainLucky: + title: "그냥 운이 좋았어" + description: "매 10초마다 0.01%의 확률로 달성됩니다" + _setNameToSyuilo: + title: "신 콤플렉스" + description: "이름을 syuilo로 설정했습니다" + _passedSinceAccountCreated1: + title: "1년" + description: "계정을 생성하고 1년이 지났습니다" + _passedSinceAccountCreated2: + title: "2년" + description: "계정을 생성하고 2년이 지났습니다" + _passedSinceAccountCreated3: + title: "3년" + description: "계정을 생성하고 3년이 지났습니다" + _loggedInOnBirthday: + title: "생일 축하합니다!" + description: "설정한 생일에 로그인했습니다" + _cookieClicked: + title: "쿠키 클리커 게임" + description: "쿠키를 클릭했습니다" + flavor: "뭔가 문제가 있나요?" + _brainDiver: + title: "Brain Diver" + description: "Brain Diver로의 링크를 첨부했습니다" + flavor: "Misskey-Misskey La-Tu-Ma" _role: new: "새 역할 생성" edit: "역할 수정" name: "역할 이름" description: "역할 설명" - permission: "역할의 권한" + permission: "역할 권한" descriptionOfPermission: "모더레이터는 기본적인 중재와 관련된 작업을 수행할 수 있습니다.\n관리자는 인스턴스의 모든 설정을 변경할 수 있습니다." assignTarget: "할당 대상" descriptionOfAssignTarget: "수동을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n조건부를 선택하면 조건을 설정해 일치하는 사용자를 자동으로 포함되게 할 수 있습니다." @@ -948,7 +1166,7 @@ _role: conditional: "조건부" condition: "조건" isConditionalRole: "조건부 역할입니다." - isPublic: "공개 역할" + isPublic: "역할 공개" descriptionOfIsPublic: "역할에 할당된 사용자를 누구나 볼 수 있습니다. 또한 사용자 프로필에 이 역할이 표시됩니다." options: "옵션" policies: "정책" @@ -956,7 +1174,7 @@ _role: useBaseValue: "기본값 사용" chooseRoleToAssign: "할당할 역할 선택" canEditMembersByModerator: "모더레이터의 역할 수정 허용" - descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 추가하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 가능합니다." + descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 할당하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 할당이 가능합니다." priority: "우선순위" _priority: low: "낮음" @@ -971,19 +1189,20 @@ _role: driveCapacity: "드라이브 용량" pinMax: "고정할 수 있는 노트 수" antennaMax: "최대 안테나 생성 허용 수" - wordMuteMax: "뮤트할 수 있는 단어의 수" - webhookMax: "생성할 수 있는 WebHook의 수" + wordMuteMax: "단어 뮤트할 수 있는 문자 수" + webhookMax: "생성할 수 있는 웹훅 수" clipMax: "생성할 수 있는 클립 수" noteEachClipsMax: "각 클립에 추가할 수 있는 노트 수" - userListMax: "생성할 수 있는 리스트 수" - userEachUserListsMax: "리스트당 최대 사용자 수" + userListMax: "생성할 수 있는 유저 리스트 수" + userEachUserListsMax: "유저 리스트당 최대 사용자 수" rateLimitFactor: "속도 제한" descriptionOfRateLimitFactor: "작을수록 제한이 완화되고, 클수록 제한이 강화됩니다." + canHideAds: "광고 숨기기" _condition: isLocal: "로컬 사용자" isRemote: "리모트 사용자" - createdLessThan: "다음 일수 이내에 가입한 유저" - createdMoreThan: "다음 일수 이상 활동한 유저" + createdLessThan: "가압한 지 다음 일수 이내인 유저" + createdMoreThan: "가입한 지 다음 일수 이상인 유저" followersLessThanOrEq: "팔로워 수가 다음 이하인 유저" followersMoreThanOrEq: "팔로워 수가 다음 이상인 유저" followingLessThanOrEq: "팔로잉 수가 다음 이하인 유저" @@ -1583,6 +1802,7 @@ _notification: pollEnded: "투표 결과가 발표되었습니다" unreadAntennaNote: "안테나 {name}" emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다" + achievementEarned: "도전 과제를 달성했습니다" _types: all: "전부" follow: "팔로잉" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 5735c6322..e99d49710 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -109,6 +109,7 @@ clickToShow: "Klik om te bekijken" sensitive: "NSFW" add: "Toevoegen" reaction: "Reacties" +reactions: "Reacties" reactionSetting: "Reacties die in de reactie-selector worden getoond" reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen, Druk op \"+\" om toe te voegen" rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 1bdfcd967..d78185be8 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -110,6 +110,7 @@ clickToShow: "Kliknij, aby wyświetlić" sensitive: "NSFW" add: "Dodaj" reaction: "Reakcja" +reactions: "Reakcja" reactionSetting: "Reakcje do pokazania w wyborniku reakcji" reactionSettingDescription2: "Przeciągnij aby zmienić kolejność, naciśnij aby usunąć, naciśnij „+” aby dodać" rememberNoteVisibility: "Zapamiętuj ustawienia widoczności wpisu" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index c8dc09723..8eac5fee6 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -107,6 +107,7 @@ clickToShow: "Clique para ver" sensitive: "Conteúdo sensível" add: "Adicionar" reaction: "Reações" +reactions: "Reações" reactionSetting: "Quais reações a mostrar no selecionador de reações" reactionSettingDescription2: "Arraste para reordenar, clique para excluir, pressione + para adicionar." rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 7c01aa725..b1ec5426a 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -107,6 +107,7 @@ clickToShow: "Click pentru a afișa" sensitive: "NSFW" add: "Adaugă" reaction: "Reacție" +reactions: "Reacție" reactionSetting: "Reacții care să apară in selectorul de reacții" reactionSettingDescription2: "Trage pentru a rearanja, apasă pe \"+\" pentru a adăuga." rememberNoteVisibility: "Amintește setarea de vizibilitate a notelor" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 9d836f17b..d7aca1c9f 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -107,6 +107,7 @@ clickToShow: "Нажмите для просмотра" sensitive: "Содержимое не для всех" add: "Добавить" reaction: "Реакции" +reactions: "Реакции" reactionSetting: "Реакции, отображаемые в палитре" reactionSettingDescription2: "Расставляйте перетаскиванием, удаляйте нажатием, добавляйте кнопкой «+»." rememberNoteVisibility: "Запоминать видимость заметок" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 60945593b..ee2ca11fa 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -110,6 +110,7 @@ clickToShow: "Kliknutím zobrazíte" sensitive: "NSFW" add: "Pridať" reaction: "Reakcie" +reactions: "Reakcie" reactionSetting: "Reakcie zobrazené vo výbere reakcií" reactionSettingDescription2: "Ťahaním preusporiadate, kliknutím odstránite, Stlačením \"+\" pridáte" rememberNoteVisibility: "Zapamätať nastavenia viditeľnosti poznámky" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index b2647c968..1abd0d194 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -110,6 +110,7 @@ clickToShow: "Klicka för att visa" sensitive: "Känsligt innehåll" add: "Lägg till" reaction: "Reaktioner" +reactions: "Reaktioner" reactionSetting: "Reaktioner som ska visas i reaktionsväljaren" reactionSettingDescription2: "Dra för att omordna, klicka för att radera, tryck \"+\" för att lägga till." rememberNoteVisibility: "Komihåg notvisningsinställningar" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index b77bc55b2..08737fb1b 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -110,6 +110,7 @@ clickToShow: "คลิกเพื่อแสดง" sensitive: "เนื้อหาที่ละเอียดอ่อน NSFW" add: "เพิ่ม" reaction: "รีแอคชั่น" +reactions: "รีแอคชั่น" reactionSetting: "รีแอคชั่นไปยังแสดงผลในตัวเลือกการรีแอคชั่น" reactionSettingDescription2: "กดลากเพื่อจัดลำดับใหม่ กดคลิกเพื่อลบ กด \"+\" เพื่อเพิ่ม" rememberNoteVisibility: "จดจำการตั้งค่าการมองเห็นตัวโน้ต" @@ -932,6 +933,23 @@ assign: "กำหนด" unassign: "ยังไม่มอบหมาย" color: "สี" manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" +youCannotCreateAnymore: "คุณถึงขีดจํากัดการสร้างแล้วนะ" +cannotPerformTemporary: "ไม่สามารถใช้การได้ชั่วคราว" +cannotPerformTemporaryDescription: "การดําเนินการนี้ไม่สามารถดําเนินการได้ชั่วคราว เนื่องจากเกินขีดจํากัดการดําเนินการ กรุณารอสักครู่แล้วลองใหม่อีกครั้งนะค่ะ" +preset: "พรีเซ็ต" +selectFromPresets: "เลือกจากการพรีเซ็ต" +achievements: "ความสำเร็จ" +_achievements: + earnedAt: "ได้รับเมื่อ" + _types: + _followers100: + title: "บุคคลที่เป็นที่นิยม" + _followers500: + title: "เสาสัญญาณ" + _iLoveMisskey: + title: "ฉันรัก Misskey" + _driveFolderCircularReference: + title: "อ้างอิงวงจร" _role: new: "บทบาทใหม่" edit: "แก้ไขบทบาท" @@ -948,6 +966,7 @@ _role: isPublic: "บทบาทสาธารณะ" descriptionOfIsPublic: "ทุกคนสามารถดูได้ว่าผู้ใช้งานนั้นได้รับมอบหมายบทบาทด้วยหรือไม่ \n\nบทบาทจะแสดงในโปรไฟล์ของผู้ใช้ด้วย" options: "ตัวเลือกบทบาท" + policies: "นโยบาย" baseRole: "บทบาทพื้นฐาน" useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น" chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด" @@ -965,7 +984,17 @@ _role: canInvite: "สร้างรหัสเชิญอินสแตนซ์" canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" driveCapacity: "ความจุของไดรฟ์" + pinMax: "จํานวนสูงสุดของโน้ตที่ปักหมุดไว้" antennaMax: "จำนวนสูงสุดของเสาอากาศ" + wordMuteMax: "จำนวนอักขระสูงสุดที่อนุญาตในการปิดเสียงคำ" + webhookMax: "จำนวนเว็บฮุคสูงสุด" + clipMax: "จำนวนคลิปสูงสุด" + noteEachClipsMax: "จำนวนโน้ตสูงสุดภายในคลิป" + userListMax: "จำนวนรายชื่อผู้ใช้สูงสุด" + userEachUserListsMax: "จำนวนผู้ใช้สูงสุดภายในรายการผู้ใช้" + rateLimitFactor: "ขีดจำกัดอัตรา" + descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า" + canHideAds: "ซ่อนโฆษณา" _condition: isLocal: "ผู้ใช้ภายใน" isRemote: "ผู้ใช้ระยะไกล" @@ -1570,6 +1599,7 @@ _notification: pollEnded: "โพลสำรวจความคิดเห็นผลลัพธ์มีพร้อมใช้งาน" unreadAntennaNote: "เสาอากาศ {name}" emptyPushNotificationMessage: "การแจ้งเตือนแบบพุชได้รับการอัพเดทแล้ว" + achievementEarned: "รับความสำเร็จ" _types: all: "ทั้งหมด" follow: "กำลังติดตาม" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 992275c7e..6ca8d7059 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -109,6 +109,7 @@ clickToShow: "Натисніть для перегляду" sensitive: "NSFW" add: "Додати" reaction: "Реакції" +reactions: "Реакції" reactionSetting: "Налаштування реакцій" reactionSettingDescription2: "Перемістити щоб змінити порядок, Клацнути мишою щоб видалити, Натиснути \"+\" щоб додати." rememberNoteVisibility: "Пам’ятати параметри видимісті" @@ -586,7 +587,7 @@ pluginTokenRequestedDescription: "Цей плагін зможе викорис notificationType: "Тип сповіщення" edit: "Редагувати" useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий" -emailServer: "Сервер електронної пошти" +emailServer: "Email сервер" enableEmail: "Увімкнути функцію доставки пошти" emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю." email: "E-mail" @@ -892,7 +893,10 @@ unsubscribePushNotification: "Вимкнути push-сповіщення" windowMaximize: "Розгорнути" windowRestore: "Відновити" caption: "Підпис" +tools: "Інструменти" like: "Вподобати" +unlike: "Не вподобати" +numberOfLikes: "Вподобання" show: "Відображення" color: "Колір" _role: diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 92931de6f..b460b5e83 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -107,6 +107,7 @@ clickToShow: "Nhấn để xem" sensitive: "Nhạy cảm" add: "Thêm" reaction: "Biểu cảm" +reactions: "Biểu cảm" reactionSetting: "Chọn những biểu cảm hiển thị" reactionSettingDescription2: "Kéo để sắp xếp, nhấn để xóa, nhấn \"+\" để thêm." rememberNoteVisibility: "Lưu kiểu tút mặc định" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index c857f04f9..817fc6946 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -110,6 +110,7 @@ clickToShow: "点击以显示" sensitive: "敏感内容" add: "添加" reaction: "回应" +reactions: "回应" reactionSetting: "在选择器中显示的回应" reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。" rememberNoteVisibility: "保存上次设置的可见性" @@ -607,7 +608,7 @@ wordMute: "文字屏蔽" regexpError: "正则表达式错误" regexpErrorDescription: "{tab} 屏蔽文字的第 {line} 行的正则表达式有错误:" instanceMute: "实例的屏蔽" -userSaysSomething: "{name}说了什么,但是被您屏蔽了" +userSaysSomething: "{name}说了什么,但是被屏蔽词过滤了" makeActive: "启用" display: "显示" copy: "复制" @@ -826,7 +827,7 @@ makeReactionsPublicDescription: "将您发表过的回应设置成公开可见 classic: "经典" muteThread: "屏蔽帖子列表" unmuteThread: "取消屏蔽帖子列表" -ffVisibility: "连接的可见范围" +ffVisibility: "关注关系的可见范围" ffVisibilityDescription: "您可以设置您的关注/关注者信息的公开范围" continueThread: "查看更多帖子" deleteAccountConfirm: "将要删除账户。是否确认?" @@ -981,6 +982,7 @@ _role: userEachUserListsMax: "单个用户列表内用户数量限制" rateLimitFactor: "速率限制" descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。" + canHideAds: "可以隐藏广告" _condition: isLocal: "是本地用户" isRemote: "是远程用户" @@ -1008,7 +1010,7 @@ _emailUnavailable: mx: "邮件服务器不正确" smtp: "邮件服务器没有响应" _ffVisibility: - public: "发布" + public: "公开" followers: "只有关注你的用户能看到" private: "私密" _signup: diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 50f4a12c7..025a4c3e5 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -110,6 +110,7 @@ clickToShow: "按一下以顯示" sensitive: "敏感內容" add: "新增" reaction: "情感" +reactions: "情感" reactionSetting: "在選擇器中顯示反應" reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。" rememberNoteVisibility: "記住貼文可見性" @@ -389,7 +390,7 @@ administrator: "管理員" token: "權杖" twoStepAuthentication: "兩階段驗證" moderator: "監察員" -moderation: "言論調節" +moderation: "監察" nUsersMentioned: "提到了{n}" securityKey: "安全金鑰" securityKeyName: "金鑰名稱" @@ -932,8 +933,67 @@ assign: "指派" unassign: "取消指派" color: "顏色" manageCustomEmojis: "管理自訂表情符號" +youCannotCreateAnymore: "您無法再建立更多了。" cannotPerformTemporary: "暫時無法進行" cannotPerformTemporaryDescription: "由於超過操作次數限制,暫時無法進行。請過一段時間之後再嘗試。" +preset: "預設值" +selectFromPresets: "從預設值中選擇" +achievements: "成就" +_achievements: + earnedAt: "獲得日期" + _types: + _notes1: + title: "just setting up my msky" + description: "發出了第一則貼文" + flavor: "祝您的Misskey生活愉快!" + _notes10: + title: "若干貼文" + description: "發表了10則貼文" + _notes100: + title: "許多的貼文" + description: "發表了100則貼文" + _notes500: + title: "滿滿的貼文" + description: "發表了500則貼文" + _notes1000: + title: "一堆貼文" + description: "發表了1000則貼文" + _notes5000: + title: "滔滔不絕的貼文" + description: "發表了5000則貼文" + _notes10000: + title: "超級貼文" + description: "發表了10000則貼文" + _notes20000: + title: "需要更多的貼文" + description: "發表了20000則貼文" + _notes30000: + title: "貼文貼文貼文" + description: "發表了30000則貼文" + _notes40000: + title: "貼文工廠" + description: "發表了40000則貼文" + _notes50000: + title: "貼文星球" + description: "發表了50000則貼文" + _notes60000: + title: "貼文類星體" + description: "發表了60000則貼文" + _notes70000: + title: "貼文黑洞" + description: "發表了70000則貼文" + _notes80000: + title: "貼文銀河" + description: "發表了80000則貼文" + _notes90000: + title: "貼文宇宙" + description: "發表了90000則貼文" + _notes100000: + description: "發表了100,000則貼文" + flavor: "有這麼多東西要寫嗎?" + _login3: + title: "初學者 I" + description: "總登入天數為3天" _role: new: "建立角色" edit: "編輯角色" @@ -970,8 +1030,15 @@ _role: driveCapacity: "雲端硬碟容量" pinMax: "置頂貼文的最大數量" antennaMax: "可建立的天線數量" + wordMuteMax: "靜音文字的最大字數" webhookMax: "可建立的Webhook數量" clipMax: "可建立的摘錄數量" + noteEachClipsMax: "摘錄內貼文的最大數量" + userListMax: "可建立的使用者清單數量" + userEachUserListsMax: "使用者清單內使用者的最大數量" + rateLimitFactor: "速率限制" + descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。" + canHideAds: "不顯示廣告" _condition: isLocal: "本地使用者" isRemote: "遠端使用者" diff --git a/package.json b/package.json index eafcab030..25a74d404 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.0.0", + "version": "13.1.0", "codename": "nasubi", "repository": { "type": "git", @@ -38,7 +38,7 @@ "cleanall": "pnpm clean-all" }, "resolutions": { - "chokidar": "^3.3.1", + "chokidar": "^3.5.3", "lodash": "^4.17.21" }, "dependencies": { diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc index c82564eab..55a88456e 100644 --- a/packages/backend/.swcrc +++ b/packages/backend/.swcrc @@ -9,7 +9,17 @@ "transform": { "legacyDecorator": true, "decoratorMetadata": true - } + }, + "experimental": { + "keepImportAssertions": true + }, + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + }, + "target": "es2021" }, "minify": false } diff --git a/packages/backend/migration/1674086433654-flashScriptLength.js b/packages/backend/migration/1674086433654-flashScriptLength.js new file mode 100644 index 000000000..a4d149fe1 --- /dev/null +++ b/packages/backend/migration/1674086433654-flashScriptLength.js @@ -0,0 +1,11 @@ +export class flashScriptLength1674086433654 { + name = 'flashScriptLength1674086433654' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "flash" ALTER COLUMN "script" TYPE character varying(32768)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "flash" ALTER COLUMN "script" TYPE character varying(16384)`); + } +} diff --git a/packages/backend/migration/1674118260469-achievement.js b/packages/backend/migration/1674118260469-achievement.js new file mode 100644 index 000000000..131ab96f8 --- /dev/null +++ b/packages/backend/migration/1674118260469-achievement.js @@ -0,0 +1,33 @@ +export class achievement1674118260469 { + name = 'achievement1674118260469' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "notification" ADD "achievement" character varying(128)`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD "achievements" jsonb NOT NULL DEFAULT '[]'`); + await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app')`); + await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`); + await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`); + await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app')`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`); + await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`); + } + + async down(queryRunner) { + await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'pollEnded')`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`); + await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`); + await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`); + await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`); + await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`); + await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`); + await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "achievements"`); + await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "achievement"`); + } +} diff --git a/packages/backend/migration/1674255666603-loggedInDates.js b/packages/backend/migration/1674255666603-loggedInDates.js new file mode 100644 index 000000000..6d75ab643 --- /dev/null +++ b/packages/backend/migration/1674255666603-loggedInDates.js @@ -0,0 +1,11 @@ +export class loggedInDates1674255666603 { + name = 'loggedInDates1674255666603' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "loggedInDates" character varying(32) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "loggedInDates"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index a9ba3ebaf..68cfbb05a 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -7,6 +7,8 @@ "start": "node ./built/index.js", "start:test": "NODE_ENV=test node ./built/index.js", "migrate": "pnpm typeorm migration:run -d ormconfig.js", + "build:swc": "swc src -d built -D", + "watch:swc": "swc src -d built -D -w", "build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", "watch": "node watch.mjs", "lint": "tsc --noEmit && eslint --quiet \"src/**/*.ts\"", @@ -129,6 +131,7 @@ }, "devDependencies": { "@redocly/openapi-core": "1.0.0-beta.120", + "@swc/cli": "^0.1.59", "@swc/core": "1.3.26", "@swc/jest": "0.2.24", "@types/accepts": "1.3.5", diff --git a/packages/backend/src/RootModule.ts b/packages/backend/src/MainModule.ts similarity index 63% rename from packages/backend/src/RootModule.ts rename to packages/backend/src/MainModule.ts index 3fc392776..fc568e883 100644 --- a/packages/backend/src/RootModule.ts +++ b/packages/backend/src/MainModule.ts @@ -1,13 +1,13 @@ import { Module } from '@nestjs/common'; import { ServerModule } from '@/server/ServerModule.js'; import { GlobalModule } from '@/GlobalModule.js'; -import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js'; +import { DaemonModule } from '@/daemons/DaemonModule.js'; @Module({ imports: [ GlobalModule, ServerModule, - QueueProcessorModule, + DaemonModule, ], }) -export class RootModule {} +export class MainModule {} diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 4630217c4..93cb3131b 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -17,6 +17,9 @@ import { JanitorService } from '@/daemons/JanitorService.js'; import { QueueStatsService } from '@/daemons/QueueStatsService.js'; import { ServerStatsService } from '@/daemons/ServerStatsService.js'; import { NestLogger } from '@/NestLogger.js'; +import { ChartManagementService } from '@/core/chart/ChartManagementService.js'; +import { ServerService } from '@/server/ServerService.js'; +import { MainModule } from '@/MainModule.js'; import { envOption } from '../env.js'; const _filename = fileURLToPath(import.meta.url); @@ -70,6 +73,15 @@ export async function masterMain() { process.exit(1); } + const app = await NestFactory.createApplicationContext(MainModule, { + logger: new NestLogger(), + }); + app.enableShutdownHooks(); + + // start server + const serverService = app.get(ServerService); + serverService.launch(); + bootLogger.succ('Misskey initialized'); if (!envOption.disableClustering) { @@ -78,15 +90,10 @@ export async function masterMain() { bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); - if (!envOption.noDaemons) { - const daemons = await NestFactory.createApplicationContext(DaemonModule, { - logger: new NestLogger(), - }); - daemons.enableShutdownHooks(); - daemons.get(JanitorService).start(); - daemons.get(QueueStatsService).start(); - daemons.get(ServerStatsService).start(); - } + app.get(ChartManagementService).start(); + app.get(JanitorService).start(); + app.get(QueueStatsService).start(); + app.get(ServerStatsService).start(); } function showEnvironment(): void { diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts index f29e37de7..e0574643b 100644 --- a/packages/backend/src/boot/worker.ts +++ b/packages/backend/src/boot/worker.ts @@ -1,32 +1,23 @@ import cluster from 'node:cluster'; import { NestFactory } from '@nestjs/core'; -import { envOption } from '@/env.js'; import { ChartManagementService } from '@/core/chart/ChartManagementService.js'; -import { ServerService } from '@/server/ServerService.js'; import { QueueProcessorService } from '@/queue/QueueProcessorService.js'; import { NestLogger } from '@/NestLogger.js'; -import { RootModule } from '../RootModule.js'; +import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js'; /** * Init worker process */ export async function workerMain() { - const app = await NestFactory.createApplicationContext(RootModule, { + const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, { logger: new NestLogger(), }); - app.enableShutdownHooks(); - - // start server - const serverService = app.get(ServerService); - serverService.launch(); + jobQueue.enableShutdownHooks(); // start job queue - if (!envOption.onlyServer) { - const queueProcessorService = app.get(QueueProcessorService); - queueProcessorService.start(); - } + jobQueue.get(QueueProcessorService).start(); - app.get(ChartManagementService).run(); + jobQueue.get(ChartManagementService).start(); if (cluster.isWorker) { // Send a 'ready' message to parent process diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts new file mode 100644 index 000000000..26dd356d3 --- /dev/null +++ b/packages/backend/src/core/AchievementService.ts @@ -0,0 +1,118 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { CreateNotificationService } from '@/core/CreateNotificationService.js'; + +const ACHIEVEMENT_TYPES = [ + 'notes1', + 'notes10', + 'notes100', + 'notes500', + 'notes1000', + 'notes5000', + 'notes10000', + 'notes20000', + 'notes30000', + 'notes40000', + 'notes50000', + 'notes60000', + 'notes70000', + 'notes80000', + 'notes90000', + 'notes100000', + 'login3', + 'login7', + 'login15', + 'login30', + 'login60', + 'login100', + 'login200', + 'login300', + 'login400', + 'login500', + 'login600', + 'login700', + 'login800', + 'login900', + 'login1000', + 'passedSinceAccountCreated1', + 'passedSinceAccountCreated2', + 'passedSinceAccountCreated3', + 'loggedInOnBirthday', + 'loggedInOnNewYearsDay', + 'noteClipped1', + 'noteFavorited1', + 'profileFilled', + 'markedAsCat', + 'following1', + 'following10', + 'following50', + 'following100', + 'following300', + 'followers1', + 'followers10', + 'followers50', + 'followers100', + 'followers300', + 'followers500', + 'followers1000', + 'collectAchievements30', + 'viewAchievements3min', + 'iLoveMisskey', + 'client30min', + 'noteDeletedWithin1min', + 'postedAtLateNight', + 'postedAt0min0sec', + 'selfQuote', + 'htl20npm', + 'outputHelloWorldOnScratchpad', + 'open3windows', + 'driveFolderCircularReference', + 'reactWithoutRead', + 'clickedClickHere', + 'justPlainLucky', + 'setNameToSyuilo', + 'cookieClicked', + 'brainDiver', +] as const; + +@Injectable() +export class AchievementService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private createNotificationService: CreateNotificationService, + ) { + } + + @bindThis + public async create( + userId: User['id'], + type: string, + ): Promise { + if (!ACHIEVEMENT_TYPES.includes(type)) return; + + const date = Date.now(); + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: userId }); + + if (profile.achievements.some(a => a.name === type)) return; + + await this.userProfilesRepository.update(userId, { + achievements: [...profile.achievements, { + name: type, + unlockedAt: date, + }], + }); + + this.createNotificationService.createNotification(userId, 'achievementEarned', { + achievement: type, + }); + } +} diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 0ae1ee32b..eddf40794 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -4,6 +4,7 @@ import { AccountUpdateService } from './AccountUpdateService.js'; import { AiService } from './AiService.js'; import { AntennaService } from './AntennaService.js'; import { AppLockService } from './AppLockService.js'; +import { AchievementService } from './AchievementService.js'; import { CaptchaService } from './CaptchaService.js'; import { CreateNotificationService } from './CreateNotificationService.js'; import { CreateSystemUserService } from './CreateSystemUserService.js'; @@ -128,6 +129,7 @@ const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useEx const $AiService: Provider = { provide: 'AiService', useExisting: AiService }; const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService }; const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService }; +const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService }; const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService }; const $CreateNotificationService: Provider = { provide: 'CreateNotificationService', useExisting: CreateNotificationService }; const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService }; @@ -255,6 +257,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AiService, AntennaService, AppLockService, + AchievementService, CaptchaService, CreateNotificationService, CreateSystemUserService, @@ -376,6 +379,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AiService, $AntennaService, $AppLockService, + $AchievementService, $CaptchaService, $CreateNotificationService, $CreateSystemUserService, @@ -498,6 +502,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AiService, AntennaService, AppLockService, + AchievementService, CaptchaService, CreateNotificationService, CreateSystemUserService, @@ -618,6 +623,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AiService, $AntennaService, $AppLockService, + $AchievementService, $CaptchaService, $CreateNotificationService, $CreateSystemUserService, diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 8639b5713..2864ad440 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -125,7 +125,7 @@ export class UndiciFetcher { ...(options.headers ?? {}), }, }).catch((err) => { - this.logger?.error('fetch error', err); + this.logger?.error(`fetch error to ${typeof url === 'string' ? url : url.href}`, err); throw new StatusError('Resource Unreachable', 500, 'Resource Unreachable'); }); if (!res.ok && !privateOptions.noOkError) { diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index d44d06a44..ab22a0c41 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -57,7 +57,7 @@ export class ApRequestService { method: 'POST', headers: this.objectAssignWithLcKey({ 'Date': new Date().toUTCString(), - 'Host': u.hostname, + 'Host': u.host, 'Content-Type': 'application/activity+json', 'Digest': digestHeader, }, args.additionalHeaders), @@ -83,7 +83,7 @@ export class ApRequestService { headers: this.objectAssignWithLcKey({ 'Accept': 'application/activity+json, application/ld+json', 'Date': new Date().toUTCString(), - 'Host': new URL(args.url).hostname, + 'Host': new URL(args.url).host, }, args.additionalHeaders), }; @@ -106,6 +106,8 @@ export class ApRequestService { request.headers = this.objectAssignWithLcKey(request.headers, { Signature: signatureHeader, }); + // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! + delete request.headers['host']; return { request, diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts index 37de30b71..4fba1b57d 100644 --- a/packages/backend/src/core/chart/ChartManagementService.ts +++ b/packages/backend/src/core/chart/ChartManagementService.ts @@ -54,7 +54,7 @@ export class ChartManagementService implements OnApplicationShutdown { } @bindThis - public async run() { + public async start() { // 20分おきにメモリ情報をDBに書き込み this.saveIntervalId = setInterval(() => { for (const chart of this.charts) { diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index a1c2c9cff..a8210eea0 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -114,6 +114,9 @@ export class NotificationEntityService implements OnModuleInit { ...(notification.type === 'groupInvited' ? { invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId!), } : {}), + ...(notification.type === 'achievementEarned' ? { + achievement: notification.achievement, + } : {}), ...(notification.type === 'app' ? { body: notification.customBody, header: notification.customHeader ?? token?.name, diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index bf6f6f455..34b523e14 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -12,7 +12,7 @@ import { Cache } from '@/misc/cache.js'; import type { Instance } from '@/models/entities/Instance.js'; import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; -import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository } from '@/models/index.js'; +import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -343,6 +343,7 @@ export class UserEntityService implements OnModuleInit { options?: { detail?: D, includeSecrets?: boolean, + userProfile?: UserProfile, }, ): Promise> { const opts = Object.assign({ @@ -375,7 +376,7 @@ export class UserEntityService implements OnModuleInit { .innerJoinAndSelect('pin.note', 'note') .orderBy('pin.id', 'DESC') .getMany() : []; - const profile = opts.detail ? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null; + const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null; const followingCount = profile == null ? null : (profile.ffVisibility === 'public') || isMe ? user.followingCount : @@ -493,6 +494,8 @@ export class UserEntityService implements OnModuleInit { mutingNotificationTypes: profile!.mutingNotificationTypes, emailNotificationTypes: profile!.emailNotificationTypes, showTimelineReplies: user.showTimelineReplies ?? falsy, + achievements: profile!.achievements, + loggedInDays: profile!.loggedInDates.length, } : {}), ...(opts.includeSecrets ? { diff --git a/packages/backend/src/models/entities/Flash.ts b/packages/backend/src/models/entities/Flash.ts index d9a6ac987..07039d4fa 100644 --- a/packages/backend/src/models/entities/Flash.ts +++ b/packages/backend/src/models/entities/Flash.ts @@ -44,7 +44,7 @@ export class Flash { public user: User | null; @Column('varchar', { - length: 16384, + length: 32768, }) public script: string; diff --git a/packages/backend/src/models/entities/Notification.ts b/packages/backend/src/models/entities/Notification.ts index 6679cdb80..66f131d1c 100644 --- a/packages/backend/src/models/entities/Notification.ts +++ b/packages/backend/src/models/entities/Notification.ts @@ -64,6 +64,7 @@ export class Notification { * receiveFollowRequest - フォローリクエストされた * followRequestAccepted - 自分の送ったフォローリクエストが承認された * groupInvited - グループに招待された + * achievementEarned - 実績を獲得 * app - アプリ通知 */ @Index() @@ -129,6 +130,11 @@ export class Notification { }) public choice: number | null; + @Column('varchar', { + length: 128, nullable: true, + }) + public achievement: string | null; + /** * アプリ通知のbody */ diff --git a/packages/backend/src/models/entities/UserProfile.ts b/packages/backend/src/models/entities/UserProfile.ts index c561da87c..86df8d5d9 100644 --- a/packages/backend/src/models/entities/UserProfile.ts +++ b/packages/backend/src/models/entities/UserProfile.ts @@ -213,6 +213,19 @@ export class UserProfile { }) public mutingNotificationTypes: typeof notificationTypes[number][]; + @Column('varchar', { + length: 32, array: true, default: '{}', + }) + public loggedInDates: string[]; + + @Column('jsonb', { + default: [], + }) + public achievements: { + name: string; + unlockedAt: number; + }[]; + //#region Denormalized fields @Index() @Column('varchar', { diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 034e9cc5a..6a8f35cdd 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; +import { GlobalModule } from '@/GlobalModule.js'; import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueProcessorService } from './QueueProcessorService.js'; import { DbQueueProcessorsService } from './DbQueueProcessorsService.js'; @@ -34,6 +35,7 @@ import { ExportFavoritesProcessorService } from './processors/ExportFavoritesPro @Module({ imports: [ + GlobalModule, CoreModule, ], providers: [ diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index fac8497b5..eb6a3795e 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -14,6 +14,7 @@ import { genIdenticon } from '@/misc/gen-identicon.js'; import { createTemp } from '@/misc/create-temp.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { LoggerService } from '@/core/LoggerService.js'; +import { bindThis } from '@/decorators.js'; import { ActivityPubServerService } from './ActivityPubServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js'; import { ApiServerService } from './api/ApiServerService.js'; @@ -22,7 +23,6 @@ import { WellKnownServerService } from './WellKnownServerService.js'; import { MediaProxyServerService } from './MediaProxyServerService.js'; import { FileServerService } from './FileServerService.js'; import { ClientServerService } from './web/ClientServerService.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ServerService { @@ -82,13 +82,13 @@ export class ServerService { fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { const path = request.params.path; + reply.header('Cache-Control', 'public, max-age=86400'); + if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) { reply.code(404); return; } - reply.header('Cache-Control', 'public, max-age=86400'); - const name = path.split('@')[0].replace('.webp', ''); const host = path.split('@')[1]?.replace('.webp', ''); @@ -101,7 +101,12 @@ export class ServerService { reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); if (emoji == null) { - return await reply.redirect('/static-assets/emoji-unknown.png'); + if ('fallback' in request.query) { + return await reply.redirect('/static-assets/emoji-unknown.png'); + } else { + reply.code(404); + return; + } } const url = new URL('/proxy/emoji.webp', this.config.url); @@ -127,6 +132,8 @@ export class ServerService { relations: ['avatar'], }); + reply.header('Cache-Control', 'public, max-age=86400'); + if (user) { reply.redirect(this.userEntityService.getAvatarUrlSync(user)); } else { @@ -138,6 +145,7 @@ export class ServerService { const [temp, cleanup] = await createTemp(); await genIdenticon(request.params.x, fs.createWriteStream(temp)); reply.header('Content-Type', 'image/png'); + reply.header('Cache-Control', 'public, max-age=86400'); return fs.createReadStream(temp).on('close', () => cleanup()); }); diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 14927da7d..466651f37 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -175,6 +175,7 @@ import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js'; import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js'; import * as ep___i_apps from './endpoints/i/apps.js'; import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js'; +import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js'; import * as ep___i_changePassword from './endpoints/i/change-password.js'; import * as ep___i_deleteAccount from './endpoints/i/delete-account.js'; import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; @@ -329,6 +330,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_stats from './endpoints/users/stats.js'; +import * as ep___users_achievements from './endpoints/users/achievements.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___retention from './endpoints/retention.js'; import { GetterService } from './GetterService.js'; @@ -509,6 +511,7 @@ const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: e const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default }; const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default }; const $i_authorizedApps: Provider = { provide: 'ep:i/authorized-apps', useClass: ep___i_authorizedApps.default }; +const $i_claimAchievement: Provider = { provide: 'ep:i/claim-achievement', useClass: ep___i_claimAchievement.default }; const $i_changePassword: Provider = { provide: 'ep:i/change-password', useClass: ep___i_changePassword.default }; const $i_deleteAccount: Provider = { provide: 'ep:i/delete-account', useClass: ep___i_deleteAccount.default }; const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: ep___i_exportBlocking.default }; @@ -663,6 +666,7 @@ const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by- const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default }; const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default }; const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default }; +const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default }; const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default }; @@ -847,6 +851,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_2fa_unregister, $i_apps, $i_authorizedApps, + $i_claimAchievement, $i_changePassword, $i_deleteAccount, $i_exportBlocking, @@ -1001,6 +1006,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_search, $users_show, $users_stats, + $users_achievements, $fetchRss, $retention, ], @@ -1179,6 +1185,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_2fa_unregister, $i_apps, $i_authorizedApps, + $i_claimAchievement, $i_changePassword, $i_deleteAccount, $i_exportBlocking, @@ -1331,6 +1338,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_search, $users_show, $users_stats, + $users_achievements, $fetchRss, $retention, ], diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 54c4206ea..3678fe14e 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -174,6 +174,7 @@ import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js'; import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js'; import * as ep___i_apps from './endpoints/i/apps.js'; import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js'; +import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js'; import * as ep___i_changePassword from './endpoints/i/change-password.js'; import * as ep___i_deleteAccount from './endpoints/i/delete-account.js'; import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; @@ -328,6 +329,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_stats from './endpoints/users/stats.js'; +import * as ep___users_achievements from './endpoints/users/achievements.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___retention from './endpoints/retention.js'; @@ -506,6 +508,7 @@ const eps = [ ['i/2fa/unregister', ep___i_2fa_unregister], ['i/apps', ep___i_apps], ['i/authorized-apps', ep___i_authorizedApps], + ['i/claim-achievement', ep___i_claimAchievement], ['i/change-password', ep___i_changePassword], ['i/delete-account', ep___i_deleteAccount], ['i/export-blocking', ep___i_exportBlocking], @@ -660,6 +663,7 @@ const eps = [ ['users/search', ep___users_search], ['users/show', ep___users_show], ['users/stats', ep___users_stats], + ['users/achievements', ep___users_achievements], ['fetch-rss', ep___fetchRss], ['retention', ep___retention], ]; diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index ee63d291b..ff0a78b92 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -28,8 +28,8 @@ export const meta = { recursiveNesting: { message: 'It can not be structured like nesting folders recursively.', - code: 'NO_SUCH_PARENT_FOLDER', - id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1', + code: 'RECURSIVE_NESTING', + id: 'dbeb024837894013aed44279f9199740', }, }, diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index 3bcd6ff8f..6beef5ab8 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/index.js'; +import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -29,15 +29,36 @@ export default class extends Endpoint { @Inject(DI.usersRepository) private usersRepository: UsersRepository, + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, user, token) => { const isSecure = token == null; - // ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す - return await this.userEntityService.pack(user.id, user, { + const now = new Date(); + const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`; + + // 渡ってきている user はキャッシュされていて古い可能性があるので改めて取得 + const userProfile = await this.userProfilesRepository.findOneOrFail({ + where: { + userId: user.id, + }, + relations: ['user'], + }); + + if (!userProfile.loggedInDates.includes(today)) { + this.userProfilesRepository.update({ userId: user.id }, { + loggedInDates: [...userProfile.loggedInDates, today], + }); + userProfile.loggedInDates = [...userProfile.loggedInDates, today]; + } + + return await this.userEntityService.pack(userProfile.user!, userProfile.user!, { detail: true, includeSecrets: isSecure, + userProfile, }); }); } diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts new file mode 100644 index 000000000..52ae5475b --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts @@ -0,0 +1,28 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { AchievementService } from '@/core/AchievementService.js'; + +export const meta = { + requireCredential: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + required: ['name'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + private achievementService: AchievementService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.achievementService.create(me.id, ps.name); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts new file mode 100644 index 000000000..2a095d83e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/achievements.ts @@ -0,0 +1,31 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UserProfilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + requireCredential: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); + + return profile.achievements; + }); + } +} diff --git a/packages/backend/src/server/api/integration/TwitterServerService.ts b/packages/backend/src/server/api/integration/TwitterServerService.ts index 9cfadbfa1..f31a788d3 100644 --- a/packages/backend/src/server/api/integration/TwitterServerService.ts +++ b/packages/backend/src/server/api/integration/TwitterServerService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import { v4 as uuid } from 'uuid'; import { IsNull } from 'typeorm'; -import autwh from 'autwh'; +import * as autwh from 'autwh'; import type { Config } from '@/config.js'; import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index e2fc27fec..a4513696a 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -24,6 +24,11 @@ const v = localStorage.getItem('v') || VERSION; + let forceError = localStorage.getItem('forceError'); + if (forceError != null) { + renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.') + } + //#region Detect language & fetch translations const localeVersion = localStorage.getItem('localeVersion'); const localeOutdated = (localeVersion == null || localeVersion !== v); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 573e2faf8..7e9e19336 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -1,4 +1,4 @@ -export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const; +export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 93916ccf2..31c125d3a 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -2,11 +2,11 @@ import { defineAsyncComponent, reactive } from 'vue'; import * as misskey from 'misskey-js'; import { showSuspendedDialog } from './scripts/show-suspended-dialog'; import { i18n } from './i18n'; +import { miLocalStorage } from './local-storage'; import { del, get, set } from '@/scripts/idb-proxy'; import { apiUrl } from '@/config'; import { waiting, api, popup, popupMenu, success, alert } from '@/os'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; -import { miLocalStorage } from './local-storage'; // TODO: 他のタブと永続化されたstateを同期 @@ -20,6 +20,11 @@ export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : n export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); export const iAmAdmin = $i != null && $i.isAdmin; +export let notesCount = $i == null ? 0 : $i.notesCount; +export function incNotesCount() { + notesCount++; +} + export async function signout() { waiting(); miLocalStorage.removeItem('account'); diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue new file mode 100644 index 000000000..64fea9635 --- /dev/null +++ b/packages/frontend/src/components/MkAchievements.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index cbdf92453..4f463d73d 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -7,9 +7,9 @@ {{ c.text }} - {{ c.text }} -
- {{ button.text }} + {{ c.text }} +
+ {{ button.text }}
@@ -41,7 +41,7 @@
@@ -62,8 +62,10 @@ const props = withDefaults(defineProps<{ component: AsUiComponent; components: Ref[]; size: 'small' | 'medium' | 'large'; + align: 'left' | 'center' | 'right'; }>(), { size: 'medium', + align: 'left', }); const c = props.component; diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index 03736ac5e..68e0f8185 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -20,6 +20,7 @@ import * as os from '@/os'; import { useInterval } from '@/scripts/use-interval'; import * as game from '@/scripts/clicker-game'; import number from '@/filters/number'; +import { claimAchievement } from '@/scripts/achievements'; defineProps<{ }>(); @@ -30,14 +31,18 @@ let cps = $ref(0); let prevCookies = $ref(0); function onClick(ev: MouseEvent) { + const x = ev.clientX; + const y = ev.clientY; + os.popup(MkPlusOneEffect, { x, y }, {}, 'end'); + saveData.value!.cookies++; saveData.value!.totalCookies++; saveData.value!.totalHandmadeCookies++; saveData.value!.clicked++; - const x = ev.clientX; - const y = ev.clientY; - os.popup(MkPlusOneEffect, { x, y }, {}, 'end'); + if (cookies.value === 1) { + claimAchievement('cookieClicked'); + } } useInterval(() => { diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 82653ca0b..156013b9a 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -33,6 +33,7 @@ import * as Misskey from 'misskey-js'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; +import { claimAchievement } from '@/scripts/achievements'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; @@ -159,9 +160,11 @@ function onDrop(ev: DragEvent) { }).then(() => { // noop }).catch(err => { - switch (err) { - case 'detected-circular-definition': + switch (err.code) { + case 'RECURSIVE_NESTING': + claimAchievement('driveFolderCircularReference'); os.alert({ + type: 'error', title: i18n.ts.unableToProcess, text: i18n.ts.circularReferenceFolder, }); diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 112a64f52..af7175e5c 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -99,6 +99,7 @@ import { stream } from '@/stream'; import { defaultStore } from '@/store'; import { i18n } from '@/i18n'; import { uploadFile, uploads } from '@/scripts/upload'; +import { claimAchievement } from '@/scripts/achievements'; const props = withDefaults(defineProps<{ initialFolder?: Misskey.entities.DriveFolder; @@ -268,9 +269,11 @@ function onDrop(ev: DragEvent): any { }).then(() => { // noop }).catch(err => { - switch (err) { - case 'detected-circular-definition': + switch (err.code) { + case 'RECURSIVE_NESTING': + claimAchievement('driveFolderCircularReference'); os.alert({ + type: 'error', title: i18n.ts.unableToProcess, text: i18n.ts.circularReferenceFolder, }); diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index ee256d926..de8db54bf 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -35,6 +35,8 @@ import * as Misskey from 'misskey-js'; import * as os from '@/os'; import { stream } from '@/stream'; import { i18n } from '@/i18n'; +import { claimAchievement } from '@/scripts/achievements'; +import { $i } from '@/account'; const props = withDefaults(defineProps<{ user: Misskey.entities.UserDetailed, @@ -90,6 +92,21 @@ async function onClick() { userId: props.user.id, }); hasPendingFollowRequestFromYou = true; + + claimAchievement('following1'); + + if ($i.followingCount >= 10) { + claimAchievement('following10'); + } + if ($i.followingCount >= 50) { + claimAchievement('following50'); + } + if ($i.followingCount >= 100) { + claimAchievement('following100'); + } + if ($i.followingCount >= 300) { + claimAchievement('following300'); + } } } } catch (err) { diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index 9912faffe..c0638c0fe 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -13,7 +13,7 @@ :href="image.url" :title="image.name" > - +
GIF
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index c6f861218..f263ae0ce 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -45,7 +45,8 @@ onMounted(() => { src: media.url, w: media.properties.width, h: media.properties.height, - alt: media.name, + alt: media.comment || media.name, + comment: media.comment || media.name, }; if (media.properties.orientation != null && media.properties.orientation >= 5) { [item.w, item.h] = [item.h, item.w]; @@ -69,6 +70,7 @@ onMounted(() => { }, imageClickAction: 'close', tapAction: 'toggle-controls', + bgOpacity: 1, pswpModule: PhotoSwipe, }); @@ -88,9 +90,28 @@ onMounted(() => { [itemData.w, itemData.h] = [itemData.h, itemData.w]; } itemData.msrc = file.thumbnailUrl; + itemData.alt = file.comment || file.name; + itemData.comment = file.comment || file.name; itemData.thumbCropped = true; }); + lightbox.on('uiRegister', () => { + lightbox.pswp.ui.registerElement({ + name: 'altText', + className: 'pwsp__alt-text-container', + appendTo: 'wrapper', + onInit: (el, pwsp) => { + let textBox = document.createElement('p'); + textBox.className = 'pwsp__alt-text _acrylic'; + el.appendChild(textBox); + + pwsp.on('change', (a) => { + textBox.textContent = pwsp.currSlide.data.comment; + }); + }, + }); + }); + lightbox.init(); }); @@ -185,5 +206,36 @@ const previewable = (file: misskey.entities.DriveFile): boolean => { // なぜか機能しない //z-index: v-bind(pswpZIndex); z-index: 2000000; + --pswp-bg: var(--modalBg); +} + +.pswp__bg { + background: var(--modalBg); + backdrop-filter: var(--modalBgFilter); +} + +.pwsp__alt-text-container { + display: flex; + flex-direction: row; + align-items: center; + + position: absolute; + bottom: 30px; + left: 50%; + transform: translateX(-50%); + + width: 75%; + max-width: 800px; +} + +.pwsp__alt-text { + color: var(--fg); + margin: 0 auto; + text-align: center; + padding: var(--margin); + border-radius: var(--radius); + max-height: 8em; + overflow-y: auto; + text-shadow: var(--bg) 0 0 10px, var(--bg) 0 0 3px, var(--bg) 0 0 3px; } diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 9b2501a2e..1f6a2883d 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -143,6 +143,7 @@ import { getNoteMenu } from '@/scripts/get-note-menu'; import { useNoteCapture } from '@/scripts/use-note-capture'; import { deepClone } from '@/scripts/clone'; import { useTooltip } from '@/scripts/use-tooltip'; +import { claimAchievement } from '@/scripts/achievements'; const props = defineProps<{ note: misskey.entities.Note; @@ -268,6 +269,9 @@ function react(viaKeyboard = false): void { noteId: appearNote.id, reaction: reaction, }); + if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { + claimAchievement('reactWithoutRead'); + } }, () => { focus(); }); diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 56061e0e6..48ace56d9 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -159,6 +159,7 @@ import { getNoteMenu } from '@/scripts/get-note-menu'; import { useNoteCapture } from '@/scripts/use-note-capture'; import { deepClone } from '@/scripts/clone'; import { useTooltip } from '@/scripts/use-tooltip'; +import { claimAchievement } from '@/scripts/achievements'; const props = defineProps<{ note: misskey.entities.Note; @@ -279,6 +280,9 @@ function react(viaKeyboard = false): void { noteId: appearNote.id, reaction: reaction, }); + if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { + claimAchievement('reactWithoutRead'); + } }, () => { focus(); }); diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 4f8257991..e992495a7 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -2,6 +2,7 @@
+
@@ -14,6 +15,7 @@ +
{{ i18n.ts._notification.pollEnded }} + {{ i18n.ts._notification.achievementEarned }} {{ notification.header }} @@ -57,6 +60,9 @@ + + {{ i18n.ts._achievements._types['_' + notification.achievement].title }} + {{ i18n.ts.youGotNewFollower }}
{{ i18n.ts.followRequestAccepted }} {{ i18n.ts.receiveFollowRequest }}
|
@@ -82,6 +88,7 @@ import { i18n } from '@/i18n'; import * as os from '@/os'; import { stream } from '@/stream'; import { useTooltip } from '@/scripts/use-tooltip'; +import { $i } from '@/account'; const props = withDefaults(defineProps<{ notification: misskey.entities.Notification; @@ -240,6 +247,12 @@ useTooltip(reactionRef, (showing) => { pointer-events: none; } +.t_achievementEarned { + padding: 3px; + background: #88a6b7; + pointer-events: none; +} + .tail { flex: 1; min-width: 0; @@ -267,9 +280,9 @@ useTooltip(reactionRef, (showing) => { } .text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + display: flex; + width: 100%; + overflow: clip; } .quote { diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index ab5dff8db..f5ae7bcee 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -10,7 +10,7 @@ diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index 25b9da2d0..d12aafd06 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -24,7 +24,7 @@ + + diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index e90dd7ea6..ec4042d18 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -20,6 +20,7 @@ import * as os from '@/os'; import { useTooltip } from '@/scripts/use-tooltip'; import { $i } from '@/account'; import MkReactionEffect from '@/components/MkReactionEffect.vue'; +import { claimAchievement } from '@/scripts/achievements'; const props = defineProps<{ reaction: string; @@ -52,6 +53,9 @@ const toggleReaction = () => { noteId: props.note.id, reaction: props.reaction, }); + if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) { + claimAchievement('reactWithoutRead'); + } } }; diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue index ec199ad27..457504e6c 100644 --- a/packages/frontend/src/components/MkUserCardMini.vue +++ b/packages/frontend/src/components/MkUserCardMini.vue @@ -11,20 +11,28 @@ diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index bc88cf3be..b7dd0296c 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -1,5 +1,6 @@