diff --git a/Dockerfile b/Dockerfile index 9e1381eb45..d7cf130a7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ FROM docker.io/node:20-slim WORKDIR /firefish # Install runtime dependencies -RUN apt-get update && DEBIAN_FRONTEND='noninteractive' apt-get install -y --no-install-recommends zip unzip tini ffmpeg ca-certificates +RUN apt-get update && DEBIAN_FRONTEND='noninteractive' apt-get install -y --no-install-recommends zip unzip tini ffmpeg ca-certificates curl RUN echo 'deb https://deb.debian.org/debian experimental main' | tee /etc/apt/sources.list RUN apt-get update && DEBIAN_FRONTEND='noninteractive' apt-get --target-release experimental install -y --no-install-recommends libc6 diff --git a/README.md b/README.md index 6edbd5efbd..26913a0001 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ If you have access to a server that supports one of the sources below, I recomme ## Dependencies -- At least [NodeJS](https://nodejs.org/en/) v18.16.0 (v20/v21 recommended) +- At least [NodeJS](https://nodejs.org/en/) v18.17.0 (v20/v21 recommended) - At least [PostgreSQL](https://www.postgresql.org/) v12 (v16 recommended) - At least [Redis](https://redis.io/) v7 - Web Proxy (one of the following) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index c2c60786d9..aaad55a158 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -6,8 +6,10 @@ services: container_name: firefish_web restart: unless-stopped depends_on: - - db - - redis + db: + condition: service_healthy + redis: + condition: service_healthy ports: - "3000:3000" networks: @@ -19,6 +21,15 @@ services: - ./custom:/firefish/custom:ro - ./files:/firefish/files - ./.config:/firefish/.config:ro + healthcheck: + test: curl -f http://localhost:3000 || exit 1 + interval: 5s + timeout: 5s + retries: 5 + deploy: + resources: + limits: + memory: 4096M redis: restart: unless-stopped @@ -28,6 +39,16 @@ services: - calcnet volumes: - ./redis:/data + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 5s + retries: 5 + # deploy: + # resources: + # limits: + # memory: 200M + db: restart: unless-stopped @@ -39,6 +60,15 @@ services: - .config/docker.env volumes: - ./db:/var/lib/postgresql/data + healthcheck: + test: pg_isready --user="$${POSTGRES_USER}" --dbname="$${POSTGRES_DB}" + interval: 5s + timeout: 5s + retries: 5 + # deploy: + # resources: + # limits: + # memory: 200M networks: calcnet: diff --git a/docs/api-change.md b/docs/api-change.md index 1fac31a7a9..7bab3af1b3 100644 --- a/docs/api-change.md +++ b/docs/api-change.md @@ -4,6 +4,7 @@ Breaking changes are indicated by the :warning: icon. ## Unreleased +- :warning: `followingCount` and `followersCount` in `users/show` will be `null` (instead of 0) if these values are unavailable. - :warning: `admin/search/index-all` is removed since posts are now indexed automatically. - New optional parameters are added to `notes/search` endpoint: - `sinceDate` diff --git a/docs/changelog.md b/docs/changelog.md index 6d8abed428..b1abd1b04a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -11,9 +11,9 @@ Critical security updates are indicated by the :warning: icon. - Add langage annotation to post contents (!10687) - Add a toggleable setting to show a warning when you attempt to post files without alt text - Fix bugs -- Update documents +- Update documents and example config files -## v20240301 +## [v20240301](https://firefish.dev/firefish/firefish/-/compare/v20240229...v20240301?from_project_id=7&straight=false) - Add a page (`/my/follow-requests/sent`) to check your follow requests that haven't been approved - Add ability to hide replies from certain users in timelines @@ -32,34 +32,34 @@ Critical security updates are indicated by the :warning: icon. - Disable new user registration - Fix bugs -## v20240229 +## [v20240229](https://firefish.dev/firefish/firefish/-/compare/v20240228...v20240229?from_project_id=7&straight=false) - Add ability to pull-down-to-refresh timelines in PWA - Make passkey/security key independent of TOTP (!10670) - Fix bugs -## v20240228 +## [v20240228](https://firefish.dev/firefish/firefish/-/compare/v20240225...v20240228?from_project_id=7&straight=false) - Update "About Firefish" page (!10673) - Fix bugs (!10675 !10676 !10678 !10679) - Remove charts generation to improve performance (#10611) -## v20240225 +## [v20240225](https://firefish.dev/firefish/firefish/-/compare/v20240222...v20240225?from_project_id=7&straight=false) - Fix bugs - Add syntax highlighting in MFM code blocks in various programming languages -## v20240222 +## [v20240222](https://firefish.dev/firefish/firefish/-/compare/v20240221-1...v20240222?from_project_id=7&straight=false) - Enhance Mastodon post import feature (!10652) - Minor style change in the web client - Refactoring -## v20240221-1 +## [v20240221-1](https://firefish.dev/firefish/firefish/-/compare/v20240221...v20240221-1?from_project_id=7&straight=false) - Fix a bug -## v20240221 +## [v20240221](https://firefish.dev/firefish/firefish/-/compare/v20240217-1...v20240221?from_project_id=7&straight=false) - Add the ability to give regular (non-moderator) users permission to manage custom emojis - Fix a bug that made impossible to update user profiles under some conditions @@ -67,11 +67,11 @@ Critical security updates are indicated by the :warning: icon. - It's just a paraphrase of DMs without recipients - You can also convert your existing public posts to private posts -## :warning: v20240217-1 +## :warning: [v20240217-1](https://firefish.dev/firefish/firefish/-/compare/v20240217...v20240217-1?from_project_id=7&straight=false) - Fix a [security issue](https://github.com/misskey-dev/misskey/security/advisories/GHSA-qqrm-9grj-6v32) -## v20240217 +## [v20240217](https://firefish.dev/firefish/firefish/-/compare/v20240216...v20240217?from_project_id=7&straight=false) - Add ability to specify the search engine used in the search bar MFM - Remove auto NSFW media detection @@ -80,49 +80,49 @@ Critical security updates are indicated by the :warning: icon. - Change the second tab on the notifications page from "unread" to "reactions" - Add ability to show a huge post button on the posting form - This is a joke feature inspired by https://mstdn.poyo.me/@prime/110668364208741253 -- Bug fix +- Fix bugs - Add `/api/emojis` endpoint (compatible with Misskey v13) for better experiences with Misskey clients - This does not mean we will continue to maintain API compatibility with Misskey. Instead, we plan to improve the compatibility with the Mastodon API. -## v20240216 +## [v20240216](https://firefish.dev/firefish/firefish/-/compare/v20240215...v20240216?from_project_id=7&straight=false) - Style changes in the web client (a770ef4314e21f17fdce1f19feb3758953b04486 ab39ff5954a392cc6688a02f1723e1702df5e35c 4eefd534d8150e2cd5cf31dddd327edceb5b84dc) - Clicking the "like" button now sends the actual emoji reaction (star, good, heart, etc.) instead of an empty "like" -## v20240215 +## [v20240215](https://firefish.dev/firefish/firefish/-/compare/v20240214...v20240215?from_project_id=7&straight=false) - Separate settings for displaying rounded avatars for cat and non-cat accounts - Add a toggleable setting to replace the chat button with account menu on mobile - Reduce the size of the container image (!10667) -## v20240214 +## [v20240214](https://firefish.dev/firefish/firefish/-/compare/v20240213...v20240214?from_project_id=7&straight=false) - Fix container images -## v20240213 +## [v20240213](https://firefish.dev/firefish/firefish/-/compare/v20240212...v20240213?from_project_id=7&straight=false) -- Bug fix +- Fix bugs - Refactoring -## v20240212 +## [v20240212](https://firefish.dev/firefish/firefish/-/compare/v20240210...v20240212?from_project_id=7&straight=false) - Refactoring - Add a toggleable setting to hide follow buttons in a misclickable position - Add a toggleable setting to show preview in posting form by default -## v20240210 +## [v20240210](https://firefish.dev/firefish/firefish/-/compare/v20240208...v20240210?from_project_id=7&straight=false) - Security update (cf5b42a160ae8a4d94bf3dcea04ce12935ca4f76) - Refactoring -## v20240208 +## [v20240208](https://firefish.dev/firefish/firefish/-/compare/v20240206...v20240208?from_project_id=7&straight=false) -- Bug fix (!10654 !10665) +- Fix bugs (!10654 !10665) - Enlarge profile picture by clicking it (!10659) - Support Pleroma chat (!10660) - [Add documentation about downgrading](./docs/downgrade.md) -## v20240206 +## [v20240206](https://firefish.dev/firefish/firefish/-/compare/v1.0.5-rc...v20240206?from_project_id=7&straight=false) -- Many bug fixes +- Fix many bugs - Per-post language selector (!10616) diff --git a/docs/downgrade.sql b/docs/downgrade.sql index 64622c3af7..a96d8c49db 100644 --- a/docs/downgrade.sql +++ b/docs/downgrade.sql @@ -1,6 +1,9 @@ BEGIN; DELETE FROM "migrations" WHERE name IN ( + 'FixMutingIndices1710690239308', + 'RemoveMentionedUsersColumn1710688552234', + 'NoteFile1710304584214', 'RenameMetaColumns1705944717480', 'SeparateHardMuteWordsAndPatterns1706413792769', 'IndexAltTextAndCw1708872574733', @@ -16,6 +19,33 @@ DELETE FROM "migrations" WHERE name IN ( 'RemoveNativeUtilsMigration1705877093218' ); +-- fix-muting-indices +DROP INDEX "IDX_renote_muting_createdAt"; +DROP INDEX "IDX_renote_muting_muteeId"; +DROP INDEX "IDX_renote_muting_muterId"; +DROP INDEX "IDX_reply_muting_createdAt"; +DROP INDEX "IDX_reply_muting_muteeId"; +DROP INDEX "IDX_reply_muting_muterId"; +CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt"); +CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId"); +CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId"); + +-- remove-mentioned-users-column +ALTER TABLE "note" ADD "mentionedRemoteUsers" text NOT NULL DEFAULT '[]'::text; +CREATE TABLE "temp_mentions_1710688552234" AS + SELECT "id", "url", "uri", "username", "host" + FROM "user" + JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL; +CREATE UNIQUE INDEX "temp_mentions_id" ON "temp_mentions_1710688552234" ("id"); +UPDATE "note" SET "mentionedRemoteUsers" = ( + SELECT COALESCE(json_agg(row_to_json("data")::jsonb - 'id')::text, '[]') FROM "temp_mentions_1710688552234" AS "data" + WHERE "data"."id" = ANY("note"."mentions") +); +DROP TABLE "temp_mentions_1710688552234"; + +-- note-file +DROP TABLE "note_file"; + -- rename-meta-columns ALTER TABLE "meta" RENAME COLUMN "tosUrl" TO "ToSUrl"; ALTER TABLE "meta" RENAME COLUMN "objectStorageUseSsl" TO "objectStorageUseSSL"; diff --git a/docs/notice-for-admins.md b/docs/notice-for-admins.md index 6fa5689642..c52cf61620 100644 --- a/docs/notice-for-admins.md +++ b/docs/notice-for-admins.md @@ -4,6 +4,9 @@ The full-text search engine used in Firefish has been changed to [PGroonga](http ## For systemd/pm2 users +- Required Node.js version has been bumped from v18.16.0 to v18.17.0. +- You need to install PGroonga on your system. Please follow the instructions below. + ### 1. Install PGroonga Please execute `psql --version` to check your PostgreSQL major version. This will print a message like this: diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index f38779bda4..9eb758a74d 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -323,8 +323,8 @@ _2fa: securityKeyInfo: A més de l'autenticació d'empremta digital o PIN, també podeu configurar l'autenticació mitjançant claus de seguretat de maquinari compatibles amb FIDO2 per protegir encara més el vostre compte. - step4: A partir d'ara, qualsevol intent d'inici de sessió futur demanarà aquest - token d'inici de sessió. + step4: A partir d'ara, qualsevol intent d'inici de sessió futur demanarà aquesta + clau d'inici de sessió. registerSecurityKey: Registrar una clau de seguretat o d'accés step1: En primer lloc, instal·la una aplicació d'autenticació (com ara {a} o {b}) al dispositiu. @@ -2067,9 +2067,8 @@ _relayStatus: deleted: Eliminat editNote: Edita la publicació edited: 'Editat el {date} {time}' -signupsDisabled: Actualment, les inscripcions en aquest servidor estan desactivades, - però sempre podeu registrar-vos en un altre servidor. Si teniu un codi d'invitació - per a aquest servidor, introduïu-lo a continuació. +signupsDisabled: Actualment, les inscripcions en aquest servidor estan desactivades. + Si teniu un codi d'invitació per a aquest servidor, introduïu-lo a continuació. userSaysSomethingReasonQuote: '{name} ha citat una publicació que conté {reason}' userSaysSomethingReasonReply: '{name} ha respost a una publicació que conté {reason}' userSaysSomethingReasonRenote: '{name} ha impulsat una publicació que conté {reason}' @@ -2253,7 +2252,34 @@ searchWordsDescription: "Per cercar publicacions, escriu el terme a buscar. Sepa o la ID en aquest camp i fes clic al botó 'Trobar'. Fent clic a 'Cercar' trobarà publicacions que, literalment , continguin la ID/adreça URL." searchPostsWithFiles: Només publicacions amb fitxers -searchCwAndAlt: Inclou avisos de contingut i arxius amb descripcions. +searchCwAndAlt: Inclou avisos de contingut i arxius amb descripcions searchUsers: Publicat per (opcional) searchRange: Publicat dintre de (opcional) publishTimelines: Publica línies de temps per visitants +toPost: Publicà +publishTimelinesDescription: Si està activat, les línies de temps Global i Local es + mostraran a {url} fins i tot sense estar registrat. +noAltTextWarning: Alguns fitxers adjunts no tenen una descripció. T'has s oblidat + d'escriure-les? +showNoAltTextWarning: Mostra un avís si públiques un fitxer sense descripció +toReply: Respon +toQuote: Cita +toEdit: Edita +searchUsersDescription: "Per buscar publicacions concretes d'un usuari/servidor, escriu + la ID (@usuari@exemple.com, o @usuari per un usuari local) o nom del domini (exemple.com).\n + \nSi escrius 'me' (sense cometes), totes les teves publicacions (incloent-hi publicacions + sense llistar, només per a seguidors i secretes) es buscaran.\n\nSi escrius 'local' + (sense cometes), el resultat serà filtrat per mostrar només publicacions d'aquest + servidor." +messagingUnencryptedInfo: Els xats a Firefish no són encriptats d'extrem a extrem. + No comparteixis dades sensibles fent servir Firefish. +searchRangeDescription: "Si vols filtrar per un període de temps, has de fer servir + aquest format: 20220615-20231031\n\nSi no escrius l'any (per exemple 0105-0106 o + 20231105-0110), serà interpretat com l'any en curs.\n\nInclús pots morir la data + de començament o de finalització. Per exemple, -0102 filtrarà els resultats per + mostrar només publicacions fetes abans del 2 de gener d'aquest any, i 20231026- + filtrarà els resultats per mostrar publicacions fetes després del 26 d'octubre del + 2023." +moderationNote: Nota de moderació +ipFirstAcknowledged: Data en què es va veure la adreça IP per primera vegada +driveCapacityOverride: Capacitat del disc esgotada diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 44e8fb1d7c..df6e57ba5f 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -2218,3 +2218,4 @@ renotes: Boosts quotes: Zitate moreUrlsDescription: "Die Seiten, welche angepinnt werde sollen, im Hilfe-Menü in der unteren linken Ecke in folgender Notation angeben:\n\"Anzeigename\": https://example.com/" +toQuote: Zitat diff --git a/locales/en-US.yml b/locales/en-US.yml index e9abbfa2c7..77564ec7bd 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -462,7 +462,7 @@ securityKeyName: "Key name" registerSecurityKey: "Register a security key" lastUsed: "Last used" unregister: "Unregister" -passwordLessLogin: "Password-less login" +passwordLessLogin: "Password-less sign in" resetPassword: "Reset password" newPasswordIs: "The new password is \"{password}\"" reduceUiAnimation: "Reduce UI animations" @@ -527,7 +527,7 @@ disableDrawer: "Don't use drawer-style menus" youHaveNoGroups: "You have no groups" joinOrCreateGroup: "Get invited to a group or create your own." noHistory: "No history available" -signinHistory: "Login history" +signinHistory: "Sign in history" disableAnimatedMfm: "Disable MFM with animation" doing: "Processing..." category: "Category" @@ -717,9 +717,9 @@ useGlobalSetting: "Use global settings" useGlobalSettingDesc: "If turned on, your account's notification settings will be used. If turned off, individual configurations can be made." other: "Other" -regenerateLoginToken: "Regenerate login token" -regenerateLoginTokenDescription: "Regenerates the token used internally during login. - Normally this action is not necessary. If regenerated, all devices will be logged +regenerateLoginToken: "Regenerate sign in token" +regenerateLoginTokenDescription: "Regenerates the token used internally during sign + in. Normally this action is not necessary. If regenerated, all devices will be logged out." setMultipleBySeparatingWithSpace: "Separate multiple entries with spaces." fileIdOrUrl: "File ID or URL" @@ -1001,7 +1001,7 @@ check: "Check" driveCapOverrideLabel: "Change the drive capacity for this user" driveCapOverrideCaption: "Reset the capacity to default by inputting a value of 0 or lower." -requireAdminForView: "You must log in with an administrator account to view this." +requireAdminForView: "You must sign in with an administrator account to view this." isSystemAccount: "This account is created and automatically operated by the system. Please do not moderate, edit, delete, or otherwise tamper with this account, or it may break your server." @@ -1011,7 +1011,7 @@ document: "Documentation" numberOfPageCache: "Number of cached pages" numberOfPageCacheDescription: "Increasing this number will improve convenience for users but cause more server load as well as more memory to be used." -logoutConfirm: "Really log out?" +logoutConfirm: "Really sign out?" lastActiveDate: "Last used at" statusbar: "Status bar" pleaseSelect: "Select an option" @@ -1112,6 +1112,9 @@ signupsDisabled: "Signups on this server are currently disabled. If you have an code for this server, please enter it below." apps: "Apps" sendModMail: "Send Moderation Notice" +moderationNote: "Moderation Note" +ipFirstAcknowledged: "The date of the first acquisition of the IP address" +driveCapacityOverride: "Drive Capacity Override" preventAiLearning: "Prevent AI bot scraping" preventAiLearningDescription: "Request third-party AI language models not to study content you upload, such as posts and images." @@ -1195,7 +1198,9 @@ searchWordsDescription: "To search for posts, enter the search term. Separate wo search.\nFor example, 'morning night' will find posts that contain both 'morning' and 'night', and 'morning OR night' will find posts that contain either 'morning' or 'night' (or both).\nYou can also combine AND/OR conditions like '(morning OR - night) sleepy'.\n\nIf you want to go to a specific user page or post page, enter + night) sleepy'.\nIf you want to search for a sequence of words (e.g., a sentence), you + must put it in double quotes, not to make it an AND search: \"Today I learned\"\n\n + If you want to go to a specific user page or post page, enter the ID or URL in this field and click the 'Lookup' button. Clicking 'Search' will search for posts that literally contain the ID/URL." searchUsers: "Posted by (optional)" @@ -1214,7 +1219,7 @@ searchRangeDescription: "If you want to filter the time period, enter it in this searchPostsWithFiles: "Only posts with files" searchCwAndAlt: "Include content warnings and file descriptions" publishTimelines: "Publish timelines for visitors" -publishTimelinesDescription: "If enabled, the Local and Global timeline will be shown +publishTimelinesDescription: "If enabled, the Local and Global timelines will be shown on {url} even when signed out." noAltTextWarning: "Some attached file(s) have no description. Did you forget to write?" showNoAltTextWarning: "Show a warning if you attempt to post files without a description" @@ -1627,7 +1632,7 @@ _2fa: step2Url: "You can also enter this URL if you're using a desktop program:" step3Title: "Enter an authentication code" step3: "Enter the token provided by your app to finish setup." - step4: "From now on, any future login attempts will ask for such a login token." + step4: "From now on, any future sign in attempts will ask for such a token." securityKeyNotSupported: "Your browser does not support security keys." securityKeyInfo: "Besides fingerprint or PIN authentication, you can also setup authentication via hardware security keys that support FIDO2 to further secure @@ -2220,3 +2225,5 @@ _iconSets: moreUrls: "Pinned pages" moreUrlsDescription: "Enter the pages you want to pin to the help menu in the lower left corner using this notation:\n\"Display name\": https://example.com/" +messagingUnencryptedInfo: "Chats on Firefish are not end-to-end encrypted. Don't share + any sensitive infomation over Firefish." diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 429ce14835..ae612fd478 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -666,7 +666,7 @@ useGlobalSettingDesc: "S'il est activé, les paramètres de notification de votr other: "Autre" regenerateLoginToken: "Régénérer le jeton de connexion" regenerateLoginTokenDescription: "Générer un nouveau jeton d'authentification. Cette - opération ne devrait pas être nécessaire ; lors de la génération d'un nouveau jeton, + opération ne devrait pas être nécessaire ; lors de la génération d'un nouveau jeton, tous les appareils seront déconnectés." setMultipleBySeparatingWithSpace: "Vous pouvez en définir plusieurs, en les séparant par des espaces." @@ -2038,7 +2038,7 @@ noInstances: Il n'y a aucun serveur showLocalPosts: 'Montrer les notes locales dans :' homeTimeline: Timeline d'Accueil socialTimeline: Timeline Sociale -requireAdminForView: Vous avez besoin d'un compte d'administration pour voir cela. +requireAdminForView: Vous avez besoin d'un compte d'administration pour voir ceci. isSystemAccount: Ce compte est créé et géré automatiquement par le système. Veuillez ne pas modérer, éditer, supprimer ou altérer d'une autre manière ce compte, ou cela risque de perturber votre serveur. @@ -2091,9 +2091,8 @@ _experiments: peut entraîner des ralentissements lors du chargement si votre file d'attente est congestionnée. userSaysSomethingReasonQuote: '{name} a cité une publication contenant {reason}' -signupsDisabled: Les inscriptions sur ce serveur sont actuellement désactivés, mais - vous pouvez toujours vous inscrire sur un autre serveur ! Si vous avez un code d'invitation - pour ce serveur, entrez-le ci-dessous s'il vous plait. +signupsDisabled: Les inscriptions sur ce serveur sont actuellement désactivés. Si + vous avez un code d'invitation pour ce serveur, saisissez-le ci-dessous. apps: Applications userSaysSomethingReasonReply: '{noms} a répondu à une publication contenant {raison}' defaultValueIs: 'défaut : {valeur}' @@ -2300,3 +2299,16 @@ searchRangeDescription: "Si vous voulez filtrer par période de temps, saisissez résultats de recherche pour afficher seulement les messages effectués avant le 2 janvier de cette année, et 20231026- filtrera les résultats pour afficher seulement les messages effectués après le 26 octobre 2023." +toReply: Répondre +toPost: Publier +toQuote: Citer +toEdit: Modifier +messagingUnencryptedInfo: Les conversations sur Firefish ne sont pas cryptées. Ne + partagez aucune information sensible sur Firefish. +moderationNote: Note de modération +driveCapacityOverride: Limite de stockage personalisée +ipFirstAcknowledged: La date de la première acquisition de l'adresse IP +noAltTextWarning: Certains fichiers joints n'ont aucune description. Avez-vous oublié + de l'écrire ? +showNoAltTextWarning: Afficher un avertissement si vous essayez de publier des fichiers + sans description diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 6b8e2b9407..a3f6a68f2a 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -415,7 +415,7 @@ securityKeyName: "Nama kunci" registerSecurityKey: "Daftarkan kunci keamanan" lastUsed: "Terakhir digunakan" unregister: "Batalkan pendaftaran" -passwordLessLogin: "Setel login tanpa kata sandi" +passwordLessLogin: "Masuk tanpa kata sandi" resetPassword: "Atur ulang kata sandi" newPasswordIs: "Kata sandi baru adalah \"{password}\"" reduceUiAnimation: "Kurangi animasi antarmuka" @@ -655,10 +655,10 @@ useGlobalSetting: "Gunakan setelan global" useGlobalSettingDesc: "Jika dinyalakan, setelan pemberitahuan akun kamu akan digunakan. Jika dimatikan, konfigurasi secara individu dapat dibuat." other: "Lainnya" -regenerateLoginToken: "Perbarui token login" +regenerateLoginToken: "Perbarui token masuk" regenerateLoginTokenDescription: "Perbarui token yang digunakan secara internal saat - login. Normalnya aksi ini tidak diperlukan. Jika diperbarui, semua perangkat akan - dilogout." + masuk ke akun. Normalnya aksi ini tidak diperlukan. Jika diperbarui, semua perangkat + akan dikeluarkan dari akun." setMultipleBySeparatingWithSpace: "Kamu dapat menyetel banyak dengan memisahkannya menggunakan spasi." fileIdOrUrl: "File-ID atau URL" @@ -1296,8 +1296,8 @@ _2fa: step2Url: "Di aplikasi desktop, masukkan URL berikut:" step3: "Masukkan token yang telah disediakan oleh aplikasimu untuk menyelesaikan pemasangan." - step4: "Mulai sekarang, upaya login apapun akan meminta token login dari aplikasi - otentikasi kamu." + step4: "Mulai sekarang, upaya pemasukan akun apa pun akan meminta token masuk dari + aplikasi autentikasi kamu." securityKeyInfo: "Kamu dapat memasang otentikasi WebAuthN untuk mengamankan proses login lebih lanjut dengan tidak hanya perangkat keras kunci keamanan yang mendukung FIDO2, namun juga sidik jari atau otentikasi PIN pada perangkatmu." @@ -1988,9 +1988,8 @@ moveAccountDescription: Proses ini permanen. Pastikan kamu sudah mengatur alias akun ini ke akun barumu sebelum pindah. Silakan masukkan tag akun dengan format seperti @orang@server.com sendModMail: Kirim Pemberitahuan Moderasi -signupsDisabled: Pendaftaran ke server ini nonaktif, tapi kamu dapat selalu mendaftar - ke server lain! Jika kamu memiliki kode undangan server ini, harap masukkan di bawah - ini. +signupsDisabled: Pendaftaran ke server ini nonaktifkam. Jika kamu memiliki kode undangan + server ini, harap masukkan di bawah ini. enableCustomKaTeXMacro: Aktifkan makro KaTeX khusus isBot: Akun ini akun otomatis customMOTD: MOTD khusus (pesan layar percik) @@ -2185,7 +2184,7 @@ emojiModPerm: Perizinan pengelolaan emoji kustom emojiModPermDescription: "Tambah: Perbolehkan pengguna ini untuk menambahkan emoji kustom baru dan menetapkan tag/kategori/lisensi untuk semua emoji kustom yang telah ditambahkan.\nTambah dan Sunting: Perizinan \"Tambah\" + Perbolehkan pengguna ini - untuk menyunting nama/kategori/tag/lisensi emoji kustom yang sudah ada.\nPerbolehkan + untuk menyunting nama/kategori/tag/lisensi emoji kustom yang sudah ada.\n Perbolehkan Semua: Perizinan \"Tambah dan Sunting\" + Perbolehkan pengguna ini untuk menghapus semua emoji kustom yang sudah ada." private: Privat @@ -2241,7 +2240,7 @@ reloading: Memuat ulang replyMute: Bisukan balasan dalam lini masa searchRange: Dikirim dalam (opsional) searchUsersDescription: "Untuk mencari kiriman oleh pengguna/server tertentu, masukkan - ID (@pengguna@contoh.id, atau @pengguna untuk pengguna lokal) atau nama domain (contoh.id).\n + ID (@pengguna@contoh.id, atau @pengguna untuk pengguna lokal) atau nama domain (contoh.id)\n \nJika kamu memasukkan 'me' ('aku', tanpa tanda kutip), semua kirimanmu (termasuk kiriman yang tidak terdaftar, khusus pengikut, langsung, dan rahasia) akan dicari.\n \nJika Anda memasukkan 'local' (tanpa tanda kutip), hasilnya akan disaring untuk @@ -2253,3 +2252,16 @@ searchRangeDescription: "Jika kamu ingin memfilter periode waktu, masukkan dalam pencarian untuk menampilkan hanya kiriman yang dibuat sebelum tanggal 2 Januari tahun ini, dan 20231026- akan memfilter hasil pencarian untuk menampilkan hanya kiriman yang dibuat setelah tanggal 26 Oktober 2023." +toPost: Kirim +toQuote: Kutip +noAltTextWarning: Beberapa berkas yang dilampirkan tidak memiliki deskripsi. Lupa + menulis deskripsinya? +toEdit: Sunting +showNoAltTextWarning: Tampilkan peringatan jika kamu mencoba mengirim berkas tanpa + deskripsi +toReply: Balas +messagingUnencryptedInfo: Percakapan di Firefish tidak terenkripsi secara ujung ke + ujung. Jangan bagikan informasi sensitif apa pun melalui Firefish. +moderationNote: Catatan Moderasi +driveCapacityOverride: Penimpaan Kapasitas Drive +ipFirstAcknowledged: Tanggal akuisisi pertama dari alamat IP diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 6630bdf79b..c61187a510 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -107,7 +107,7 @@ cantRenote: "この投稿はブーストできません。" cantReRenote: "ブーストをブーストすることはできません。" quote: "引用" quotes: "引用" -toQuote: "引用" +toQuote: "引用する" pinnedNote: "ピン留めされた投稿" pinned: "ピン留め" you: "あなた" @@ -416,7 +416,7 @@ securityKeyName: "キーの名前" registerSecurityKey: "セキュリティキーを登録する" lastUsed: "最後の使用" unregister: "登録を解除" -passwordLessLogin: "パスワード無しでログイン" +passwordLessLogin: "パスワード無しでサインイン" resetPassword: "パスワードをリセット" newPasswordIs: "新しいパスワードは「{password}」です" reduceUiAnimation: "UIのアニメーションを減らす" @@ -481,7 +481,7 @@ disableDrawer: "メニューをドロワーで表示しない" youHaveNoGroups: "グループがありません" joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。" noHistory: "履歴はありません" -signinHistory: "ログイン履歴" +signinHistory: "サインイン履歴" disableAnimatedMfm: "動きのあるMFMを無効にする" doing: "やっています" category: "カテゴリ" @@ -648,8 +648,8 @@ notificationSettingDesc: "表示する通知の種別を選択してください useGlobalSetting: "グローバル設定を使う" useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。" other: "その他" -regenerateLoginToken: "ログイントークンを再生成" -regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。" +regenerateLoginToken: "サインイントークンを再生成" +regenerateLoginTokenDescription: "サインインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスからサインアウトされます。" setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。" fileIdOrUrl: "ファイルIDまたはURL" behavior: "動作" @@ -908,14 +908,14 @@ thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。" check: "チェック" driveCapOverrideLabel: "このユーザーのドライブ容量上限を変更" driveCapOverrideCaption: "0以下を指定すると解除されます。" -requireAdminForView: "閲覧するには管理者アカウントでログインしている必要があります。" +requireAdminForView: "閲覧するには管理者アカウントでサインインしている必要があります。" isSystemAccount: "システムにより自動で作成・管理されているアカウントです。モデレーション・編集・削除を行うとサーバーの動作が不正になる可能性があるため、操作しないでください。" typeToConfirm: "この操作を行うには {x} と入力してください" deleteAccount: "アカウント削除" document: "ドキュメント" numberOfPageCache: "ページキャッシュ数" numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。" -logoutConfirm: "ログアウトしますか?" +logoutConfirm: "サインアウトしますか?" lastActiveDate: "最終利用日時" statusbar: "ステータスバー" pleaseSelect: "選択してください" @@ -1008,7 +1008,8 @@ enableTimelineStreaming: "タイムラインを自動で更新する" searchWords: "検索語句・照会するIDやURL" searchWordsDescription: "投稿を検索するには、ここに検索語句を入力してください。空白区切りでAND検索になり、ORを挟むとOR検索になります。\n 例えば「朝 夜」と入力すると「朝」と「夜」が両方含まれた投稿を検索し、「朝 OR 夜」と入力すると「朝」または「夜」(または両方)が含まれた投稿を検索します。\n - 「(朝 OR 夜) 眠い」のように、AND検索とOR検索を同時に行うこともできます。\n\n特定のユーザーや投稿のページに飛びたい場合には、この欄にID (@user@example.com) + 「(朝 OR 夜) 眠い」のように、AND検索とOR検索を同時に行うこともできます。\n空白を含む文字列をAND検索ではなくそのまま検索したい場合、\"明日 買うもの\"\ + \ のように二重引用符 (\") で囲む必要があります。\n\n特定のユーザーや投稿のページに飛びたい場合には、この欄にID (@user@example.com) や投稿のURLを入力し「照会」を押してください。「検索」を押すとそのIDやURLが文字通り含まれる投稿を検索します。" searchUsers: "投稿元(オプション)" searchUsersDescription: "投稿検索で投稿者を絞りたい場合、@user@example.com(ローカルユーザーなら @user)の形式で投稿者のIDを入力してください。ユーザーIDではなくドメイン名 @@ -2051,3 +2052,7 @@ makePrivate: "秘密にする" makePrivateConfirm: "リモートサーバーに削除リクエストを送信し、投稿の公開範囲を「秘密」にして他の人から見られないようにします。実行しますか?" sentFollowRequests: 未承認のフォローリクエスト noSentFollowRequests: 未承認のフォローリクエストはありません +messagingUnencryptedInfo: FirefishのチャットはE2E暗号化されていません。漏洩してはいけない情報はFirefishで送らないでください。 +moderationNote: モデレーション用のメモ +ipFirstAcknowledged: IPアドレスが最初に取得された日 +driveCapacityOverride: ドライブ容量の変更 diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 51c7b30810..eea5d1b1f6 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -1,7 +1,9 @@ ---- _lang_: "Română" headlineFirefish: "O rețea conectată prin note" -introFirefish: "Bine ai venit! Firefish este un serviciu de microblogging open source și decentralizat.\nCreează \"note\" cu care să îți poți împărți gândurile cu oricine din jurul tău. 📡\nCu \"reacții\" îți poți expirma rapid părerea despre notele oricui. 👍\nHai să explorăm o lume nouă! 🚀" +introFirefish: "Bine ai venit! Firefish este un serviciu de microblogging open source + și decentralizat.\nCreează \"note\" cu care să îți poți împărți gândurile cu oricine + din jurul tău. 📡\nCu \"reacții\" îți poți expirma rapid părerea despre notele oricui. + 👍\nHai să explorăm o lume nouă! 🚀" monthAndDay: "{day}/{month}" search: "Caută" notifications: "Notificări" @@ -44,7 +46,8 @@ copyContent: "Copiază conținutul" copyLink: "Copiază link-ul" delete: "Şterge" deleteAndEdit: "Șterge și editează" -deleteAndEditConfirm: "Ești sigur că vrei să ștergi această notă și să o editezi? Vei pierde reacțiile, re-notele și răspunsurile acesteia." +deleteAndEditConfirm: "Ești sigur că vrei să ștergi această notă și să o editezi? + Vei pierde reacțiile, re-notele și răspunsurile acesteia." addToList: "Adaugă în listă" sendMessage: "Trimite un mesaj" copyUsername: "Copiază numele de utilizator" @@ -64,9 +67,11 @@ import: "Importă" export: "Exportă" files: "Fișiere" download: "Descarcă" -driveFileDeleteConfirm: "Ești sigur ca vrei să ștergi fișierul \"{name}\"? Notele atașate fișierului vor fi șterse și ele." +driveFileDeleteConfirm: "Ești sigur ca vrei să ștergi fișierul \"{name}\"? Notele + atașate fișierului vor fi șterse și ele." unfollowConfirm: "Ești sigur ca vrei să nu mai urmărești pe {name}?" -exportRequested: "Ai cerut un export. S-ar putea să ia un pic. Va fi adăugat in Drive-ul tău odată completat." +exportRequested: "Ai cerut un export. S-ar putea să ia un pic. Va fi adăugat in Drive-ul + tău odată completat." importRequested: "Ai cerut un import. S-ar putea să ia un pic." lists: "Liste" noLists: "Nu ai nici o listă" @@ -81,9 +86,12 @@ error: "Eroare" somethingHappened: "A survenit o eroare" retry: "Reîncearcă" pageLoadError: "A apărut o eroare la încărcarea paginii." -pageLoadErrorDescription: "De obicei asta este cauzat de o eroare de rețea sau cache-ul browser-ului. Încearcă să cureți cache-ul și apoi să încerci din nou puțin mai târziu." -serverIsDead: "Serverul nu răspunde. Te rugăm să aștepți o perioadă și să încerci din nou." -youShouldUpgradeClient: "Pentru a vedea această pagină, te rugăm să îți actualizezi clientul." +pageLoadErrorDescription: "De obicei asta este cauzat de o eroare de rețea sau cache-ul + browser-ului. Încearcă să cureți cache-ul și apoi să încerci din nou puțin mai târziu." +serverIsDead: "Serverul nu răspunde. Te rugăm să aștepți o perioadă și să încerci + din nou." +youShouldUpgradeClient: "Pentru a vedea această pagină, te rugăm să îți actualizezi + clientul." enterListName: "Introdu un nume pentru listă" privacy: "Confidenţialitate" makeFollowManuallyApprove: "Fă cererile de urmărire să necesite aprobare" @@ -137,14 +145,21 @@ emojiUrl: "URL-ul emoji-ului" addEmoji: "Adaugă un emoji" settingGuide: "Setări recomandate" cacheRemoteFiles: "Ține fișierele externe in cache" -cacheRemoteFilesDescription: "Când această setare este dezactivată, fișierele externe sunt încărcate direct din instanța externă. Dezactivarea va scădea utilizarea spațiului de stocare, dar va crește traficul, deoarece thumbnail-urile nu vor fi generate." +cacheRemoteFilesDescription: "Când această setare este dezactivată, fișierele externe + sunt încărcate direct din instanța externă. Dezactivarea va scădea utilizarea spațiului + de stocare, dar va crește traficul, deoarece thumbnail-urile nu vor fi generate." flagAsBot: "Marchează acest cont ca bot" -flagAsBotDescription: "Activează această opțiune dacă acest cont este controlat de un program. Daca e activată, aceasta va juca rolul unui indicator pentru dezvoltatori pentru a preveni interacțiunea în lanțuri infinite cu ceilalți boți și ajustează sistemele interne al Firefish pentru a trata acest cont drept un bot." +flagAsBotDescription: "Activează această opțiune dacă acest cont este controlat de + un program. Daca e activată, aceasta va juca rolul unui indicator pentru dezvoltatori + pentru a preveni interacțiunea în lanțuri infinite cu ceilalți boți și ajustează + sistemele interne al Firefish pentru a trata acest cont drept un bot." flagAsCat: "Marchează acest cont ca pisică" flagAsCatDescription: "Activează această opțiune dacă acest cont este o pisică." flagShowTimelineReplies: "Arată răspunsurile în cronologie" -flagShowTimelineRepliesDescription: "Dacă e activată vor fi arătate în cronologie răspunsurile utilizatorilor către alte notele altor utilizatori." -autoAcceptFollowed: "Aprobă automat cererile de urmărire de la utilizatorii pe care îi urmărești" +flagShowTimelineRepliesDescription: "Dacă e activată vor fi arătate în cronologie + răspunsurile utilizatorilor către alte notele altor utilizatori." +autoAcceptFollowed: "Aprobă automat cererile de urmărire de la utilizatorii pe care + îi urmărești" addAccount: "Adaugă un cont" loginFailed: "Autentificare eșuată" showOnRemote: "Vezi mai multe pe instanța externă" @@ -156,7 +171,11 @@ searchWith: "Caută: {q}" youHaveNoLists: "Nu ai nici o listă" followConfirm: "Ești sigur ca vrei să urmărești pe {name}?" proxyAccount: "Cont proxy" -proxyAccountDescription: "Un cont proxy este un cont care se comportă ca un urmăritor extern pentru utilizatorii puși sub anumite condiții. De exemplu, când un cineva adaugă un utilizator extern intr-o listă, activitatea utilizatorului extern nu va fi adusă în instanță daca nici un utilizator local nu urmărește acel utilizator, așa că în schimb contul proxy îl va urmări." +proxyAccountDescription: "Un cont proxy este un cont care se comportă ca un urmăritor + extern pentru utilizatorii puși sub anumite condiții. De exemplu, când un cineva + adaugă un utilizator extern intr-o listă, activitatea utilizatorului extern nu va + fi adusă în instanță daca nici un utilizator local nu urmărește acel utilizator, + așa că în schimb contul proxy îl va urmări." host: "Gazdă" selectUser: "Selectează un utilizator" recipient: "Destinatar" @@ -186,11 +205,14 @@ instanceInfo: "Informații despre instanță" statistics: "Statistici" clearQueue: "Șterge coada" clearQueueConfirmTitle: "Ești sigur că vrei să cureți coada?" -clearQueueConfirmText: "Orice notă rămasă în coadă nu va fi federată. De obicei această operație nu este necesară." +clearQueueConfirmText: "Orice notă rămasă în coadă nu va fi federată. De obicei această + operație nu este necesară." clearCachedFiles: "Golește cache-ul" -clearCachedFilesConfirm: "Ești sigur că vrei să ștergi toate fișierele externe din cache?" +clearCachedFilesConfirm: "Ești sigur că vrei să ștergi toate fișierele externe din + cache?" blockedInstances: "Instanțe blocate" -blockedInstancesDescription: "Scrie hostname-urile instanțelor pe care dorești să le blochezi. Instanțele listate nu vor mai putea să comunice cu această instanță." +blockedInstancesDescription: "Scrie hostname-urile instanțelor pe care dorești să + le blochezi. Instanțele listate nu vor mai putea să comunice cu această instanță." muteAndBlock: "Amuțiri și Blocări" mutedUsers: "Utilizatori amuțiți" blockedUsers: "Utilizatori blocați" @@ -238,7 +260,8 @@ saved: "Salvat" messaging: "Chat" upload: "Încarcă" keepOriginalUploading: "Păstrează imaginea originală" -keepOriginalUploadingDescription: "Salvează imaginea originala încărcată fără modificări. Dacă e oprită, o versiune pentru afișarea pe web va fi generată la încărcare." +keepOriginalUploadingDescription: "Salvează imaginea originala încărcată fără modificări. + Dacă e oprită, o versiune pentru afișarea pe web va fi generată la încărcare." fromDrive: "Din Drive" fromUrl: "Din URL" uploadFromUrl: "Încarcă dintr-un URL" @@ -254,7 +277,8 @@ agreeTo: "Sunt de acord cu {0}" tos: "Termenii de utilizare" start: "Să începem" home: "Acasă" -remoteUserCaution: "Deoarece acest utilizator este dintr-o instanță externă, informația afișată poate fi incompletă." +remoteUserCaution: "Deoarece acest utilizator este dintr-o instanță externă, informația + afișată poate fi incompletă." activity: "Activitate" images: "Imagini" birthday: "Zi de naștere" @@ -287,7 +311,8 @@ unableToDelete: "Nu se poate șterge" inputNewFileName: "Introdu un nou nume de fișier" inputNewDescription: "Introdu o descriere nouă" inputNewFolderName: "Introdu un nume de folder nou" -circularReferenceFolder: "Destinația folderului este un subfolder al folderului pe care dorești să îl muți." +circularReferenceFolder: "Destinația folderului este un subfolder al folderului pe + care dorești să îl muți." hasChildFilesOrFolders: "Acest folder nu este gol, așa că nu poate fi șters." copyUrl: "Copiază URL" rename: "Redenumește" @@ -318,7 +343,8 @@ yearX: "{year}" pages: "Pagini" enableLocalTimeline: "Activează cronologia locală" enableGlobalTimeline: "Activeaza cronologia globală" -disablingTimelinesInfo: "Administratorii și Moderatorii vor avea mereu access la toate cronologiile, chiar dacă nu sunt activate." +disablingTimelinesInfo: "Administratorii și Moderatorii vor avea mereu access la toate + cronologiile, chiar dacă nu sunt activate." registration: "Inregistrare" enableRegistration: "Activează înregistrările pentru utilizatori noi" invite: "Invită" @@ -330,9 +356,11 @@ bannerUrl: "URL-ul imaginii de banner" backgroundImageUrl: "URL-ul imaginii de fundal" basicInfo: "Informații de bază" pinnedUsers: "Utilizatori fixați" -pinnedUsersDescription: "Scrie utilizatorii, separați prin pauză de rând, care vor fi fixați pe pagina \"Explorează\"." +pinnedUsersDescription: "Scrie utilizatorii, separați prin pauză de rând, care vor + fi fixați pe pagina \"Explorează\"." pinnedPages: "Pagini fixate" -pinnedPagesDescription: "Introdu linkurile Paginilor pe care le vrei fixate in vâruful paginii acestei instanțe, separate de pauze de rând." +pinnedPagesDescription: "Introdu linkurile Paginilor pe care le vrei fixate in vâruful + paginii acestei instanțe, separate de pauze de rând." pinnedClipId: "ID-ul clip-ului pe care să îl fixezi" pinnedNotes: "Notă fixată" hcaptcha: "hCaptcha" @@ -343,14 +371,17 @@ recaptcha: "reCAPTCHA" enableRecaptcha: "Activează reCAPTCHA" recaptchaSiteKey: "Site key" recaptchaSecretKey: "Secret key" -avoidMultiCaptchaConfirm: "Folosirea mai multor sisteme Captcha poate cauza interferență între acestea. Ai dori să dezactivezi alte sisteme Captcha acum active? Dacă preferi să rămână activate, apasă Anulare." +avoidMultiCaptchaConfirm: "Folosirea mai multor sisteme Captcha poate cauza interferență + între acestea. Ai dori să dezactivezi alte sisteme Captcha acum active? Dacă preferi + să rămână activate, apasă Anulare." antennas: "Antene" manageAntennas: "Gestionează Antenele" name: "Nume" antennaSource: "Sursa antenei" antennaKeywords: "Cuvinte cheie ascultate" antennaExcludeKeywords: "Cuvinte cheie excluse" -antennaKeywordsDescription: "Separă cu spații pentru o condiție ȘI sau cu o întrerupere de rând pentru o condiție SAU." +antennaKeywordsDescription: "Separă cu spații pentru o condiție ȘI sau cu o întrerupere + de rând pentru o condiție SAU." notifyAntenna: "Notifică-mă pentru note noi" withFileAntenna: "Doar note cu fișiere" enableServiceworker: "Activează ServiceWorker" @@ -437,7 +468,8 @@ strongPassword: "Parolă puternică" passwordMatched: "Se potrivește!" passwordNotMatched: "Nu se potrivește" signinWith: "Autentifică-te cu {x}" -signinFailed: "Nu se poate autentifica. Numele de utilizator sau parola introduse sunt incorecte." +signinFailed: "Nu se poate autentifica. Numele de utilizator sau parola introduse + sunt incorecte." tapSecurityKey: "Apasă pe cheia ta de securitate." or: "Sau" language: "Limbă" @@ -478,19 +510,26 @@ showFeaturedNotesInTimeline: "Arată notele recomandate în cronologii" objectStorage: "Object Storage" useObjectStorage: "Folosește Object Storage" objectStorageBaseUrl: "URL de bază" -objectStorageBaseUrlDesc: "URL-ul este folosit pentru referință. Specifică URL-ul CDN-ului sau Proxy-ului tău dacă folosești unul. Pentru S3 folosește 'https://.s3.amazonaws.com' și pentru GCS sau servicii echivalente folosește 'https://storage.googleapis.com/', etc." +objectStorageBaseUrlDesc: "URL-ul este folosit pentru referință. Specifică URL-ul + CDN-ului sau Proxy-ului tău dacă folosești unul. Pentru S3 folosește 'https://.s3.amazonaws.com' + și pentru GCS sau servicii echivalente folosește 'https://storage.googleapis.com/', + etc." objectStorageBucket: "Bucket" objectStorageBucketDesc: "Te rog specifică numele bucket-ului furnizorului tău." objectStoragePrefix: "Prefix" objectStoragePrefixDesc: "Fișierele vor fi stocate sub directoare cu acest prefix." objectStorageEndpoint: "Endpoint" -objectStorageEndpointDesc: "Lasă acest câmp gol dacă folosești AWS S3, dacă nu specifică endpoint-ul ca '' sau ':', depinzând de ce serviciu folosești." +objectStorageEndpointDesc: "Lasă acest câmp gol dacă folosești AWS S3, dacă nu specifică + endpoint-ul ca '' sau ':', depinzând de ce serviciu folosești." objectStorageRegion: "Regiune" -objectStorageRegionDesc: "Specifică o regiune precum 'xx-east-1'. Dacă serviciul tău nu face distincția între regiuni lasă acest câmp gol sau introdu 'us-east-1'." +objectStorageRegionDesc: "Specifică o regiune precum 'xx-east-1'. Dacă serviciul tău + nu face distincția între regiuni lasă acest câmp gol sau introdu 'us-east-1'." objectStorageUseSSL: "Folosește SSl" -objectStorageUseSSLDesc: "Oprește această opțiune dacă nu vei folosi HTTPS pentru conexiunile API-ului" +objectStorageUseSSLDesc: "Oprește această opțiune dacă nu vei folosi HTTPS pentru + conexiunile API-ului" objectStorageUseProxy: "Conectează-te prin Proxy" -objectStorageUseProxyDesc: "Oprește această opțiune dacă vei nu folosi un Proxy pentru conexiunile API-ului" +objectStorageUseProxyDesc: "Oprește această opțiune dacă vei nu folosi un Proxy pentru + conexiunile API-ului" objectStorageSetPublicRead: "Setează \"public-read\" pentru încărcare" serverLogs: "Loguri server" deleteAll: "Șterge tot" @@ -518,7 +557,9 @@ sort: "Sortează" ascendingOrder: "Crescător" descendingOrder: "Descrescător" scratchpad: "Scratchpad" -scratchpadDescription: "Scratchpad-ul oferă un mediu de experimentare în AiScript. Poți scrie, executa și verifica rezultatele acestuia interacționând cu Firefish în el." +scratchpadDescription: "Scratchpad-ul oferă un mediu de experimentare în AiScript. + Poți scrie, executa și verifica rezultatele acestuia interacționând cu Firefish + în el." output: "Ieșire" script: "Script" disablePagesScript: "Dezactivează AiScript în Pagini" @@ -526,11 +567,14 @@ updateRemoteUser: "Actualizează informațiile utilizatorului extern" deleteAllFiles: "Șterge toate fișierele" deleteAllFilesConfirm: "Ești sigur că vrei să ștergi toate fișierele?" removeAllFollowing: "Dezurmărește toți utilizatorii urmăriți" -removeAllFollowingDescription: "Asta va dez-urmări toate conturile din {host}. Te rog execută asta numai dacă instanța, de ex., nu mai există." +removeAllFollowingDescription: "Asta va dez-urmări toate conturile din {host}. Te + rog execută asta numai dacă instanța, de ex., nu mai există." userSuspended: "Acest utilizator a fost suspendat." userSilenced: "Acest utilizator a fost setat silențios." yourAccountSuspendedTitle: "Acest cont a fost suspendat" -yourAccountSuspendedDescription: "Acest cont a fost suspendat din cauza încălcării termenilor de serviciu al serverului sau ceva similar. Contactează administratorul dacă ai dori să afli un motiv mai detaliat. Te rog nu crea un cont nou." +yourAccountSuspendedDescription: "Acest cont a fost suspendat din cauza încălcării + termenilor de serviciu al serverului sau ceva similar. Contactează administratorul + dacă ai dori să afli un motiv mai detaliat. Te rog nu crea un cont nou." menu: "Meniu" divider: "Separator" addItem: "Adaugă element" @@ -569,12 +613,14 @@ permission: "Permisiuni" enableAll: "Actevează tot" disableAll: "Dezactivează tot" tokenRequested: "Acordă acces la cont" -pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici." +pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile + setate aici." notificationType: "Tipul notificării" edit: "Editează" emailServer: "Server email" enableEmail: "Activează distribuția de emailuri" -emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola" +emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți + uiți parola" email: "Email" emailAddress: "Adresă de email" smtpConfig: "Configurare Server SMTP" @@ -582,13 +628,15 @@ smtpHost: "Gazdă" smtpPort: "Port" smtpUser: "Nume de utilizator" smtpPass: "Parolă" -emptyToDisableSmtpAuth: "Lasă username-ul și parola necompletate pentru a dezactiva verificarea SMTP" +emptyToDisableSmtpAuth: "Lasă username-ul și parola necompletate pentru a dezactiva + verificarea SMTP" smtpSecure: "Folosește SSL/TLS implicit pentru conecțiunile SMTP" smtpSecureInfo: "Oprește opțiunea asta dacă STARTTLS este folosit" testEmail: "Testează livrarea emailurilor" wordMute: "Cuvinte pe mut" regexpError: "Eroare de Expresie Regulată" -regexpErrorDescription: "A apărut o eroare în expresia regulată pe linia {line} al cuvintelor {tab} setate pe mut:" +regexpErrorDescription: "A apărut o eroare în expresia regulată pe linia {line} al + cuvintelor {tab} setate pe mut:" instanceMute: "Instanțe pe mut" userSaysSomething: "{name} a spus ceva" makeActive: "Activează" @@ -604,10 +652,13 @@ create: "Crează" notificationSetting: "Setări notificări" notificationSettingDesc: "Selectează tipurile de notificări care să fie arătate" useGlobalSetting: "Folosește setările globale" -useGlobalSettingDesc: "Dacă opțiunea e pornită, notificările contului tău vor fi folosite. Dacă e oprită, configurația va fi individuală." +useGlobalSettingDesc: "Dacă opțiunea e pornită, notificările contului tău vor fi folosite. + Dacă e oprită, configurația va fi individuală." other: "Altele" regenerateLoginToken: "Regenerează token de login" -regenerateLoginTokenDescription: "Regenerează token-ul folosit intern în timpul logări. În mod normal asta nu este necesar. Odată regenerat, toate dispozitivele vor fi delogate." +regenerateLoginTokenDescription: "Regenerează token-ul folosit intern în timpul logări. + În mod normal asta nu este necesar. Odată regenerat, toate dispozitivele vor fi + delogate." setMultipleBySeparatingWithSpace: "Separă mai multe intrări cu spații." fileIdOrUrl: "Introdu ID sau URL" behavior: "Comportament" @@ -615,13 +666,15 @@ sample: "exemplu" abuseReports: "Rapoarte" reportAbuse: "Raportează" reportAbuseOf: "Raportează {name}" -fillAbuseReportDescription: "Te rog scrie detaliile legate de acest raport. Dacă este despre o notă specifică, te rog introdu URL-ul ei." +fillAbuseReportDescription: "Te rog scrie detaliile legate de acest raport. Dacă este + despre o notă specifică, te rog introdu URL-ul ei." abuseReported: "Raportul tău a fost trimis. Mulțumim." reporter: "Raportorul" reporteeOrigin: "Originea raportatului" reporterOrigin: "Originea raportorului" forwardReport: "Redirecționează raportul către instanța externă" -forwardReportIsAnonymous: "În locul contului tău, va fi afișat un cont anonim, de sistem, ca raportor către instanța externă." +forwardReportIsAnonymous: "În locul contului tău, va fi afișat un cont anonim, de + sistem, ca raportor către instanța externă." send: "Trimite" abuseMarkAsResolved: "Marchează raportul ca rezolvat" openInNewTab: "Deschide în tab nou" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 820cbdbcc4..2e220950d8 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -873,7 +873,7 @@ recommended: "推荐" check: "检查" driveCapOverrideLabel: "修改此用户的网盘容量" driveCapOverrideCaption: "输入 0 或以下的值将容量重置为默认值。" -requireAdminForView: "需要使用管理员账号登录才能查看。" +requireAdminForView: "您需要使用管理员账号登录才能查看。" isSystemAccount: "该账号由系统自动创建。请不要修改、编辑、删除或以其它方式篡改这个账号,否则可能会破坏您的服务器。" typeToConfirm: "输入 {x} 以确认操作" deleteAccount: "删除账号" @@ -1263,7 +1263,7 @@ _2fa: step2: "然后,扫描屏幕上显示的二维码。" step2Url: "如果您使用的是桌面程序,您也可以输入这个URL:" step3: "输入您的应用提供的令牌以完成设置。" - step4: "从现在开始,任何登录操作都将要求您提供这样一个登录令牌。" + step4: "从现在开始,任何登录操作都将要求您提供这样一个令牌。" securityKeyInfo: "除了指纹或 PIN 身份验证外,您还可以通过支持 FIDO2 的硬件安全密钥设置身份验证,以进一步保护您的账号。" token: 2FA 令牌 step3Title: 输入验证码 @@ -1934,7 +1934,10 @@ migrationConfirm: "您确实确定要将账号迁移到 {account} 吗?此操 noteId: 帖子 ID moveFrom: 从旧账号迁移至此账号 defaultReaction: 发出和收到帖子的默认表情符号反应 -sendModMail: 发送审核通知 +sendModMail: 发送管理通知 +moderationNote: "管理笔记" +ipFirstAcknowledged: "首次获取此 IP 地址的日期" +driveCapacityOverride: "网盘容量变更" isLocked: 该账号设置了关注请求 _filters: notesBefore: 帖子早于 @@ -2044,8 +2047,12 @@ publishTimelines: 为访客发布时间线 publishTimelinesDescription: 如果启用,在用户登出时本地和全局时间线也会显示在 {url} 上。 searchWordsDescription: "要搜索帖子,请输入关键词。交集搜索关键词之间使用空格进行区分,并集搜索关键词之间使用 OR 进行区分。\n例如 '早上 晚上' 将查找包含 '早上' 和 '晚上' 的帖子,而 '早上 OR 晚上' 将查找包含 '早上' 或 '晚上' (以及同时包含两者)的帖子。\n您还可以组合交集/并集条件,例如 - '(早上 OR 晚上) 困了' 。\n\n如果您想转到特定的用户页面或帖子页面,请在此字段中输入用户 ID 或 URL,然后单击 “查询” 按钮。 单击 “搜索” - 将搜索字面包含用户 ID/URL 的帖子。" + '(早上 OR 晚上) 困了' 。\n如果您想搜索单词序列(例如一个英语句子),您必须将其放在双引号中,例如 \"Today I learned\" 以区分于交集搜索。\n + \n如果您想转到特定的用户页面或帖子页面,请在此字段中输入用户 ID 或 URL,然后单击 “查询” 按钮。 单击 “搜索” 将搜索字面包含用户 ID/URL + 的帖子。" searchRangeDescription: "如果您要过滤时间段,请按以下格式输入:20220615-20231031\n\n如果您省略年份(例如 0105-0106 或 20231105-0110),它将被解释为当前年份。\n\n您还可以省略开始日期或结束日期。 例如 -0102 将过滤搜索结果以仅显示今年 1 月 2 日之前发布的帖子,而 20231026- 将过滤结果以仅显示 2023 年 10 月 26 日之后发布的帖子。" +messagingUnencryptedInfo: "Firefish 上的聊天没有经过端到端加密,请不要在聊天中分享您的敏感信息。" +noAltTextWarning: 有些附件没有描述。您是否忘记写描述了? +showNoAltTextWarning: 当您尝试发布没有描述的帖子附件时显示警告 diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 78814ec896..b2c07af664 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -2048,3 +2048,7 @@ searchUsersDescription: "如欲搜尋特定使用者的貼文,請以「@user@e \n輸入「me」以搜尋自己的所有貼文(包含不在主頁顯示、追隨者、指定使用者、祕密貼文)。\n\n輸入「local」以搜尋本地伺服器的貼文。" searchRangeDescription: "如欲搜尋特定期間的貼文,請以「20220615-20231031」的格式輸入日期範圍。\n\n今年的日期可省略年份(例如0105-0106、20231105-0110)。\n\ \n開始日期和結果日期可擇一省略。舉例來說,「-0102」表示僅搜尋今年1月2日為止的貼文,「20231026-」表示僅搜尋2023年10月26日以後的貼文。" +noAltTextWarning: 有些附件沒有說明,您是否忘記寫了? +moderationNote: 管理員備註 +ipFirstAcknowledged: 首次取得此 IP 位址的日期 +driveCapacityOverride: 雲端硬碟容量變更 diff --git a/package.json b/package.json index d3ea3850b4..cb439757a9 100644 --- a/package.json +++ b/package.json @@ -43,14 +43,14 @@ "gulp-terser": "2.1.0" }, "devDependencies": { - "@biomejs/biome": "1.5.3", - "@biomejs/cli-darwin-arm64": "^1.5.3", - "@biomejs/cli-darwin-x64": "^1.5.3", - "@biomejs/cli-linux-arm64": "^1.5.3", - "@biomejs/cli-linux-x64": "^1.5.3", - "@types/node": "20.11.24", + "@biomejs/biome": "1.6.1", + "@biomejs/cli-darwin-arm64": "^1.6.1", + "@biomejs/cli-darwin-x64": "^1.6.1", + "@biomejs/cli-linux-arm64": "^1.6.1", + "@biomejs/cli-linux-x64": "^1.6.1", + "@types/node": "20.11.28", "execa": "8.0.1", "pnpm": "8.15.4", - "typescript": "5.3.3" + "typescript": "5.4.2" } } diff --git a/packages/backend-rs/Cargo.lock b/packages/backend-rs/Cargo.lock index 43cd907126..24a232ebce 100644 --- a/packages/backend-rs/Cargo.lock +++ b/packages/backend-rs/Cargo.lock @@ -128,9 +128,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "arrayvec" @@ -180,16 +180,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atomic-write-file" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8204db279bf648d64fe845bd8840f78b39c8132ed4d6a4194c3b10d4b4cfb0b" -dependencies = [ - "nix", - "rand", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -342,9 +332,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "bytecheck" @@ -388,9 +378,9 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" -version = "1.0.89" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[package]] name = "cfg-if" @@ -406,9 +396,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "chrono" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", @@ -421,9 +411,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.1" +version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" dependencies = [ "clap_builder", "clap_derive", @@ -431,9 +421,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -443,11 +433,11 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.52", @@ -868,9 +858,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "h2" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" dependencies = [ "bytes", "fnv", @@ -922,6 +912,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1321,18 +1317,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "nix" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" -dependencies = [ - "bitflags 2.4.2", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "nom" version = "7.1.3" @@ -1499,7 +1483,7 @@ version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro-error", "proc-macro2", "quote", @@ -1672,9 +1656,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -1799,9 +1783,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.24" +version = "0.11.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2" dependencies = [ "base64", "bytes", @@ -2015,7 +1999,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bd3534a9978d0aa7edd2808dc1f8f31c4d0ecd31ddf71d997b3c98e9f3c9114" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro-error", "proc-macro2", "quote", @@ -2056,7 +2040,7 @@ version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec13bfb4c4aef208f68dbea970dd40d13830c868aa8dcb4e106b956e6bb4f2fa" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "sea-bae", @@ -2283,9 +2267,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2296,9 +2280,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" dependencies = [ "ahash 0.8.11", "atoi", @@ -2308,7 +2292,6 @@ dependencies = [ "chrono", "crc", "crossbeam-queue", - "dotenvy", "either", "event-listener", "futures-channel", @@ -2344,9 +2327,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" dependencies = [ "proc-macro2", "quote", @@ -2357,14 +2340,13 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ - "atomic-write-file", "dotenvy", "either", - "heck", + "heck 0.4.1", "hex", "once_cell", "proc-macro2", @@ -2384,9 +2366,9 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", "base64", @@ -2431,9 +2413,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", "base64", @@ -2462,7 +2444,6 @@ dependencies = [ "rust_decimal", "serde", "serde_json", - "sha1", "sha2", "smallvec", "sqlx-core", @@ -2476,9 +2457,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" dependencies = [ "atoi", "chrono", @@ -2639,18 +2620,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", @@ -2735,9 +2716,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -3013,9 +2994,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "whoami" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fec781d48b41f8163426ed18e8fc2864c12937df9ce54c88ede7bd47270893e" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" dependencies = [ "redox_syscall", "wasite", diff --git a/packages/backend-rs/Cargo.toml b/packages/backend-rs/Cargo.toml index 1c80914c54..63c32fd031 100644 --- a/packages/backend-rs/Cargo.toml +++ b/packages/backend-rs/Cargo.toml @@ -12,31 +12,31 @@ napi = ["dep:napi", "dep:napi-derive"] crate-type = ["cdylib", "lib"] [dependencies] -async-trait = "0.1.75" +async-trait = "0.1.77" cfg-if = "1.0.0" -chrono = "0.4.31" +chrono = "0.4.35" cuid2 = "0.1.2" jsonschema = "0.17.1" once_cell = "1.19.0" parse-display = "0.8.2" rand = "0.8.5" schemars = { version = "0.8.16", features = ["chrono"] } -sea-orm = { version = "0.12.10", features = ["sqlx-postgres", "runtime-tokio-rustls"] } -serde = { version = "1.0.193", features = ["derive"] } -serde_json = "1.0.108" -thiserror = "1.0.52" -tokio = { version = "1.35.1", features = ["full"] } +sea-orm = { version = "0.12.14", features = ["sqlx-postgres", "runtime-tokio-rustls"] } +serde = { version = "1.0.197", features = ["derive"] } +serde_json = "1.0.114" +thiserror = "1.0.58" +tokio = { version = "1.36.0", features = ["full"] } # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix -napi = { version = "2.14.1", default-features = false, features = ["napi6", "tokio_rt"], optional = true } -napi-derive = { version = "2.14.5", optional = true } +napi = { version = "2.16.0", default-features = false, features = ["napi9", "tokio_rt"], optional = true } +napi-derive = { version = "2.16.0", optional = true } basen = "0.1.0" [dev-dependencies] pretty_assertions = "1.4.0" [build-dependencies] -napi-build = "2.1.0" +napi-build = "2.1.2" [profile.release] lto = true diff --git a/packages/backend-rs/src/model/entity/drive_file.rs b/packages/backend-rs/src/model/entity/drive_file.rs index 1a7a0f35b1..a5f7e9bfca 100644 --- a/packages/backend-rs/src/model/entity/drive_file.rs +++ b/packages/backend-rs/src/model/entity/drive_file.rs @@ -64,6 +64,8 @@ pub enum Relation { DriveFolder, #[sea_orm(has_many = "super::messaging_message::Entity")] MessagingMessage, + #[sea_orm(has_many = "super::note_file::Entity")] + NoteFile, #[sea_orm(has_many = "super::page::Entity")] Page, #[sea_orm( @@ -94,6 +96,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::NoteFile.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::Page.def() diff --git a/packages/backend-rs/src/model/entity/mod.rs b/packages/backend-rs/src/model/entity/mod.rs index 38be7561b1..223b07fb4f 100644 --- a/packages/backend-rs/src/model/entity/mod.rs +++ b/packages/backend-rs/src/model/entity/mod.rs @@ -35,6 +35,7 @@ pub mod muting; pub mod note; pub mod note_edit; pub mod note_favorite; +pub mod note_file; pub mod note_reaction; pub mod note_thread_muting; pub mod note_unread; diff --git a/packages/backend-rs/src/model/entity/note.rs b/packages/backend-rs/src/model/entity/note.rs index 4774cae952..f713e2e47e 100644 --- a/packages/backend-rs/src/model/entity/note.rs +++ b/packages/backend-rs/src/model/entity/note.rs @@ -38,8 +38,6 @@ pub struct Model { #[sea_orm(column_name = "visibleUserIds")] pub visible_user_ids: Vec, pub mentions: Vec, - #[sea_orm(column_name = "mentionedRemoteUsers", column_type = "Text")] - pub mentioned_remote_users: String, pub emojis: Vec, pub tags: Vec, #[sea_orm(column_name = "hasPoll")] @@ -100,6 +98,8 @@ pub enum Relation { NoteEdit, #[sea_orm(has_many = "super::note_favorite::Entity")] NoteFavorite, + #[sea_orm(has_many = "super::note_file::Entity")] + NoteFile, #[sea_orm(has_many = "super::note_reaction::Entity")] NoteReaction, #[sea_orm(has_many = "super::note_unread::Entity")] @@ -164,6 +164,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::NoteFile.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::NoteReaction.def() diff --git a/packages/backend-rs/src/model/entity/note_file.rs b/packages/backend-rs/src/model/entity/note_file.rs new file mode 100644 index 0000000000..c75222360b --- /dev/null +++ b/packages/backend-rs/src/model/entity/note_file.rs @@ -0,0 +1,48 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "note_file")] +pub struct Model { + #[sea_orm(column_name = "serialNo", primary_key)] + pub serial_no: i64, + #[sea_orm(column_name = "noteId")] + pub note_id: String, + #[sea_orm(column_name = "fileId")] + pub file_id: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::drive_file::Entity", + from = "Column::FileId", + to = "super::drive_file::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + DriveFile, + #[sea_orm( + belongs_to = "super::note::Entity", + from = "Column::NoteId", + to = "super::note::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Note, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::DriveFile.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Note.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/packages/backend-rs/src/model/entity/prelude.rs b/packages/backend-rs/src/model/entity/prelude.rs index 10d3795362..0935ac9ff7 100644 --- a/packages/backend-rs/src/model/entity/prelude.rs +++ b/packages/backend-rs/src/model/entity/prelude.rs @@ -33,6 +33,7 @@ pub use super::muting::Entity as Muting; pub use super::note::Entity as Note; pub use super::note_edit::Entity as NoteEdit; pub use super::note_favorite::Entity as NoteFavorite; +pub use super::note_file::Entity as NoteFile; pub use super::note_reaction::Entity as NoteReaction; pub use super::note_thread_muting::Entity as NoteThreadMuting; pub use super::note_unread::Entity as NoteUnread; diff --git a/packages/backend/package.json b/packages/backend/package.json index e9e7422727..e87c051fb6 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -22,23 +22,23 @@ "@swc/core-android-arm64": "1.3.11" }, "dependencies": { - "@bull-board/api": "5.14.2", - "@bull-board/koa": "5.14.2", - "@bull-board/ui": "5.14.2", + "@bull-board/api": "5.15.1", + "@bull-board/koa": "5.15.1", + "@bull-board/ui": "5.15.1", "@discordapp/twemoji": "^15.0.2", "@koa/cors": "5.0.0", "@koa/multer": "3.0.2", "@koa/router": "12.0.1", "@ladjs/koa-views": "9.0.0", "@peertube/http-signature": "1.7.0", - "@redocly/openapi-core": "1.10.3", + "@redocly/openapi-core": "1.10.4", "@sinonjs/fake-timers": "11.2.2", "@twemoji/parser": "^15.0.0", - "adm-zip": "^0.5.10", + "adm-zip": "^0.5.12", "ajv": "8.12.0", - "archiver": "7.0.0", + "archiver": "7.0.1", "argon2": "^0.40.1", - "aws-sdk": "2.1571.0", + "aws-sdk": "2.1578.0", "axios": "^1.6.7", "backend-rs": "workspace:*", "bcryptjs": "2.4.3", @@ -51,7 +51,7 @@ "cli-highlight": "2.1.11", "color-convert": "2.0.1", "content-disposition": "0.5.4", - "date-fns": "3.3.1", + "date-fns": "3.5.0", "decompress": "^4.2.1", "deep-email-validator": "0.1.21", "deepl-node": "1.12.0", @@ -60,9 +60,9 @@ "file-type": "19.0.0", "fluent-ffmpeg": "2.1.2", "form-data": "^4.0.0", - "got": "14.2.0", + "got": "14.2.1", "gunzip-maybe": "^1.4.2", - "happy-dom": "^13.6.2", + "happy-dom": "^13.8.6", "hpagent": "1.2.0", "ioredis": "5.3.2", "ip-cidr": "4.0.0", @@ -71,7 +71,7 @@ "json5": "2.2.3", "jsonld": "8.3.2", "jsrsasign": "11.1.0", - "koa": "2.15.0", + "koa": "2.15.1", "koa-body": "^6.0.1", "koa-bodyparser": "4.4.1", "koa-favicon": "2.1.0", @@ -88,7 +88,7 @@ "multer": "1.4.5-lts.1", "nested-property": "4.0.0", "node-fetch": "3.3.2", - "nodemailer": "6.9.11", + "nodemailer": "6.9.12", "opencc-js": "^1.0.5", "os-utils": "0.0.14", "otpauth": "^9.2.2", @@ -100,7 +100,7 @@ "punycode": "2.3.1", "pureimage": "0.4.13", "qrcode": "1.5.3", - "qs": "6.11.2", + "qs": "6.12.0", "random-seed": "0.3.0", "ratelimiter": "3.4.1", "re2": "1.20.10", @@ -115,7 +115,7 @@ "stringz": "2.1.0", "summaly": "2.7.0", "syslog-pro": "1.0.0", - "systeminformation": "5.22.0", + "systeminformation": "5.22.2", "tar-stream": "^3.1.7", "tesseract.js": "^5.0.5", "tinycolor2": "1.6.0", @@ -129,7 +129,7 @@ }, "devDependencies": { "@swc/cli": "0.3.10", - "@swc/core": "1.4.4", + "@swc/core": "1.4.8", "@types/adm-zip": "^0.5.5", "@types/bcryptjs": "2.4.6", "@types/color-convert": "^2.0.3", @@ -138,7 +138,7 @@ "@types/fluent-ffmpeg": "2.1.24", "@types/js-yaml": "4.0.9", "@types/jsonld": "1.5.13", - "@types/jsrsasign": "10.5.12", + "@types/jsrsasign": "10.5.13", "@types/koa": "2.15.0", "@types/koa-bodyparser": "4.3.12", "@types/koa-cors": "0.0.6", @@ -150,7 +150,7 @@ "@types/koa__multer": "2.0.7", "@types/koa__router": "12.0.4", "@types/mocha": "10.0.6", - "@types/node": "20.11.24", + "@types/node": "20.11.28", "@types/node-fetch": "2.6.11", "@types/nodemailer": "6.4.14", "@types/oauth": "0.9.4", @@ -183,7 +183,7 @@ "ts-loader": "9.5.1", "ts-node": "10.9.2", "tsconfig-paths": "4.2.0", - "typescript": "5.3.3", + "typescript": "5.4.2", "webpack": "^5.90.3", "ws": "8.16.0" } diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 9418f59e4c..e54a2889e2 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -71,7 +71,7 @@ function greet() { 136, 0, )( - " If you like Firefish, please consider starring or contributing to the repo. https://firefish.dev/firefish/firefish", + " If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish", ), ); @@ -149,7 +149,7 @@ function showNodejsVersion(): void { nodejsLogger.info(`Version ${process.version} detected.`); - const minVersion = "v18.16.0"; + const minVersion = "v18.17.0"; if (semver.lt(process.version, minVersion)) { nodejsLogger.error(`At least Node.js ${minVersion} required!`); process.exit(1); diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index b14f028e8b..b6c3f0db8f 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -73,6 +73,7 @@ import { UserPending } from "@/models/entities/user-pending.js"; import { Webhook } from "@/models/entities/webhook.js"; import { UserIp } from "@/models/entities/user-ip.js"; import { NoteEdit } from "@/models/entities/note-edit.js"; +import { NoteFile } from "@/models/entities/note-file.js"; import { entities as charts } from "@/services/chart/entities.js"; import { dbLogger } from "./logger.js"; @@ -143,6 +144,7 @@ export const entities = [ Note, NoteEdit, NoteFavorite, + NoteFile, NoteReaction, NoteWatching, NoteThreadMuting, diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 36a9176b32..284a360dd7 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -6,7 +6,7 @@ import { EventEmitter } from "node:events"; import { inspect } from "node:util"; import boot from "./boot/index.js"; -Error.stackTraceLimit = Infinity; +Error.stackTraceLimit = Number.POSITIVE_INFINITY; EventEmitter.defaultMaxListeners = 128; boot().catch((err) => { diff --git a/packages/backend/src/migration/1705877093218-remove-native-utils-migration.ts b/packages/backend/src/migration/1705877093218-remove-native-utils-migration.ts index 098fde3f54..a155a4ff78 100644 --- a/packages/backend/src/migration/1705877093218-remove-native-utils-migration.ts +++ b/packages/backend/src/migration/1705877093218-remove-native-utils-migration.ts @@ -7,7 +7,7 @@ export class RemoveNativeUtilsMigration1705877093218 await queryRunner.query(`DROP TABLE IF EXISTS "reversi_game"`); await queryRunner.query(`DROP TABLE IF EXISTS "reversi_matching"`); await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_note_url" ON "note" ("text")`, + `CREATE INDEX IF NOT EXISTS "IDX_note_url" ON "note" ("url")`, ); await queryRunner.query(`DROP TABLE IF EXISTS "antenna_note"`); await queryRunner.query( @@ -105,10 +105,10 @@ export class RemoveNativeUtilsMigration1705877093218 `CREATE INDEX "IDX_9937ea48d7ae97ffb4f3f063a4" ON "antenna_note" ("read")`, ); await queryRunner.query( - `ALTER TABLE "antenna_note" ADD CONSTRAINT IF NOT EXISTS "FK_0d775946662d2575dfd2068a5f5" FOREIGN KEY ("antennaId") REFERENCES "antenna"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + `ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_0d775946662d2575dfd2068a5f5" FOREIGN KEY ("antennaId") REFERENCES "antenna"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query( - `ALTER TABLE "antenna_note" ADD CONSTRAINT IF NOT EXISTS "FK_bd0397be22147e17210940e125b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + `ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_bd0397be22147e17210940e125b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query(`DROP INDEX IF EXISTS "IDX_note_url"`); await queryRunner.query( @@ -124,10 +124,10 @@ export class RemoveNativeUtilsMigration1705877093218 `CREATE INDEX IF NOT EXISTS "IDX_e247b23a3c9b45f89ec1299d06" ON "reversi_matching" ("childId")`, ); await queryRunner.query( - `ALTER TABLE "reversi_matching" ADD CONSTRAINT IF NOT EXISTS "FK_3b25402709dd9882048c2bbade0" FOREIGN KEY ("parentId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + `ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_3b25402709dd9882048c2bbade0" FOREIGN KEY ("parentId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query( - `ALTER TABLE "reversi_matching" ADD CONSTRAINT IF NOT EXISTS "FK_e247b23a3c9b45f89ec1299d066" FOREIGN KEY ("childId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + `ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_e247b23a3c9b45f89ec1299d066" FOREIGN KEY ("childId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query( `COMMENT ON COLUMN "reversi_matching"."createdAt" IS 'The created date of the ReversiMatching.'`, @@ -139,10 +139,10 @@ export class RemoveNativeUtilsMigration1705877093218 `CREATE INDEX IF NOT EXISTS "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt")`, ); await queryRunner.query( - `ALTER TABLE "reversi_game" ADD CONSTRAINT IF NOT EXISTS "FK_f7467510c60a45ce5aca6292743" FOREIGN KEY ("user1Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + `ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_f7467510c60a45ce5aca6292743" FOREIGN KEY ("user1Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query( - `ALTER TABLE "reversi_game" ADD CONSTRAINT IF NOT EXISTS "FK_6649a4e8c5d5cf32fb03b5da9f6" FOREIGN KEY ("user2Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + `ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_6649a4e8c5d5cf32fb03b5da9f6" FOREIGN KEY ("user2Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query( `COMMENT ON COLUMN "reversi_game"."createdAt" IS 'The created date of the ReversiGame.'`, diff --git a/packages/backend/src/migration/1710304584214-note-file.ts b/packages/backend/src/migration/1710304584214-note-file.ts new file mode 100644 index 0000000000..be0458d297 --- /dev/null +++ b/packages/backend/src/migration/1710304584214-note-file.ts @@ -0,0 +1,41 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class NoteFile1710304584214 implements MigrationInterface { + name = "NoteFile1710304584214"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "note_file" ( + "serialNo" bigserial PRIMARY KEY, + "noteId" varchar(32) NOT NULL, + "fileId" varchar(32) NOT NULL + )`, + ); + await queryRunner.query(` + INSERT INTO "note_file" ("noteId", "fileId") + SELECT "t"."id", "t"."fid" FROM ( + SELECT ROW_NUMBER() OVER () AS "rn", * FROM ( + SELECT "id", UNNEST("fileIds") AS "fid" FROM "note" + ) AS "s" + ) AS "t" + INNER JOIN "drive_file" ON "drive_file"."id" = "t"."fid" + ORDER BY "rn" + `); + await queryRunner.query( + `ALTER TABLE "note_file" ADD FOREIGN KEY ("noteId") REFERENCES "note" ("id") ON DELETE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "note_file" ADD FOREIGN KEY ("fileId") REFERENCES "drive_file" ("id") ON DELETE CASCADE`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_note_file_noteId" ON "note_file" ("noteId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_note_file_fileId" ON "note_file" ("fileId")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "note_file"`); + } +} diff --git a/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts b/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts new file mode 100644 index 0000000000..e2b1eed950 --- /dev/null +++ b/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class RemoveMentionedUsersColumn1710688552234 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "note" DROP COLUMN "mentionedRemoteUsers"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "note" ADD "mentionedRemoteUsers" TEXT NOT NULL DEFAULT '[]'::text`, + ); + await queryRunner.query(`CREATE TEMP TABLE IF NOT EXISTS "temp_mentions" AS + SELECT "id", "url", "uri", "username", "host" + FROM "user" + JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL`); + await queryRunner.query( + `CREATE UNIQUE INDEX "temp_mentions_id" ON "temp_mentions"("id")`, + ); + await queryRunner.query(`UPDATE "note" SET "mentionedRemoteUsers" = ( + SELECT COALESCE(json_agg(row_to_json("data")::jsonb - 'id')::text, '[]') FROM "temp_mentions" AS "data" + WHERE "data"."id" = ANY("note"."mentions") + )`); + } +} diff --git a/packages/backend/src/migration/1710690239308-fix-muting-indices.ts b/packages/backend/src/migration/1710690239308-fix-muting-indices.ts new file mode 100644 index 0000000000..3dc24c2531 --- /dev/null +++ b/packages/backend/src/migration/1710690239308-fix-muting-indices.ts @@ -0,0 +1,57 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class FixMutingIndices1710690239308 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_renote_muting_createdAt"`); + await queryRunner.query(`DROP INDEX "IDX_renote_muting_muteeId"`); + await queryRunner.query(`DROP INDEX "IDX_renote_muting_muterId"`); + await queryRunner.query(`DROP INDEX "IDX_reply_muting_createdAt"`); + await queryRunner.query(`DROP INDEX "IDX_reply_muting_muteeId"`); + await queryRunner.query(`DROP INDEX "IDX_reply_muting_muterId"`); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_createdAt" ON "renote_muting" ("createdAt")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_muteeId" ON "renote_muting" ("muteeId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_muterId" ON "renote_muting" ("muterId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_reply_muting_createdAt" ON "reply_muting" ("createdAt")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_reply_muting_muteeId" ON "reply_muting" ("muteeId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_reply_muting_muterId" ON "reply_muting" ("muterId")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_renote_muting_createdAt"`); + await queryRunner.query(`DROP INDEX "IDX_renote_muting_muteeId"`); + await queryRunner.query(`DROP INDEX "IDX_renote_muting_muterId"`); + await queryRunner.query(`DROP INDEX "IDX_reply_muting_createdAt"`); + await queryRunner.query(`DROP INDEX "IDX_reply_muting_muteeId"`); + await queryRunner.query(`DROP INDEX "IDX_reply_muting_muterId"`); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_reply_muting_createdAt" ON "muting" ("createdAt")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_reply_muting_muteeId" ON "muting" ("muteeId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_reply_muting_muterId" ON "muting" ("muterId")`, + ); + } +} diff --git a/packages/backend/src/misc/convert-host.ts b/packages/backend/src/misc/convert-host.ts index 856ce3c127..65cc3ca1f5 100644 --- a/packages/backend/src/misc/convert-host.ts +++ b/packages/backend/src/misc/convert-host.ts @@ -1,6 +1,10 @@ import { URL } from "node:url"; import config from "@/config/index.js"; import { toASCII } from "punycode"; +import Logger from "@/services/logger.js"; +import { inspect } from "node:util"; + +const logger = new Logger("convert-host"); export function getFullApAccount(username: string, host: string | null) { return host @@ -13,6 +17,20 @@ export function isSelfHost(host: string) { return toPuny(config.host) === toPuny(host); } +export function isSameOrigin(src: unknown): boolean | null { + if (typeof src !== "string") { + logger.debug(`unknown origin: ${inspect(src)}`); + return null; + } + try { + const u = new URL(src); + return u.origin === config.url; + } catch (e) { + logger.debug(inspect(e)); + return false; + } +} + export function extractDbHost(uri: string) { const url = new URL(uri); return toPuny(url.hostname); diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts index 9d698527c4..016b7159c9 100644 --- a/packages/backend/src/misc/schema.ts +++ b/packages/backend/src/misc/schema.ts @@ -32,6 +32,7 @@ import { packedQueueCountSchema } from "@/models/schema/queue.js"; import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js"; import { packedEmojiSchema } from "@/models/schema/emoji.js"; import { packedNoteEdit } from "@/models/schema/note-edit.js"; +import { packedNoteFileSchema } from "@/models/schema/note-file.js"; export const refs = { UserLite: packedUserLiteSchema, @@ -47,6 +48,7 @@ export const refs = { App: packedAppSchema, MessagingMessage: packedMessagingMessageSchema, Note: packedNoteSchema, + NoteFile: packedNoteFileSchema, NoteEdit: packedNoteEdit, NoteReaction: packedNoteReactionSchema, NoteFavorite: packedNoteFavoriteSchema, diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index 5b31a7e336..3c4510b533 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -4,12 +4,17 @@ import { Index, JoinColumn, Column, + ManyToMany, ManyToOne, + OneToMany, + type Relation, } from "typeorm"; import { id } from "../id.js"; +import { Note } from "./note.js"; import { User } from "./user.js"; import { DriveFolder } from "./drive-folder.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; +import { NoteFile } from "./note-file.js"; @Entity() @Index(["userId", "folderId", "id"]) @@ -31,12 +36,6 @@ export class DriveFile { }) public userId: User["id"] | null; - @ManyToOne((type) => User, { - onDelete: "SET NULL", - }) - @JoinColumn() - public user: User | null; - @Index() @Column("varchar", { length: 512, @@ -171,12 +170,6 @@ export class DriveFile { }) public folderId: DriveFolder["id"] | null; - @ManyToOne((type) => DriveFolder, { - onDelete: "SET NULL", - }) - @JoinColumn() - public folder: DriveFolder | null; - @Index() @Column("boolean", { default: false, @@ -205,4 +198,30 @@ export class DriveFile { nullable: true, }) public requestIp: string | null; + + //#region Relations + @OneToMany( + () => NoteFile, + (noteFile: NoteFile) => noteFile.file, + ) + public noteFiles: Relation; + + @ManyToMany( + () => Note, + (note: Note) => note.files, + ) + public notes: Relation; + + @ManyToOne(() => User, { + onDelete: "SET NULL", + }) + @JoinColumn() + public user: User | null; + + @ManyToOne(() => DriveFolder, { + onDelete: "SET NULL", + }) + @JoinColumn() + public folder: DriveFolder | null; + //#endregion Relations } diff --git a/packages/backend/src/models/entities/note-file.ts b/packages/backend/src/models/entities/note-file.ts new file mode 100644 index 0000000000..7e7013e03a --- /dev/null +++ b/packages/backend/src/models/entities/note-file.ts @@ -0,0 +1,45 @@ +import { + Entity, + Index, + Column, + ManyToOne, + PrimaryGeneratedColumn, + type Relation, +} from "typeorm"; +import { Note } from "./note.js"; +import { DriveFile } from "./drive-file.js"; +import { id } from "../id.js"; + +@Entity() +export class NoteFile { + @PrimaryGeneratedColumn("increment") + public serialNo: number; + + @Index("IDX_note_file_noteId", { unique: false }) + @Column({ + ...id(), + nullable: false, + }) + public noteId: Note["id"]; + + @Index("IDX_note_file_fileId", { unique: false }) + @Column({ + ...id(), + nullable: false, + }) + public fileId: DriveFile["id"]; + + //#region Relations + @ManyToOne( + () => Note, + (note: Note) => note.noteFiles, + ) + public note: Relation; + + @ManyToOne( + () => DriveFile, + (file: DriveFile) => file.noteFiles, + ) + public file: Relation; + //#endregion Relations +} diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index e45fb02466..e6fd892af3 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -2,15 +2,20 @@ import { Entity, Index, JoinColumn, + JoinTable, Column, PrimaryColumn, + ManyToMany, ManyToOne, + OneToMany, + type Relation, } from "typeorm"; import { User } from "./user.js"; -import type { DriveFile } from "./drive-file.js"; +import { DriveFile } from "./drive-file.js"; import { id } from "../id.js"; import { noteVisibilities } from "../../types.js"; import { Channel } from "./channel.js"; +import { NoteFile } from "./note-file.js"; @Entity() @Index("IDX_NOTE_TAGS", { synchronize: false }) @@ -34,12 +39,6 @@ export class Note { }) public replyId: Note["id"] | null; - @ManyToOne((type) => Note, { - onDelete: "CASCADE", - }) - @JoinColumn() - public reply: Note | null; - @Index() @Column({ ...id(), @@ -48,12 +47,6 @@ export class Note { }) public renoteId: Note["id"] | null; - @ManyToOne((type) => Note, { - onDelete: "CASCADE", - }) - @JoinColumn() - public renote: Note | null; - @Index() @Column("varchar", { length: 256, @@ -93,12 +86,6 @@ export class Note { }) public userId: User["id"]; - @ManyToOne((type) => User, { - onDelete: "CASCADE", - }) - @JoinColumn() - public user: User | null; - @Column("boolean", { default: false, }) @@ -151,6 +138,8 @@ export class Note { }) public score: number; + // FIXME: file id is not removed from this array even if the file is deleted + // TODO: drop this column and use note_files @Index() @Column({ ...id(), @@ -183,11 +172,6 @@ export class Note { }) public mentions: User["id"][]; - @Column("text", { - default: "[]", - }) - public mentionedRemoteUsers: string; - @Column("varchar", { length: 128, array: true, @@ -216,12 +200,55 @@ export class Note { }) public channelId: Channel["id"] | null; - @ManyToOne((type) => Channel, { + //#region Relations + @OneToMany( + () => NoteFile, + (noteFile: NoteFile) => noteFile.note, + ) + public noteFiles: Relation; + + @ManyToMany( + () => DriveFile, + (file: DriveFile) => file.notes, + ) + @JoinTable({ + name: "note_file", + joinColumn: { + name: "noteId", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "fileId", + referencedColumnName: "id", + }, + }) + public files: Relation; + + @ManyToOne(() => Note, { + onDelete: "CASCADE", + }) + @JoinColumn() + public reply: Note | null; + + @ManyToOne(() => Note, { + onDelete: "CASCADE", + }) + @JoinColumn() + public renote: Note | null; + + @ManyToOne(() => Channel, { onDelete: "CASCADE", }) @JoinColumn() public channel: Channel | null; + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + @JoinColumn() + public user: User | null; + //#endregion Relations + //#region Denormalized fields @Index() @Column("varchar", { @@ -274,10 +301,3 @@ export class Note { } } } - -export type IMentionedRemoteUsers = { - uri: string; - url?: string; - username: string; - host: string; -}[]; diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index 8ae12a63df..5d4ff52198 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -66,12 +66,14 @@ import { InstanceRepository } from "./repositories/instance.js"; import { Webhook } from "./entities/webhook.js"; import { UserIp } from "./entities/user-ip.js"; import { NoteEdit } from "./entities/note-edit.js"; +import { NoteFileRepository } from "./repositories/note-file.js"; export const Announcements = db.getRepository(Announcement); export const AnnouncementReads = db.getRepository(AnnouncementRead); export const Apps = AppRepository; export const Notes = NoteRepository; export const NoteEdits = db.getRepository(NoteEdit); +export const NoteFiles = NoteFileRepository; export const NoteFavorites = NoteFavoriteRepository; export const NoteWatchings = db.getRepository(NoteWatching); export const NoteThreadMutings = db.getRepository(NoteThreadMuting); diff --git a/packages/backend/src/models/repositories/note-file.ts b/packages/backend/src/models/repositories/note-file.ts new file mode 100644 index 0000000000..f755fb5fea --- /dev/null +++ b/packages/backend/src/models/repositories/note-file.ts @@ -0,0 +1,4 @@ +import { db } from "@/db/postgre.js"; +import { NoteFile } from "@/models/entities/note-file.js"; + +export const NoteFileRepository = db.getRepository(NoteFile).extend({}); diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index d880ac9741..7c6105d2f2 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -513,8 +513,8 @@ export const UserRepository = db.getRepository(User).extend({ location: profile!.location, birthday: profile!.birthday, fields: profile!.fields, - followersCount: followersCount || 0, - followingCount: followingCount || 0, + followersCount: followersCount ?? null, + followingCount: followingCount ?? null, notesCount: user.notesCount, pinnedNoteIds: pins.map((pin) => pin.noteId), pinnedNotes: Notes.packMany( @@ -528,8 +528,11 @@ export const UserRepository = db.getRepository(User).extend({ pinnedPage: profile!.pinnedPageId ? Pages.pack(profile!.pinnedPageId, me) : null, - publicReactions: profile!.publicReactions, - ffVisibility: profile!.ffVisibility, + // TODO: federate publicReactions + publicReactions: + user.host == null ? profile!.publicReactions : false, + // TODO: federate ffVisibility + ffVisibility: user.host == null ? profile!.ffVisibility : "private", twoFactorEnabled: profile!.twoFactorEnabled, usePasswordLessLogin: profile!.usePasswordLessLogin, securityKeys: UserSecurityKeys.countBy({ diff --git a/packages/backend/src/models/schema/note-file.ts b/packages/backend/src/models/schema/note-file.ts new file mode 100644 index 0000000000..c9b8a7e181 --- /dev/null +++ b/packages/backend/src/models/schema/note-file.ts @@ -0,0 +1,24 @@ +export const packedNoteFileSchema = { + type: "object", + properties: { + serialNo: { + type: "number", + optional: false, + nullable: false, + }, + noteId: { + type: "string", + optional: false, + nullable: false, + format: "id", + example: "xxxxxxxxxx", + }, + fileId: { + type: "string", + optional: false, + nullable: false, + format: "id", + example: "xxxxxxxxxx", + }, + }, +} as const; diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index 19b8fe49e7..3dc54e1fb5 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -1,6 +1,5 @@ import promiseLimit from "promise-limit"; import * as mfm from "mfm-js"; -import config from "@/config/index.js"; import Resolver from "../resolver.js"; import post from "@/services/note/create.js"; import { extractMentionedUsers } from "@/services/note/create.js"; @@ -14,7 +13,7 @@ import { extractPollFromQuestion } from "./question.js"; import vote from "@/services/note/polls/vote.js"; import { apLogger } from "../logger.js"; import { DriveFile } from "@/models/entities/drive-file.js"; -import { extractDbHost, toPuny } from "@/misc/convert-host.js"; +import { extractDbHost, isSameOrigin, toPuny } from "@/misc/convert-host.js"; import { Emojis, Polls, @@ -234,7 +233,7 @@ export async function createNote( .catch(async (e) => { // トークだったらinReplyToのエラーは無視 const uri = getApId(note.inReplyTo); - if (uri.startsWith(`${config.url}/`)) { + if (isSameOrigin(uri)) { const id = uri.split("/").pop(); const talk = await MessagingMessages.findOneBy({ id }); if (talk) { @@ -439,7 +438,7 @@ export async function resolveNote( } //#endregion - if (uri.startsWith(config.url)) { + if (isSameOrigin(uri)) { throw new StatusError( "cannot resolve local note", 400, @@ -556,7 +555,7 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) { if (!uri) throw new Error("Missing note uri"); // Skip if URI points to this server - if (uri.startsWith(`${config.url}/`)) throw new Error("uri points local"); + if (isSameOrigin(uri)) throw new Error("uri points local"); // A new resolver is created if not specified if (resolver == null) resolver = new Resolver(); diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 1b7bd41bbf..6546f041b8 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -19,7 +19,7 @@ import { UserNotePining } from "@/models/entities/user-note-pining.js"; import { genId } from "@/misc/gen-id.js"; import { UserPublickey } from "@/models/entities/user-publickey.js"; import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; -import { toPuny } from "@/misc/convert-host.js"; +import { isSameOrigin, toPuny } from "@/misc/convert-host.js"; import { UserProfile } from "@/models/entities/user-profile.js"; import { toArray } from "@/prelude/array.js"; import { fetchInstanceMetadata } from "@/services/fetch-instance-metadata.js"; @@ -138,7 +138,7 @@ export async function fetchPerson( if (cached) return cached; // Fetch from the database if the URI points to this server - if (uri.startsWith(`${config.url}/`)) { + if (isSameOrigin(uri)) { const id = uri.split("/").pop(); const u = await Users.findOneBy({ id }); if (u) await uriPersonCache.set(uri, u); @@ -166,7 +166,7 @@ export async function createPerson( ): Promise { if (typeof uri !== "string") throw new Error("uri is not string"); - if (uri.startsWith(config.url)) { + if (isSameOrigin(uri)) { throw new StatusError( "cannot resolve local user", 400, @@ -419,7 +419,7 @@ export async function updatePerson( if (typeof uri !== "string") throw new Error("uri is not string"); // Skip if the URI points to this server - if (uri.startsWith(`${config.url}/`)) { + if (isSameOrigin(uri)) { return; } diff --git a/packages/backend/src/remote/activitypub/models/question.ts b/packages/backend/src/remote/activitypub/models/question.ts index 39963fddbf..59818654fd 100644 --- a/packages/backend/src/remote/activitypub/models/question.ts +++ b/packages/backend/src/remote/activitypub/models/question.ts @@ -1,10 +1,10 @@ -import config from "@/config/index.js"; import Resolver from "../resolver.js"; import type { IObject, IQuestion } from "../type.js"; import { getApId, isQuestion } from "../type.js"; import { apLogger } from "../logger.js"; import { Notes, Polls } from "@/models/index.js"; import type { IPoll } from "@/models/entities/poll.js"; +import { isSameOrigin } from "@/misc/convert-host.js"; export async function extractPollFromQuestion( source: string | IObject, @@ -57,7 +57,7 @@ export async function updateQuestion( const uri = typeof value === "string" ? value : getApId(value); // Skip if URI points to this server - if (uri.startsWith(`${config.url}/`)) throw new Error("uri points local"); + if (isSameOrigin(uri)) throw new Error("uri points local"); //#region Already registered with this server? const note = await Notes.findOneBy({ uri }); diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index f3bce41aa7..bfa424242a 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -1,6 +1,6 @@ import { In, IsNull } from "typeorm"; import config from "@/config/index.js"; -import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js"; +import type { Note } from "@/models/entities/note.js"; import type { DriveFile } from "@/models/entities/drive-file.js"; import { DriveFiles, Notes, Users, Emojis, Polls } from "@/models/index.js"; import type { Emoji } from "@/models/entities/emoji.js"; @@ -61,26 +61,6 @@ export default async function renderNote( const attributedTo = `${config.url}/users/${note.userId}`; - const mentions = ( - JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers - ).map((x) => x.uri); - - let to: string[] = []; - let cc: string[] = []; - - if (note.visibility === "public") { - to = ["https://www.w3.org/ns/activitystreams#Public"]; - cc = [`${attributedTo}/followers`].concat(mentions); - } else if (note.visibility === "home") { - to = [`${attributedTo}/followers`]; - cc = ["https://www.w3.org/ns/activitystreams#Public"].concat(mentions); - } else if (note.visibility === "followers") { - to = [`${attributedTo}/followers`]; - cc = mentions; - } else { - to = mentions; - } - const mentionedUsers = note.mentions.length > 0 ? await Users.findBy({ @@ -88,6 +68,27 @@ export default async function renderNote( }) : []; + const mentionUris = mentionedUsers + // only remote users + .filter((user) => Users.isRemoteUser(user)) + .map((user) => user.uri); + + let to: string[] = []; + let cc: string[] = []; + + if (note.visibility === "public") { + to = ["https://www.w3.org/ns/activitystreams#Public"]; + cc = [`${attributedTo}/followers`].concat(mentionUris); + } else if (note.visibility === "home") { + to = [`${attributedTo}/followers`]; + cc = ["https://www.w3.org/ns/activitystreams#Public"].concat(mentionUris); + } else if (note.visibility === "followers") { + to = [`${attributedTo}/followers`]; + cc = mentionUris; + } else { + to = mentionUris; + } + const hashtagTags = (note.tags || []).map((tag) => renderHashtag(tag)); const mentionTags = mentionedUsers.map((u) => renderMention(u)); diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index 0bc70d37f9..b159a91944 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,4 +1,3 @@ -import { Brackets } from "typeorm"; import { Notes } from "@/models/index.js"; import { Note } from "@/models/entities/note.js"; import define from "@/server/api/define.js"; @@ -7,6 +6,7 @@ import { generateVisibilityQuery } from "@/server/api/common/generate-visibility import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js"; import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js"; import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import type { SelectQueryBuilder } from "typeorm"; export const meta = { tags: ["notes"], @@ -69,91 +69,123 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery( - Notes.createQueryBuilder("note"), - ps.sinceId, - ps.untilId, - ps.sinceDate ?? undefined, - ps.untilDate ?? undefined, - ); + async function search( + modifier?: (query: SelectQueryBuilder) => void, + ): Promise { + const query = makePaginationQuery( + Notes.createQueryBuilder("note"), + ps.sinceId, + ps.untilId, + ps.sinceDate ?? undefined, + ps.untilDate ?? undefined, + ); + modifier?.(query); - if (ps.userId != null) { - query.andWhere("note.userId = :userId", { userId: ps.userId }); + if (ps.userId != null) { + query.andWhere("note.userId = :userId", { userId: ps.userId }); + } + + if (ps.channelId != null) { + query.andWhere("note.channelId = :channelId", { + channelId: ps.channelId, + }); + } + + query.innerJoinAndSelect("note.user", "user"); + + // "from: me": search all (public, home, followers, specified) my posts + // otherwise: search public indexable posts only + if (ps.userId == null || ps.userId !== me?.id) { + query + .andWhere("note.visibility = 'public'") + .andWhere("user.isIndexable = TRUE"); + } + + if (ps.userId != null) { + query.andWhere("note.userId = :userId", { userId: ps.userId }); + } + + if (ps.host === null) { + query.andWhere("note.userHost IS NULL"); + } + if (ps.host != null) { + query.andWhere("note.userHost = :userHost", { userHost: ps.host }); + } + + if (ps.withFiles === true) { + query.andWhere("note.fileIds != '{}'"); + } + + query + .leftJoinAndSelect("user.avatar", "avatar") + .leftJoinAndSelect("user.banner", "banner") + .leftJoinAndSelect("note.reply", "reply") + .leftJoinAndSelect("note.renote", "renote") + .leftJoinAndSelect("reply.user", "replyUser") + .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") + .leftJoinAndSelect("replyUser.banner", "replyUserBanner") + .leftJoinAndSelect("renote.user", "renoteUser") + .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") + .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); + + generateVisibilityQuery(query, me); + if (me) generateMutedUserQuery(query, me); + if (me) generateBlockedUserQuery(query, me); + + return await query.take(ps.limit).getMany(); } - if (ps.channelId != null) { - query.andWhere("note.channelId = :channelId", { - channelId: ps.channelId, - }); - } + let notes: Note[]; if (ps.query != null) { const q = sqlLikeEscape(ps.query); if (ps.searchCwAndAlt) { - query.andWhere( - new Brackets((qb) => { - qb.where("note.text &@~ :q", { q }) - .orWhere("note.cw &@~ :q", { q }) - .orWhere( - `EXISTS ( - SELECT FROM "drive_file" - WHERE - comment &@~ :q - AND - drive_file."id" = ANY(note."fileIds") - )`, - { q }, - ); - }), - ); + // Whether we should return latest notes first + const isDescendingOrder = + (ps.sinceId == null || ps.untilId != null) && + (ps.sinceId != null || + ps.untilId != null || + ps.sinceDate == null || + ps.untilDate != null); + + const compare = isDescendingOrder + ? (lhs: Note, rhs: Note) => + Math.sign(rhs.createdAt.getTime() - lhs.createdAt.getTime()) + : (lhs: Note, rhs: Note) => + Math.sign(lhs.createdAt.getTime() - rhs.createdAt.getTime()); + + notes = [ + ...new Map( + ( + await Promise.all([ + search((query) => { + query.andWhere("note.text &@~ :q", { q }); + }), + search((query) => { + query.andWhere("note.cw &@~ :q", { q }); + }), + search((query) => { + query + .andWhere("drive_file.comment &@~ :q", { q }) + .innerJoin("note.files", "drive_file"); + }), + ]) + ) + .flatMap((e) => e) + .map((note) => [note.id, note]), + ).values(), + ] + .sort(compare) + .slice(0, ps.limit); } else { - query.andWhere("note.text &@~ :q", { q }); + notes = await search((query) => { + query.andWhere("note.text &@~ :q", { q }); + }); } + } else { + notes = await search(); } - query.innerJoinAndSelect("note.user", "user"); - - // "from: me": search all (public, home, followers, specified) my posts - // otherwise: search public indexable posts only - if (ps.userId == null || ps.userId !== me?.id) { - query - .andWhere("note.visibility = 'public'") - .andWhere("user.isIndexable = TRUE"); - } - - if (ps.userId != null) { - query.andWhere("note.userId = :userId", { userId: ps.userId }); - } - - if (ps.host === null) { - query.andWhere("note.userHost IS NULL"); - } - if (ps.host != null) { - query.andWhere("note.userHost = :userHost", { userHost: ps.host }); - } - - if (ps.withFiles === true) { - query.andWhere("note.fileIds != '{}'"); - } - - query - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - generateVisibilityQuery(query, me); - if (me) generateMutedUserQuery(query, me); - if (me) generateBlockedUserQuery(query, me); - - const notes: Note[] = await query.take(ps.limit).getMany(); - return await Notes.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index f57983d4b9..68f605ecb2 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -90,6 +90,11 @@ export default define(meta, paramDef, async (ps, me) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); + // TODO: federate ffVisibility + if (profile.userHost != null) { + throw new ApiError(meta.errors.forbidden); + } + if (profile.ffVisibility === "private") { if (me == null || me.id !== user.id) { throw new ApiError(meta.errors.forbidden); diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 84eb7cccc5..c6e3e06f1d 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -89,6 +89,11 @@ export default define(meta, paramDef, async (ps, me) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); + // TODO: federate ffVisibility + if (profile.userHost != null) { + throw new ApiError(meta.errors.forbidden); + } + if (profile.ffVisibility === "private") { if (me == null || me.id !== user.id) { throw new ApiError(meta.errors.forbidden); diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index 483a78865f..1f9aec25a4 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -49,6 +49,11 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { const profile = await UserProfiles.findOneByOrFail({ userId: ps.userId }); + // TODO: federate publicReactions + if (profile.userHost != null) { + throw new ApiError(meta.errors.reactionsNotPublic); + } + if (!profile.publicReactions && (me == null || me.id !== ps.userId)) { throw new ApiError(meta.errors.reactionsNotPublic); } diff --git a/packages/backend/src/services/chart/core.ts b/packages/backend/src/services/chart/core.ts index d11b78c671..75177e70d0 100644 --- a/packages/backend/src/services/chart/core.ts +++ b/packages/backend/src/services/chart/core.ts @@ -463,7 +463,7 @@ export default abstract class Chart { protected commit(diff: Commit, group: string | null = null): void { for (const [k, v] of Object.entries(diff)) { if (v == null || v === 0 || (Array.isArray(v) && v.length === 0)) - // rome-ignore lint/performance/noDelete: needs to be deleted not just set to undefined + // biome-ignore lint/performance/noDelete: needs to be deleted not just set to undefined delete diff[k]; } this.buffer.push({ diff --git a/packages/backend/src/services/messages/create.ts b/packages/backend/src/services/messages/create.ts index 0b3f8eded9..81d1614cb0 100644 --- a/packages/backend/src/services/messages/create.ts +++ b/packages/backend/src/services/messages/create.ts @@ -134,13 +134,6 @@ export async function createMessage( userId: message.userId, visibility: "specified", mentions: [recipientUser].map((u) => u.id), - mentionedRemoteUsers: JSON.stringify( - [recipientUser].map((u) => ({ - uri: u.uri, - username: u.username, - host: u.host, - })), - ), } as Note; let renderedNote: Record = await renderNote( diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index fc7f1265b3..16b2c1deed 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -18,7 +18,6 @@ import { registerOrFetchInstanceDoc } from "@/services/register-or-fetch-instanc import { extractMentions } from "@/misc/extract-mentions.js"; import { extractCustomEmojisFromMfm } from "@/misc/extract-custom-emojis-from-mfm.js"; import { extractHashtags } from "@/misc/extract-hashtags.js"; -import type { IMentionedRemoteUsers } from "@/models/entities/note.js"; import { Note } from "@/models/entities/note.js"; import { Mutings, @@ -31,6 +30,7 @@ import { Channels, ChannelFollowings, NoteThreadMutings, + NoteFiles, } from "@/models/index.js"; import type { DriveFile } from "@/models/entities/drive-file.js"; import type { App } from "@/models/entities/app.js"; @@ -343,6 +343,12 @@ export default async ( const note = await insertNote(user, data, tags, emojis, mentionedUsers); + await NoteFiles.insert( + note.fileIds.map((fileId) => ({ noteId: note.id, fileId })), + ).catch((e) => { + logger.error(inspect(e)); + }); + res(note); // Register host @@ -744,21 +750,6 @@ async function insertNote( // Append mentions data if (mentionedUsers.length > 0) { insert.mentions = mentionedUsers.map((u) => u.id); - const profiles = await UserProfiles.findBy({ userId: In(insert.mentions) }); - insert.mentionedRemoteUsers = JSON.stringify( - mentionedUsers - .filter((u) => Users.isRemoteUser(u)) - .map((u) => { - const profile = profiles.find((p) => p.userId === u.id); - const url = profile != null ? profile.url : null; - return { - uri: u.uri, - url: url == null ? undefined : url, - username: u.username, - host: u.host, - } as IMentionedRemoteUsers[0]; - }), - ); } // 投稿を作成 diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index d96b8cafe1..f8c15b25c5 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -1,4 +1,4 @@ -import { Brackets, In } from "typeorm"; +import { Brackets, In, IsNull, Not } from "typeorm"; import { publishNoteStream } from "@/services/stream.js"; import renderDelete from "@/remote/activitypub/renderer/delete.js"; import renderAnnounce from "@/remote/activitypub/renderer/announce.js"; @@ -6,8 +6,8 @@ import renderUndo from "@/remote/activitypub/renderer/undo.js"; import { renderActivity } from "@/remote/activitypub/renderer/index.js"; import renderTombstone from "@/remote/activitypub/renderer/tombstone.js"; import config from "@/config/index.js"; -import { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js"; -import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js"; +import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js"; +import type { Note } from "@/models/entities/note.js"; import { Notes, Users, Instances } from "@/models/index.js"; import { deliverToFollowers, @@ -199,11 +199,12 @@ async function getMentionedRemoteUsers(note: Note) { const where = [] as any[]; // mention / reply / dm - const uris = ( - JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers - ).map((x) => x.uri); - if (uris.length > 0) { - where.push({ uri: In(uris) }); + if (note.mentions.length > 0) { + where.push({ + id: In(note.mentions), + // only remote users, local users are on the server and do not need to be notified + host: Not(IsNull()), + }); } // renote / quote diff --git a/packages/backend/test/activitypub.ts b/packages/backend/test/activitypub.ts index 7b6f85f5a1..21d5a3b61b 100644 --- a/packages/backend/test/activitypub.ts +++ b/packages/backend/test/activitypub.ts @@ -1,6 +1,6 @@ process.env.NODE_ENV = "test"; -import * as assert from "assert"; +import * as assert from "node:assert"; import rndstr from "rndstr"; import { initDb } from "../src/db/postgre.js"; import { initTestDb } from "./utils.js"; diff --git a/packages/backend/test/ap-request.ts b/packages/backend/test/ap-request.ts index 722977fe14..0f1e0c58bb 100644 --- a/packages/backend/test/ap-request.ts +++ b/packages/backend/test/ap-request.ts @@ -1,4 +1,4 @@ -import * as assert from "assert"; +import * as assert from "node:assert"; import httpSignature from "@peertube/http-signature"; import { genRsaKeyPair } from "../src/misc/gen-key-pair.js"; import { diff --git a/packages/backend/test/api-visibility.ts b/packages/backend/test/api-visibility.ts index 49b1b5a064..67ef6e2894 100644 --- a/packages/backend/test/api-visibility.ts +++ b/packages/backend/test/api-visibility.ts @@ -1,7 +1,7 @@ process.env.NODE_ENV = "test"; -import * as assert from "assert"; -import * as childProcess from "child_process"; +import * as assert from "node:assert"; +import type * as childProcess from "node:child_process"; import { async, post, diff --git a/packages/backend/test/api.ts b/packages/backend/test/api.ts index 0fc2f424ec..86916b1b87 100644 --- a/packages/backend/test/api.ts +++ b/packages/backend/test/api.ts @@ -1,7 +1,7 @@ process.env.NODE_ENV = "test"; -import * as assert from "assert"; -import * as childProcess from "child_process"; +import * as assert from "node:assert"; +import type * as childProcess from "node:child_process"; import { async, post, diff --git a/packages/backend/test/block.ts b/packages/backend/test/block.ts index 100a4ab7d5..e7bc8a74df 100644 --- a/packages/backend/test/block.ts +++ b/packages/backend/test/block.ts @@ -1,7 +1,7 @@ process.env.NODE_ENV = "test"; -import * as assert from "assert"; -import * as childProcess from "child_process"; +import * as assert from "node:assert"; +import type * as childProcess from "node:child_process"; import { async, post, diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 30f36b4653..4495a4264f 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -1,6 +1,6 @@ process.env.NODE_ENV = "test"; -import * as assert from "assert"; +import * as assert from "node:assert"; import { inspect } from "node:util"; import { signup, diff --git a/packages/backend/test/extract-mentions.ts b/packages/backend/test/extract-mentions.ts index f400e1e634..cf25a15f6e 100644 --- a/packages/backend/test/extract-mentions.ts +++ b/packages/backend/test/extract-mentions.ts @@ -1,4 +1,4 @@ -import * as assert from "assert"; +import * as assert from "node:assert"; import { parse } from "mfm-js"; import { extractMentions } from "../src/misc/extract-mentions.js"; diff --git a/packages/backend/test/fetch-resource.ts b/packages/backend/test/fetch-resource.ts index 00c0d736ef..688c3ffa81 100644 --- a/packages/backend/test/fetch-resource.ts +++ b/packages/backend/test/fetch-resource.ts @@ -1,7 +1,7 @@ process.env.NODE_ENV = "test"; -import * as assert from "assert"; -import * as childProcess from "child_process"; +import * as assert from "node:assert"; +import type * as childProcess from "node:child_process"; import * as openapi from "@redocly/openapi-core"; import { async, diff --git a/packages/backend/test/ff-visibility.ts b/packages/backend/test/ff-visibility.ts index efdbe7f0f6..2cd4d3c60e 100644 --- a/packages/backend/test/ff-visibility.ts +++ b/packages/backend/test/ff-visibility.ts @@ -1,7 +1,7 @@ process.env.NODE_ENV = "test"; -import * as assert from "assert"; -import * as childProcess from "child_process"; +import * as assert from "node:assert"; +import type * as childProcess from "node:child_process"; import { async, connectStream, diff --git a/packages/backend/test/get-file-info.ts b/packages/backend/test/get-file-info.ts index b1092af278..cfe1fd445e 100644 --- a/packages/backend/test/get-file-info.ts +++ b/packages/backend/test/get-file-info.ts @@ -1,4 +1,4 @@ -import * as assert from "assert"; +import * as assert from "node:assert"; import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { getFileInfo } from "../src/misc/get-file-info.js"; diff --git a/packages/backend/test/mfm.ts b/packages/backend/test/mfm.ts index 97e8e15e91..215da00dea 100644 --- a/packages/backend/test/mfm.ts +++ b/packages/backend/test/mfm.ts @@ -1,4 +1,4 @@ -import * as assert from "assert"; +import * as assert from "node:assert"; import * as mfm from "mfm-js"; import { fromHtml } from "../src/mfm/from-html.js"; diff --git a/packages/backend/test/mute.ts b/packages/backend/test/mute.ts index 831c2c1ee4..25556954ab 100644 --- a/packages/backend/test/mute.ts +++ b/packages/backend/test/mute.ts @@ -1,7 +1,7 @@ process.env.NODE_ENV = "test"; -import * as assert from "assert"; -import * as childProcess from "child_process"; +import * as assert from "node:assert"; +import type * as childProcess from "node:child_process"; import { async, post, diff --git a/packages/backend/test/note.ts b/packages/backend/test/note.ts index b78138b1ed..a889b254b5 100644 --- a/packages/backend/test/note.ts +++ b/packages/backend/test/note.ts @@ -1,7 +1,7 @@ process.env.NODE_ENV = "test"; -import * as assert from "assert"; -import * as childProcess from "child_process"; +import * as assert from "node:assert"; +import type * as childProcess from "node:child_process"; import { Note } from "../src/models/entities/note.js"; import { api, diff --git a/packages/backend/test/prelude/maybe.ts b/packages/backend/test/prelude/maybe.ts index df589981c3..b88431450a 100644 --- a/packages/backend/test/prelude/maybe.ts +++ b/packages/backend/test/prelude/maybe.ts @@ -1,4 +1,4 @@ -import * as assert from "assert"; +import * as assert from "node:assert"; import { just, nothing } from "../../src/prelude/maybe.js"; describe("just", () => { diff --git a/packages/backend/test/prelude/url.ts b/packages/backend/test/prelude/url.ts index 5d08ff8924..684776f52a 100644 --- a/packages/backend/test/prelude/url.ts +++ b/packages/backend/test/prelude/url.ts @@ -1,4 +1,4 @@ -import * as assert from "assert"; +import * as assert from "node:assert"; import { query } from "../../src/prelude/url.js"; describe("url", () => { diff --git a/packages/backend/test/streaming.ts b/packages/backend/test/streaming.ts index 37171f4184..6a026b6b72 100644 --- a/packages/backend/test/streaming.ts +++ b/packages/backend/test/streaming.ts @@ -1,7 +1,7 @@ process.env.NODE_ENV = "test"; -import * as assert from "assert"; -import * as childProcess from "child_process"; +import * as assert from "node:assert"; +import type * as childProcess from "node:child_process"; import { Following } from "../src/models/entities/following.js"; import { api, diff --git a/packages/backend/test/thread-mute.ts b/packages/backend/test/thread-mute.ts index 88483b51c8..08a32eaf4a 100644 --- a/packages/backend/test/thread-mute.ts +++ b/packages/backend/test/thread-mute.ts @@ -1,7 +1,7 @@ process.env.NODE_ENV = "test"; -import * as assert from "assert"; -import * as childProcess from "child_process"; +import * as assert from "node:assert"; +import type * as childProcess from "node:child_process"; import { async, connectStream, diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json index bc7a9968b5..5fafa1005f 100644 --- a/packages/backend/test/tsconfig.json +++ b/packages/backend/test/tsconfig.json @@ -26,16 +26,9 @@ "paths": { "@/*": ["../src/*"] }, - "typeRoots": [ - "../node_modules/@types", - "../src/@types" - ], - "lib": [ - "esnext" - ] + "typeRoots": ["../node_modules/@types", "../src/@types"], + "lib": ["esnext"] }, "compileOnSave": false, - "include": [ - "./**/*.ts" - ] + "include": ["./**/*.ts"] } diff --git a/packages/backend/test/user-notes.ts b/packages/backend/test/user-notes.ts index cdf5e7dbbb..9b68561b4f 100644 --- a/packages/backend/test/user-notes.ts +++ b/packages/backend/test/user-notes.ts @@ -1,7 +1,7 @@ process.env.NODE_ENV = "test"; -import * as assert from "assert"; -import * as childProcess from "child_process"; +import * as assert from "node:assert"; +import type * as childProcess from "node:child_process"; import { async, post, diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index dfacd0738a..b67dc037d4 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -1,11 +1,11 @@ -import * as childProcess from "child_process"; import { SIGKILL } from "constants"; +import * as childProcess from "node:child_process"; import * as fs from "node:fs"; import * as http from "node:http"; import * as path from "node:path"; import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; -import type { endpoints, Entities } from "firefish-js"; +import type { Entities, endpoints } from "firefish-js"; import FormData from "form-data"; import got from "got"; import fetch from "node-fetch"; diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index a900763cc4..bee0077992 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -25,24 +25,13 @@ "rootDir": "./src", "baseUrl": "./", "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] }, "outDir": "./built", - "types": [ - "node" - ], - "typeRoots": [ - "./node_modules/@types", - "./src/@types" - ], - "lib": [ - "esnext" - ] + "types": ["node"], + "typeRoots": ["./node_modules/@types", "./src/@types"], + "lib": ["esnext"] }, "compileOnSave": false, - "include": [ - "./src/**/*.ts" - ] + "include": ["./src/**/*.ts"] } diff --git a/packages/client/@types/theme.d.ts b/packages/client/@types/theme.d.ts index 5f1b81603d..4cfefc55a4 100644 --- a/packages/client/@types/theme.d.ts +++ b/packages/client/@types/theme.d.ts @@ -1,5 +1,5 @@ declare module "@/themes/*.json5" { - import { Theme } from "@/scripts/theme"; + import type { Theme } from "@/scripts/theme"; const theme: Theme; diff --git a/packages/client/package.json b/packages/client/package.json index f1b76ad13a..00e6c47d27 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -47,7 +47,7 @@ "city-timezones": "^1.2.1", "compare-versions": "6.1.0", "cropperjs": "2.0.0-beta.4", - "date-fns": "3.3.1", + "date-fns": "3.5.0", "emojilib": "^3.0.11", "eslint-config-prettier": "9.1.0", "eslint-plugin-file-progress": "^1.3.0", @@ -69,9 +69,9 @@ "prettier": "3.2.5", "prismjs": "1.29.0", "punycode": "2.3.1", - "rollup": "4.12.0", + "rollup": "4.13.0", "s-age": "1.1.2", - "sass": "1.71.1", + "sass": "1.72.0", "seedrandom": "3.0.5", "stringz": "2.1.0", "swiper": "11.0.7", @@ -81,10 +81,10 @@ "throttle-debounce": "5.0.0", "tinycolor2": "1.6.0", "tinyld": "^1.3.4", - "typescript": "5.3.3", + "typescript": "5.4.2", "unicode-emoji-json": "^0.4.0", "uuid": "9.0.1", - "vite": "5.1.5", + "vite": "5.1.6", "vite-plugin-compression": "^0.5.1", "vue": "3.4.21", "vue-draggable-plus": "^0.3.5", diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index 756e94c971..140d865f01 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -2,9 +2,8 @@ import type { entities } from "firefish-js"; import { defineAsyncComponent } from "vue"; import { i18n } from "./i18n"; import { apiUrl } from "@/config"; +import { me } from "@/me"; import { alert, api, popup, popupMenu, waiting } from "@/os"; -import { $i } from "@/reactiveAccount"; -import icon from "@/scripts/icon"; import { del, get, set } from "@/scripts/idb-proxy"; import { reloadChannel, unisonReload } from "@/scripts/unison-reload"; @@ -12,11 +11,11 @@ import { reloadChannel, unisonReload } from "@/scripts/unison-reload"; export type Account = entities.MeDetailed; -export async function signout() { +export async function signOut() { waiting(); localStorage.removeItem("account"); - await removeAccount($i.id); + await removeAccount(me.id); const accounts = await getAccounts(); @@ -29,7 +28,7 @@ export async function signout() { await fetch(`${apiUrl}/sw/unregister`, { method: "POST", body: JSON.stringify({ - i: $i.token, + i: me.token, endpoint: push.endpoint, }), }); @@ -48,7 +47,7 @@ export async function signout() { document.cookie = "igi=; path=/"; - if (accounts.length > 0) login(accounts[0].token); + if (accounts.length > 0) signIn(accounts[0].token); else unisonReload("/"); } @@ -90,7 +89,7 @@ function fetchAccount(token: string): Promise { if (res.error) { if (res.error.id === "a8c724b3-6e9c-4b46-b1a8-bc3ed6258370") { showSuspendedDialog(); - signout(); + signOut(); } else { alert({ type: "error", @@ -117,22 +116,23 @@ function showSuspendedDialog() { export function updateAccount(accountData) { for (const [key, value] of Object.entries(accountData)) { - $i[key] = value; + me[key] = value; } - localStorage.setItem("account", JSON.stringify($i)); + localStorage.setItem("account", JSON.stringify(me)); } -export function refreshAccount() { - return fetchAccount($i.token).then(updateAccount); +export async function refreshAccount() { + const accountData = await fetchAccount(me.token); + return updateAccount(accountData); } -export async function login(token: Account["token"], redirect?: string) { +export async function signIn(token: Account["token"], redirect?: string) { waiting(); if (_DEV_) console.log("logging as token ", token); - const me = await fetchAccount(token); - localStorage.setItem("account", JSON.stringify(me)); + const newAccount = await fetchAccount(token); + localStorage.setItem("account", JSON.stringify(newAccount)); document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う - await addAccount(me.id, token); + await addAccount(newAccount.id, token); if (redirect) { // 他のタブは再読み込みするだけ @@ -190,11 +190,11 @@ export async function openAccountMenu( } function switchAccountWithToken(token: string) { - login(token); + signIn(token); } const storedAccounts = await getAccounts().then((accounts) => - accounts.filter((x) => x.id !== $i.id), + accounts.filter((x) => x.id !== me.id), ); const accountsPromise = api("users/show", { userIds: storedAccounts.map((x) => x.id), @@ -256,12 +256,12 @@ export async function openAccountMenu( { type: "link", text: i18n.ts.profile, - to: `/@${$i.username}`, - avatar: $i, + to: `/@${me.username}`, + avatar: me, }, null, ]), - ...(opts.includeCurrentAccount ? [createItem($i)] : []), + ...(opts.includeCurrentAccount ? [createItem(me)] : []), ...accountItemPromises, ...(isMobile ?? false ? [ @@ -269,8 +269,8 @@ export async function openAccountMenu( { type: "link", text: i18n.ts.profile, - to: `/@${$i.username}`, - avatar: $i, + to: `/@${me.username}`, + avatar: me, }, ] : [ @@ -304,7 +304,7 @@ export async function openAccountMenu( } else { popupMenu( [ - ...(opts.includeCurrentAccount ? [createItem($i)] : []), + ...(opts.includeCurrentAccount ? [createItem(me)] : []), ...accountItemPromises, ], ev.currentTarget ?? ev.target, diff --git a/packages/client/src/components/MkChatPreview.vue b/packages/client/src/components/MkChatPreview.vue index 2857b001a3..1b330b7b2d 100644 --- a/packages/client/src/components/MkChatPreview.vue +++ b/packages/client/src/components/MkChatPreview.vue @@ -4,7 +4,7 @@ :class="{ isMe: isMe(message), isRead: message.groupId - ? message.reads.includes($i?.id) + ? message.reads.includes(me?.id) : message.isRead, }" :to=" @@ -67,14 +67,14 @@ diff --git a/packages/client/src/components/MkCropperDialog.vue b/packages/client/src/components/MkCropperDialog.vue index 97418aa65d..1dfbd1e2d5 100644 --- a/packages/client/src/components/MkCropperDialog.vue +++ b/packages/client/src/components/MkCropperDialog.vue @@ -42,7 +42,7 @@ import Cropper from "cropperjs"; import tinycolor from "tinycolor2"; import XModalWindow from "@/components/MkModalWindow.vue"; import * as os from "@/os"; -import { $i } from "@/reactiveAccount"; +import { me } from "@/me"; import { defaultStore } from "@/store"; import { apiUrl, url } from "@/config"; import { query } from "@/scripts/url"; @@ -81,7 +81,7 @@ const ok = async () => { method: "POST", body: formData, headers: { - authorization: `Bearer ${$i.token}`, + authorization: `Bearer ${me.token}`, }, }) .then((response) => response.json()) diff --git a/packages/client/src/components/MkDonation.vue b/packages/client/src/components/MkDonation.vue index 095dd7a14b..f213867d65 100644 --- a/packages/client/src/components/MkDonation.vue +++ b/packages/client/src/components/MkDonation.vue @@ -10,7 +10,7 @@
{{ i18n.ts._aboutFirefish.pleaseDonateToFirefish }} -

+

{{ i18n.t("_aboutFirefish.pleaseDonateToHost", { host: hostname, @@ -27,9 +27,9 @@ >{{ i18n.ts._aboutFirefish.donate }} {{ i18n.t("_aboutFirefish.donateHost", { host: hostname, diff --git a/packages/client/src/components/MkDrive.file.vue b/packages/client/src/components/MkDrive.file.vue index c1507151ca..53847bfd40 100644 --- a/packages/client/src/components/MkDrive.file.vue +++ b/packages/client/src/components/MkDrive.file.vue @@ -9,11 +9,11 @@ @dragstart="onDragstart" @dragend="onDragend" > -

+

{{ i18n.ts.avatar }}

-
+

{{ i18n.ts.banner }}

@@ -45,7 +45,7 @@ import MkDriveFileThumbnail from "@/components/MkDriveFileThumbnail.vue"; import bytes from "@/filters/bytes"; import * as os from "@/os"; import { i18n } from "@/i18n"; -import { $i } from "@/reactiveAccount"; +import { me } from "@/me"; import icon from "@/scripts/icon"; const props = withDefaults( diff --git a/packages/client/src/components/MkFollowButton.vue b/packages/client/src/components/MkFollowButton.vue index e705e8c89c..e71463bbe1 100644 --- a/packages/client/src/components/MkFollowButton.vue +++ b/packages/client/src/components/MkFollowButton.vue @@ -8,7 +8,7 @@