diff --git a/.config/example.yml b/.config/example.yml index 4146881b1..8d4a162cb 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -108,11 +108,6 @@ redis: #deliverJobMaxAttempts: 12 #inboxJobMaxAttempts: 8 -# Syslog option -#syslog: -# host: localhost -# port: 514 - # Proxy for HTTP/HTTPS outgoing connections #proxy: http://127.0.0.1:3128 diff --git a/CHANGELOG.md b/CHANGELOG.md index 85229a4c9..1e1099f2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,72 @@ Unreleased changes should not be listed in this file. Instead, run `git shortlog --format='%h %s' --group=trailer:changelog ..` to see unreleased changes; replace `` with the tag you wish to compare from. If you are a contributor, please read [CONTRIBUTING.md, section "Changelog Trailer"](./CONTRIBUTING.md#changelog-trailer) on what to do instead. +## 13.0.0-preview5 - 2023-05-23 +This release contains 6 breaking changes and 1 security update. + +### Security +- client: check input for aiscript +- server: validate filenames and emoji names on emoji import +- server: check URL schema of ActivityPub URIs +- server: check schema for URL previews +- server: update summaly dependency + +### Added +- client: impolement filtering and sorting in drive +- client: add "nobody" follower/following visibility +- client: re-add flag to require approval for bot follows +- client: show waveform on audio player +- client: add new deepl languages +- client: add instructions on remote interaction (when signed out) +- client: show follow button when not logged in +- server: show worker mode in process names +- server: drive endpoint to fetch files and folders combined +- activitypub: implement receiving account moves + +### Changed +- **BREAKING** server: restructure endpoints related to user administration +- **BREAKING** server: refactor streaming API data structures +- **BREAKING** server: rename configuration environment variables + The environment variables that could be used for configuration which were previously prefixed with `MK_` + are now prefixed with `FK_` instead. +- server: improve error message for invalidating follows +- server: add pagination to file attachment timeline + +### Fixed +- **BREAKING** server: properly respect follower/following visibility setting on statistics endpoint + This affects the endpoint `/api/users/stats`. +- improve documentation for `fetch-rss` endpoint +- client: fix authentication error in RSS widget +- client: fix attached files and account switcher combination in new note form +- client: improved module tracker file detection +- client: fix follow requests pagination +- client: Theme creator breaks after creating a theme +- client: replace error UUIDs with error codes +- client: allow opening links in new tab + The usual 3rd button click (usually mouse wheel) or Ctrl+Click should now work to open a link in a new tab. +- client: fix drive item updates inserting duplicate entries +- client: improve error messages for failed uploads +- client: stop unnecessary network congestion by websocket ping mechanism +- server: don't fail if a system user was already created +- server: better matching for MFM mentions +- server: fix rate limit for adding reactions +- server: check instance description length limit +- server: dont error on generating RSS feeds for profiles without public posts +- server: group delivering `Delete` activities to improve performance +- server: fix drive quota for remote users +- server: user deletion race condition (again) + +### Removed +- **BREAKING** server: remove unused API parameters `sinceId` and `untilId` from `/api/notes/reactions`. +- **BREAKING** server: remove syslog integration + If you used syslog before, the syslog protocoll will no longer be connected to. + The configuration entries for `syslog` will be ignored, you should remove them if they are set. +- client: remove `driveFolderBg` theme colour +- activitypub: remove `_misskey_content` attribute +- activitypub: remove `_misskey_reaction` attribute +- activitypub: remove `_misskey_votes` attribute +- foundkey-js: remove unused definitions for Ads and detailed instance metadata + ## 13.0.0-preview4 - 2023-02-05 This release contains 6 breaking changes, including changes to the configuration file format. diff --git a/docs/emoji.md b/docs/emoji.md index 257cbc39d..abfd13632 100644 --- a/docs/emoji.md +++ b/docs/emoji.md @@ -18,12 +18,11 @@ Please note that Emoji may be subject to copyright and you are responsible for c If you have an image file that you would like to turn into a custom emoji you can import the image as an emoji. This works just like attaching files to a note: -You can choose to upload a new file, pick a file from your Misskey drive or upload a file from another URL. +You can choose to upload a new file, pick a file from your Foundkey drive or upload a file from another URL. -::: danger +**Warning:** When you import emoji from your drive, the file will remain inside your drive. -Misskey does not make a copy of this file so if you delete it, the emoji will be broken. -::: +Foundkey does not make a copy of this file so if you delete it, the emoji will be broken. The emoji will be added to the instance and you will then be able to edit or delete it as usual. @@ -32,10 +31,9 @@ The emoji will be added to the instance and you will then be able to edit or del Emojis can be imported in bulk as packed ZIP files with a special format. This ability can be found in the three dots menu in the top right corner of the custom emoji menu. -::: warning +**Warning:** Bulk emoji import may overwrite existing emoji or otherwise mess up your instance. Be sure to only import emoji from trusted sources, ideally only ones you exported yourself. -::: ### Packed emoji format @@ -89,10 +87,9 @@ The properties of an emoji can be edited by clicking it in the list of local emo When you click on a custom emoji, a dialog for editing the properties will open. This dialog will also allow you to delete an emoji. -::: danger +**Warning:** When you delete a custom emoji, old notes that contain it will still have the text name of the emoji in it. The emoji will no longer be rendered correctly. -::: Note that remote emoji can not be edited or deleted. diff --git a/docs/install-docker.md b/docs/install-docker.md index 9444c5ace..435b09bd6 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -38,10 +38,11 @@ cp .config/docker_example.env .config/docker.env Edit `default.yml` and `docker.env` according to the instructions in the files. You will need to set the database host to `db` and Redis host to `redis` in order to use the internal container network for these services. - Edit `docker-compose.yml` if necessary. (e.g. if you want to change the port). If you are using SELinux (eg. you're on Fedora or a RHEL derivative), you'll want to add the `Z` mount flag to the volume mounts to allow the containers to access the contents of those volumes. +Also check out the [Configure Foundkey](./install.md#configure-foundkey) section in the ordinary installation instructions. + ## Build and initialize The following command will build FoundKey and initialize the database. This will take some time. diff --git a/docs/install.md b/docs/install.md index 055c5a590..0129ed95e 100644 --- a/docs/install.md +++ b/docs/install.md @@ -78,6 +78,21 @@ There are instructions for setting up [nginx](./nginx.md) for this purpose. ### Changing the default Reaction You can change the default reaction that is used when an ActivityPub "Like" is received from '๐Ÿ‘' to 'โญ' by changing the boolean value `meta.useStarForReactionFallback` in the databse respectively. +### Environment variables +There are some behaviour changes which can be accomplished using environment variables. + +|variable name|meaning| +|---|---| +|`FK_ONLY_QUEUE`|If set, only the queue processing will be run. The frontend will not be available. Cannot be combined with `FK_ONLY_SERVER` or `FK_DISABLE_CLUSTERING`.| +|`FK_ONLY_SERVER`|If set, only the frontend will be run. Queues will not be processed. Cannot be combined with `FK_ONLY_QUEUE` or `FK_DISABLE_CLUSTERING`.| +|`FK_NO_DAEMONS`|If set, the server statistics and queue statistics will not be run.| +|`FK_DISABLE_CLUSTERING`|If set, all work will be done in a single thread instead of different threads for frontend and queue. (not recommended)| +|`FK_WITH_LOG_TIME`|If set, a timestamp will be appended to all log messages.| +|`FK_SLOW`|If set, all requests will be delayed by 3s. (not recommended, useful for testing)| +|`FK_LOG_LEVEL`|Sets the log level. Messages below the set log level will be suppressed. Available log levels are `quiet` (suppress all), `error`, `warning`, `success`, `info`, `debug`.| + +If the `NODE_ENV` environment variable is set to `testing`, then the flags `FK_DISABLE_CLUSTERING` and `FK_NO_DAEMONS` will always be set, and the log level will always be `quiet`. + ## Build FoundKey Build foundkey with the following: @@ -119,7 +134,7 @@ Run `NODE_ENV=production npm start` to launch FoundKey manually. To stop the ser ### Launch with systemd -Run `systemctl --edit --full --force foundkey.service`, and paste the following: +Run `systemctl edit --full --force foundkey.service`, and paste the following: ```ini [Unit] @@ -129,7 +144,7 @@ Description=FoundKey daemon Type=simple User=foundkey ExecStart=/usr/bin/npm start -WorkingDirectory=/home/foundkey/foundkey +WorkingDirectory=/home/foundkey/FoundKey Environment="NODE_ENV=production" TimeoutSec=60 StandardOutput=syslog @@ -163,7 +178,7 @@ command_args="start" command_user="foundkey" supervisor="supervise-daemon" -supervise_daemon_args=" -d /home/foundkey/foundkey -e NODE_ENV=\"production\"" +supervise_daemon_args=" -d /home/foundkey/FoundKey -e NODE_ENV=\"production\"" pidfile="/run/${RC_SVCNAME}.pid" diff --git a/docs/migrating.md b/docs/migrating.md index 6308f667b..a44742508 100644 --- a/docs/migrating.md +++ b/docs/migrating.md @@ -20,7 +20,7 @@ cd packages/backend LINE_NUM="$(npx typeorm migration:show -d ormconfig.js | grep -n nsfwDetection1655368940105 | cut -d ':' -f 1)" NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | nl)" -for i in $(seq 1 $NUM_MIGRAIONS); do +for i in $(seq 1 $NUM_MIGRATIONS); do npx typeorm migration:revert -d ormconfig.js done ``` diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 2556bdc5e..cf486fb37 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -32,9 +32,6 @@ signup: "ุฃู†ุดุฆ ุญุณุงุจู‹ุง" save: "ุญูุธ" users: "ุงู„ู…ุณุชุฎุฏู…ูˆู†" addUser: "ุงุถุงูุฉ ู…ุณุชุฎุฏู…" -favorite: "ุฃุถูู‡ุง ู„ู„ู…ูุถู„ุฉ" -favorites: "ุงู„ู…ูุถู„ุงุช" -unfavorite: "ุฅุฒุงู„ุฉ ู…ู† ุงู„ู…ูุถู„ุฉ" pin: "ุฏุจู‘ุณู‡ุง ุนู„ู‰ ุงู„ุตูุญุฉ ุงู„ุดุฎุตูŠุฉ" unpin: "ุฃู„ุบ ุชุฏุจูŠุณู‡ุง ู…ู† ู…ู„ููƒ ุงู„ุดุฎุตูŠ" copyContent: "ุงู†ุณุฎ ุงู„ู…ุญุชูˆู‰" @@ -581,7 +578,6 @@ loadRawImages: "ุญู…ู‘ู„ ุงู„ุตูˆุฑ ุงู„ุฃุตู„ูŠุฉ ุจุฏู„ู‹ุง ู…ู† ุงู„ู…ุตุบุฑ disableShowingAnimatedImages: "ู„ุง ุชุดุบู‘ู„ ุงู„ุตูˆุฑ ุงู„ู…ุชุญุฑูƒุฉ" verificationEmailSent: "ุฃูุฑุณู„ ุจุฑูŠุฏ ุงู„ุชุญู‚ู‚. ุฃู†ู‚ุฑ ุนู„ู‰ ุงู„ุฑุงุจุท ุงู„ู…ุถู…ู† ู„ุฅูƒู…ุงู„ ุงู„ุชุญู‚ู‚." emailVerified: "ุชูุญู‚ู‘ู‚ ู…ู† ุจุฑูŠุฏูƒ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ" -noteFavoritesCount: "ุนุฏุฏ ุงู„ู…ู„ุงุญุธุงุช ุงู„ู…ูุถู„ุฉ" pageLikesCount: "ุนุฏุฏ ุงู„ุตูุญุงุช ุงู„ุชูŠ ุฃุนุฌุจุช ุจู‡ุง" pageLikedCount: "ุนุฏุฏ ุตูุญุงุชูƒ ุงู„ู…ูุนุฌุจ ุจู‡ุง" contact: "ุงู„ุชูˆุงุตู„" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 6e160296b..4a84a044f 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -32,9 +32,6 @@ signup: "เฆจเฆฟเฆฌเฆจเงเฆงเฆจ เฆ•เฆฐเงเฆจ" save: "เฆธเฆ‚เฆฐเฆ•เงเฆทเฆฃ" users: "เฆฌเงเฆฏเฆฌเฆนเฆพเฆฐเฆ•เฆพเฆฐเง€เฆ—เฆฃ" addUser: "เฆฌเงเฆฏเฆฌเฆนเฆพเฆฐเฆ•เฆพเฆฐเง€ เฆฏเง‹เฆ— เฆ•เฆฐเงเฆจ" -favorite: "เฆชเฆ›เฆจเงเฆฆ" -favorites: "เฆชเฆ›เฆจเงเฆฆเฆ—เงเฆฒเฆฟ" -unfavorite: "เฆชเฆ›เฆจเงเฆฆ เฆจเฆพ" pin: "เฆชเฆฟเฆจ เฆ•เฆฐเฆพ" unpin: "เฆชเฆฟเฆจ เฆธเฆฐเฆพเฆจ" copyContent: "เฆฌเฆฟเฆทเงŸเฆฌเฆธเงเฆคเง เฆ•เฆชเฆฟ เฆ•เฆฐเงเฆจ" @@ -633,7 +630,6 @@ disableShowingAnimatedImages: "เฆ…เงเฆฏเฆพเฆจเฆฟเฆฎเง‡เฆŸเง‡เฆก เฆšเฆฟเฆคเงเฆฐ verificationEmailSent: "เฆจเฆฟเฆถเงเฆšเฆฟเฆคเฆ•เฆฐเฆฃ เฆ‡เฆฎเง‡เฆฒ เฆชเฆพเฆ เฆพเฆจเง‹ เฆนเงŸเง‡เฆ›เง‡เฅค เฆธเง‡เฆŸเฆ†เฆช เฆธเฆฎเงเฆชเง‚เฆฐเงเฆฃ เฆ•เฆฐเฆคเง‡ เฆ‡เฆฎเง‡เฆฒ เฆเฆฐ\ \ เฆฒเฆฟเฆ™เงเฆ• เฆ…เฆจเงเฆธเฆฐเฆฃ เฆ•เฆฐเงเฆจเฅค" emailVerified: "เฆ‡เฆฎเง‡เฆ‡เฆฒ เฆจเฆฟเฆถเงเฆšเฆฟเฆค เฆ•เฆฐเฆพ เฆนเงŸเง‡เฆ›เง‡" -noteFavoritesCount: "เฆชเฆ›เฆจเงเฆฆ เฆ•เฆฐเฆพ เฆจเง‹เฆŸเง‡เฆฐ เฆธเฆ‚เฆ–เงเฆฏเฆพ" pageLikesCount: "เฆชเง‡เฆœ เฆฒเฆพเฆ‡เฆ• เฆ•เฆฐเง‡เฆ›เง‡เฆจ" pageLikedCount: "เฆชเง‡เฆœ เฆฒเฆพเฆ‡เฆ• เฆชเง‡เงŸเง‡เฆ›เง‡เฆจ" contact: "เฆชเฆฐเฆฟเฆšเฆฟเฆคเฆฟ เฆธเฆฎเง‚เฆน" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index e2d229bbb..aec2a9ec6 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -32,9 +32,6 @@ signup: "Registrar-se" save: "Desar" users: "Usuaris" addUser: "Afegir un usuari" -favorite: "Afegir a preferits" -favorites: "Favorits" -unfavorite: "Eliminar dels preferits" pin: "Fixar al perfil" unpin: "Para de fixar del perfil" copyContent: "Copiar el contingut" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 96c3963be..bc31cf821 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -32,9 +32,6 @@ signup: "Registrace" save: "Uloลพit" users: "Uลพivatelรฉ" addUser: "Pล™idat uลพivatele" -favorite: "Oblรญbenรฉ" -favorites: "Oblรญbenรฉ" -unfavorite: "Odebrat z oblรญzenรฝch" pin: "Pล™ipnout" unpin: "Odepnout" copyContent: "Zkopรญrovat obsah" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 86eac682d..fb39f8141 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -33,9 +33,6 @@ signup: "Registrieren" save: "Speichern" users: "Benutzer" addUser: "Benutzer hinzufรผgen" -favorite: "Zu Favoriten hinzufรผgen" -favorites: "Favoriten" -unfavorite: "Aus Favoriten entfernen" pin: "An dein Profil anheften" unpin: "Von deinem Profil lรถsen" copyContent: "Inhalt kopieren" @@ -654,7 +651,6 @@ disableShowingAnimatedImages: "Animierte Bilder nicht abspielen" verificationEmailSent: "Eine Bestรคtigungsmail wurde an deine Email-Adresse versendet.\ \ Besuche den dort enthaltenen Link, um die Verifizierung abzuschlieรŸen." emailVerified: "Email-Adresse bestรคtigt" -noteFavoritesCount: "Anzahl an als Favorit markierter Notizen" pageLikesCount: "Anzahl an als \"Gefรคllt mir\" markierter Seiten" pageLikedCount: "Anzahl erhaltener \"Gefรคllt mir\" auf Seiten" contact: "Kontakt" @@ -820,6 +816,7 @@ _ffVisibility: public: "ร–ffentlich" followers: "Nur fรผr Follower sichtbar" private: "Privat" + nobody: Niemand (auch nicht du) _signup: almostThere: "Fast geschafft" emailAddressInfo: "Bitte gib deine Email-Adresse ein. Sie wird nicht รถffentlich\ @@ -1381,7 +1378,7 @@ addTag: Schlagwรถrter hinzufรผgen removeTag: Schlagwรถrter entfernen exportAll: Alle exportieren exportSelected: Gewรคhlte exportieren -federateBlocks: Andere Instanzen mitteilen, wenn ich jemanden blockiere +federateBlocks: Anderen Instanzen mitteilen, wenn ich jemanden blockiere selectMode: Auswรคhlen selectAll: Alle auswรคhlen renoteUnmute: Renotes zeigen diff --git a/locales/en-US.yml b/locales/en-US.yml index fa0d9d28e..947ed7bfb 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -32,9 +32,6 @@ signup: "Sign Up" save: "Save" users: "Users" addUser: "Add a user" -favorite: "Add to favorites" -favorites: "Favorites" -unfavorite: "Remove from favorites" pin: "Pin to profile" unpin: "Unpin from profile" copyContent: "Copy contents" @@ -648,7 +645,6 @@ disableShowingAnimatedImages: "Don't play animated images" verificationEmailSent: "A verification email has been sent. Please follow the included\ \ link to complete verification." emailVerified: "Email has been verified" -noteFavoritesCount: "Number of favorite notes" pageLikesCount: "Number of liked Pages" pageLikedCount: "Number of received Page likes" contact: "Contact" @@ -843,6 +839,7 @@ _ffVisibility: public: "Public" followers: "Visible to followers only" private: "Private" + nobody: "Nobody (not even you)" _signup: almostThere: "Almost there" emailAddressInfo: "Please enter your email address. It will not be made public." diff --git a/locales/es-ES.yml b/locales/es-ES.yml index fbfcd9352..b8e4014ed 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -33,9 +33,6 @@ signup: "Registrarse" save: "Guardar" users: "Usuarios" addUser: "Agregar usuario" -favorite: "Favorito" -favorites: "Favoritos" -unfavorite: "Quitar de favoritos" pin: "Fijar" unpin: "Desfijar" copyContent: "Copiar contenido" @@ -638,7 +635,6 @@ verificationEmailSent: "Se le ha enviado un correo electrรณnico de confirmaciรณn \ favor, acceda al enlace proporcionado en el correo electrรณnico para completar\ \ la configuraciรณn." emailVerified: "Su direcciรณn de correo electrรณnico ha sido verificada." -noteFavoritesCount: "Nรบmero de notas favoritas" pageLikesCount: "Nรบmero de favoritos en la pรกgina" pageLikedCount: "Nรบmero de favoritos de su pรกgina" contact: "Contacto" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 0cea0bfcb..d6df6af2c 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -34,9 +34,6 @@ signup: "Sโ€™inscrire" save: "Enregistrer" users: "Utilisateurยทriceยทs" addUser: "Ajouter unยทe utilisateurยทrice" -favorite: "Ajouter aux favoris" -favorites: "Favoris" -unfavorite: "Retirer des favoris" pin: "ร‰pingler sur le profil" unpin: "Dรฉsรฉpingler" copyContent: "Copier le contenu" @@ -647,7 +644,6 @@ disableShowingAnimatedImages: "Dรฉsactiver l'animation des images" verificationEmailSent: "Un e-mail de vรฉrification a รฉtรฉ envoyรฉ. Veuillez accรฉder au\ \ lien pour complรฉter la vรฉrification." emailVerified: "Votre adresse e-mail a รฉtรฉ vรฉrifiรฉe" -noteFavoritesCount: "Nombre de notes dans les favoris" pageLikesCount: "Nombre de pages aimรฉes" pageLikedCount: "Nombre de vos pages aimรฉes" contact: "Contact" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 2114dc99b..cc188e570 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -32,9 +32,6 @@ signup: "Daftar" save: "Simpan" users: "Pengguna" addUser: "Tambah pengguna" -favorite: "Favorit" -favorites: "Favorit" -unfavorite: "Hapus favorit" pin: "Sematkan ke profil" unpin: "Lepas sematan dari profil" copyContent: "Salin konten" @@ -639,7 +636,6 @@ disableShowingAnimatedImages: "Jangan mainkan gambar bergerak" verificationEmailSent: "Surel verifikasi telah dikirimkan. Mohon akses tautan yang\ \ telah disertakan untuk menyelesaikan verifikasi." emailVerified: "Surel telah diverifikasi" -noteFavoritesCount: "Jumlah catatan yang difavoritkan" pageLikesCount: "Jumlah suka yang diterima Halaman" pageLikedCount: "Jumlah Halaman yang disukai" contact: "Kontak" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 1f02dddb7..05a5cee16 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -33,9 +33,6 @@ signup: "Iscriviti" save: "Salva" users: "Utente" addUser: "Aggiungi utente" -favorite: "Preferiti" -favorites: "Preferiti" -unfavorite: "Rimuovi nota dai preferiti" pin: "Fissa sul profilo" unpin: "Non fissare sul profilo" copyContent: "Copia il contenuto" @@ -625,7 +622,6 @@ disableShowingAnimatedImages: "Disabilita le immagini animate" verificationEmailSent: "Una mail di verifica รจ stata inviata. Si prega di accedere\ \ al collegamento per compiere la verifica." emailVerified: "Il tuo indirizzo email รจ stato verificato" -noteFavoritesCount: "Conteggio note tra i preferiti" pageLikesCount: "Numero di pagine che ti piacciono" pageLikedCount: "Numero delle tue pagine che hanno ricevuto \"Mi piace\"" contact: "Contatti" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 6ff9e5d10..c10755320 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -31,9 +31,6 @@ signup: "ๆ–ฐ่ฆ็™ป้Œฒ" save: "ไฟๅญ˜" users: "ใƒฆใƒผใ‚ถใƒผ" addUser: "ใƒฆใƒผใ‚ถใƒผใ‚’่ฟฝๅŠ " -favorite: "ใŠๆฐ—ใซๅ…ฅใ‚Š" -favorites: "ใŠๆฐ—ใซๅ…ฅใ‚Š" -unfavorite: "ใŠๆฐ—ใซๅ…ฅใ‚Š่งฃ้™ค" pin: "ใƒ”ใƒณ็•™ใ‚" unpin: "ใƒ”ใƒณ็•™ใ‚่งฃ้™ค" copyContent: "ๅ†…ๅฎนใ‚’ใ‚ณใƒ”ใƒผ" @@ -587,7 +584,6 @@ loadRawImages: "ๆทปไป˜็”ปๅƒใฎใ‚ตใƒ ใƒใ‚คใƒซใ‚’ใ‚ชใƒชใ‚ธใƒŠใƒซ็”ป่ณชใซใ™ใ‚‹" disableShowingAnimatedImages: "ใ‚ขใƒ‹ใƒกใƒผใ‚ทใƒงใƒณ็”ปๅƒใ‚’ๅ†็”Ÿใ—ใชใ„" verificationEmailSent: "็ขบ่ชใฎใƒกใƒผใƒซใ‚’้€ไฟกใ—ใพใ—ใŸใ€‚ใƒกใƒผใƒซใซ่จ˜่ผ‰ใ•ใ‚ŒใŸใƒชใƒณใ‚ฏใซใ‚ขใ‚ฏใ‚ปใ‚นใ—ใฆใ€่จญๅฎšใ‚’ๅฎŒไบ†ใ—ใฆใใ ใ•ใ„ใ€‚" emailVerified: "ใƒกใƒผใƒซใ‚ขใƒ‰ใƒฌใ‚นใŒ็ขบ่ชใ•ใ‚Œใพใ—ใŸ" -noteFavoritesCount: "ใŠๆฐ—ใซๅ…ฅใ‚ŠใƒŽใƒผใƒˆใฎๆ•ฐ" pageLikesCount: "Pageใซใ„ใ„ใญใ—ใŸๆ•ฐ" pageLikedCount: "Pageใซใ„ใ„ใญใ•ใ‚ŒใŸๆ•ฐ" contact: "้€ฃ็ตกๅ…ˆ" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index cb78b4651..52d457a81 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -30,9 +30,6 @@ signup: "ๆ–ฐ่ฆ็™ป้Œฒ" save: "ไฟๅญ˜" users: "ใƒฆใƒผใ‚ถใƒผ" addUser: "ใƒฆใƒผใ‚ถใƒผใ‚’่ฟฝๅŠ ใ‚„" -favorite: "ใŠๆฐ—ใซๅ…ฅใ‚Š" -favorites: "ใŠๆฐ—ใซๅ…ฅใ‚Š" -unfavorite: "ใ‚„ใฃใฑๆฐ—ใซๅ…ฅใ‚‰ใ‚“" pin: "ใƒ”ใƒณ็•™ใ‚ใ—ใจใ" unpin: "ใ‚„ใฃใฑใƒ”ใƒณ็•™ใ‚ใ›ใ‚“" copyContent: "ๅ†…ๅฎนใ‚’ใ‚ณใƒ”ใƒผ" diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml index 6ae86687f..8c6cb1972 100644 --- a/locales/kn-IN.yml +++ b/locales/kn-IN.yml @@ -27,9 +27,6 @@ signup: "เฒจเณ‹เฒ‚เฒฆเฒฃเฒฟ" save: "เฒ‰เฒณเฒฟเฒธเฒฟ" users: "เฒฌเฒณเฒ•เณ†เฒฆเฒพเฒฐ" addUser: "เฒฌเฒณเฒ•เณ†เฒฆเฒพเฒฐเฒฐเฒจเณเฒจเณ เฒธเณ‡เฒฐเฒฟเฒธเฒฟ" -favorite: "เฒฎเณ†เฒšเณเฒšเฒฟเฒจ" -favorites: "เฒฎเณ†เฒšเณเฒšเฒฟเฒจเฒตเณเฒ—เฒณเณ" -unfavorite: "เฒฎเณ†เฒšเณเฒšเณเฒ—เณ† เฒ…เฒณเฒฟเฒธเณ" pin: "เฒชเณเฒฐเณŠเฒซเฒผเณˆเฒฒเฒฟเฒ—เณ† เฒ…เฒ‚เฒŸเฒฟเฒธเณ" unpin: "เฒชเณเฒฐเณŠเฒซเฒผเณˆเฒฒเฒฟเฒ‚เฒฆ เฒ…เฒ‚เฒŸเณเฒคเณ†เฒ—เณ†" copyContent: "เฒตเฒฟเฒทเฒฏเฒตเฒจเณเฒจเณ เฒจเฒ•เฒฒเฒฟเฒธเณ" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 69a2ad1af..e89035ed7 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1,7 +1,8 @@ ---- _lang_: "ํ•œ๊ตญ์–ด" headlineMisskey: "๋…ธํŠธ๋กœ ์—ฐ๊ฒฐ๋˜๋Š” ๋„คํŠธ์›Œํฌ" -introMisskey: "ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค! FoundKey ๋Š” ์˜คํ”ˆ ์†Œ์Šค ๋ถ„์‚ฐํ˜• ๋งˆ์ดํฌ๋กœ ๋ธ”๋กœ๊ทธ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค.\n\"๋…ธํŠธ\" ๋ฅผ ์ž‘์„ฑํ•ด์„œ, ์ง€๊ธˆ ์ผ์–ด๋‚˜๊ณ  ์žˆ๋Š” ์ผ์„ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜, ๋‹น์‹ ๋งŒ์˜ ์ด์•ผ๊ธฐ๋ฅผ ๋ชจ๋‘์—๊ฒŒ ๋ฐœ์‹ ํ•˜์„ธ์š”๐Ÿ“ก\n\"๋ฆฌ์•ก์…˜\" ๊ธฐ๋Šฅ์œผ๋กœ, ์นœ๊ตฌ์˜ ๋…ธํŠธ์— ์ด์•Œ๊ฐ™์ด ๋ฐ˜์‘์„ ์ถ”๊ฐ€ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค๐Ÿ‘\n์ƒˆ๋กœ์šด ์„ธ๊ณ„๋ฅผ ํƒํ—˜ํ•ด ๋ณด์„ธ์š”๐Ÿš€" +introMisskey: "ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค! FoundKey ๋Š” ์˜คํ”ˆ ์†Œ์Šค ๋ถ„์‚ฐํ˜• ๋งˆ์ดํฌ๋กœ ๋ธ”๋กœ๊ทธ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค.\n\"๋…ธํŠธ\" ๋ฅผ ์ž‘์„ฑํ•ด์„œ, ์ง€๊ธˆ ์ผ์–ด๋‚˜๊ณ \ + \ ์žˆ๋Š” ์ผ์„ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜, ๋‹น์‹ ๋งŒ์˜ ์ด์•ผ๊ธฐ๋ฅผ ๋ชจ๋‘์—๊ฒŒ ๋ฐœ์‹ ํ•˜์„ธ์š”\U0001F4E1\n\"๋ฆฌ์•ก์…˜\" ๊ธฐ๋Šฅ์œผ๋กœ, ์นœ๊ตฌ์˜ ๋…ธํŠธ์— ์ด์•Œ๊ฐ™์ด ๋ฐ˜์‘์„ ์ถ”๊ฐ€ํ• \ + \ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค\U0001F44D\n์ƒˆ๋กœ์šด ์„ธ๊ณ„๋ฅผ ํƒํ—˜ํ•ด ๋ณด์„ธ์š”\U0001F680" monthAndDay: "{month}์›” {day}์ผ" search: "๊ฒ€์ƒ‰" notifications: "์•Œ๋ฆผ" @@ -12,7 +13,6 @@ fetchingAsApObject: "์—ฐํ•ฉ์—์„œ ์กฐํšŒ ์ค‘" ok: "OK" gotIt: "์•Œ๊ฒ ์–ด์š”" cancel: "์ทจ์†Œ" -enterUsername: "์œ ์ €๋ช… ์ž…๋ ฅ" renotedBy: "{user}๋‹˜์ด Renote" noNotes: "๋…ธํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค" noNotifications: "ํ‘œ์‹œํ•  ์•Œ๋ฆผ์ด ์—†์Šต๋‹ˆ๋‹ค" @@ -28,16 +28,9 @@ login: "๋กœ๊ทธ์ธ" loggingIn: "๋กœ๊ทธ์ธ ์ค‘" logout: "๋กœ๊ทธ์•„์›ƒ" signup: "ํšŒ์› ๊ฐ€์ž…" -uploading: "์—…๋กœ๋“œ ์ค‘" save: "์ €์žฅ" users: "์œ ์ €" addUser: "์œ ์ € ์ถ”๊ฐ€" -favorite: "์ฆ๊ฒจ์ฐพ๊ธฐ" -favorites: "์ฆ๊ฒจ์ฐพ๊ธฐ" -unfavorite: "์ฆ๊ฒจ์ฐพ๊ธฐ์—์„œ ์ œ๊ฑฐ" -favorited: "์ฆ๊ฒจ์ฐพ๊ธฐ์— ๋“ฑ๋กํ–ˆ์Šต๋‹ˆ๋‹ค" -alreadyFavorited: "์ด๋ฏธ ์ฆ๊ฒจ์ฐพ๊ธฐ์— ๋“ฑ๋ก๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค" -cantFavorite: "์ฆ๊ฒจ์ฐพ๊ธฐ์— ๋“ฑ๋กํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค" pin: "ํ”„๋กœํ•„์— ๊ณ ์ •" unpin: "ํ”„๋กœํ•„์—์„œ ๊ณ ์ • ํ•ด์ œ" copyContent: "๋‚ด์šฉ ๋ณต์‚ฌ" @@ -48,7 +41,6 @@ deleteAndEditConfirm: "์ด ๋…ธํŠธ๋ฅผ ์‚ญ์ œํ•œ ๋’ค ๋‹ค์‹œ ํŽธ์ง‘ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ addToList: "๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€" sendMessage: "๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ" copyUsername: "์œ ์ €๋ช… ๋ณต์‚ฌ" -searchUser: "์‚ฌ์šฉ์ž ๊ฒ€์ƒ‰" reply: "๋‹ต๊ธ€" loadMore: "๋” ๋ณด๊ธฐ" showMore: "๋” ๋ณด๊ธฐ" @@ -68,7 +60,6 @@ unfollowConfirm: "{name}๋‹˜์„ ์–ธํŒ”๋กœ์šฐํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" exportRequested: "๋‚ด๋ณด๋‚ด๊ธฐ๋ฅผ ์š”์ฒญํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด ์ž‘์—…์€ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‚ด๋ณด๋‚ด๊ธฐ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด \"๋“œ๋ผ์ด๋ธŒ\"์— ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค." importRequested: "๊ฐ€์ ธ์˜ค๊ธฐ๋ฅผ ์š”์ฒญํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด ์ž‘์—…์—๋Š” ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." lists: "๋ฆฌ์ŠคํŠธ" -noLists: "๋ฆฌ์ŠคํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค" note: "๋…ธํŠธ" notes: "๋…ธํŠธ" following: "ํŒ”๋กœ์ž‰" @@ -80,7 +71,8 @@ error: "์˜ค๋ฅ˜" somethingHappened: "์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค" retry: "๋‹ค์‹œ ์‹œ๋„" pageLoadError: "ํŽ˜์ด์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค." -pageLoadErrorDescription: "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ๋˜๋Š” ๋ธŒ๋ผ์šฐ์ € ์บ์‹œ๋กœ ์ธํ•ด ๋ฐœ์ƒํ–ˆ์„ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค. ์บ์‹œ๋ฅผ ์‚ญ์ œํ•˜๊ฑฐ๋‚˜, ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”." +pageLoadErrorDescription: "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ๋˜๋Š” ๋ธŒ๋ผ์šฐ์ € ์บ์‹œ๋กœ ์ธํ•ด ๋ฐœ์ƒํ–ˆ์„ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค. ์บ์‹œ๋ฅผ ์‚ญ์ œํ•˜๊ฑฐ๋‚˜, ์ž ์‹œ ํ›„\ + \ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”." serverIsDead: "์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ์‘๋‹ต์ด ์—†์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”." youShouldUpgradeClient: "์ด ํŽ˜์ด์ง€๋ฅผ ํ‘œ์‹œํ•˜๋ ค๋ฉด ์ƒˆ๋กœ๊ณ ์นจํ•˜์—ฌ ์ƒˆ๋กœ์šด ๋ฒ„์ „์˜ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์ด์šฉํ•ด ์ฃผ์‹ญ์‹œ์˜ค." enterListName: "๋ฆฌ์ŠคํŠธ ์ด๋ฆ„์„ ์ž…๋ ฅ" @@ -92,21 +84,15 @@ followRequest: "ํŒ”๋กœ์šฐ ์š”์ฒญ" followRequests: "ํŒ”๋กœ์šฐ ์š”์ฒญ" unfollow: "ํŒ”๋กœ์šฐ ํ•ด์ œ" followRequestPending: "ํŒ”๋กœ์šฐ ํ—ˆ๊ฐ€ ๋Œ€๊ธฐ์ค‘" -enterEmoji: "์ด๋ชจ์ง€ ์ž…๋ ฅ" renote: "Renote" unrenote: "Renote ์ทจ์†Œ" -renoted: "Renote ํ•˜์˜€์Šต๋‹ˆ๋‹ค" -cantRenote: "์ด ๊ฒŒ์‹œ๋ฌผ์€ Renoteํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." -cantReRenote: "Renote๋ฅผ Renoteํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." quote: "์ธ์šฉ" pinnedNote: "๊ณ ์ •ํ•ด๋†“์€ ๋…ธํŠธ" -pinned: "ํ”„๋กœํ•„์— ๊ณ ์ •" you: "๋‹น์‹ " clickToShow: "ํด๋ฆญํ•˜์—ฌ ๋ณด๊ธฐ" sensitive: "์—ด๋žŒ์ฃผ์˜" add: "์ถ”๊ฐ€" reaction: "๋ฆฌ์•ก์…˜" -reactionSetting: "์„ ํƒ๊ธฐ์— ํ‘œ์‹œํ•  ๋ฆฌ์•ก์…˜" reactionSettingDescription2: "๋Œ์–ด์„œ ์ˆœ์„œ ๋ณ€๊ฒฝ, ํด๋ฆญํ•ด์„œ ์‚ญ์ œ, ๏ผ‹๋ฅผ ๋ˆŒ๋Ÿฌ์„œ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." attachCancel: "์ฒจ๋ถ€ ์ทจ์†Œ" markAsSensitive: "์—ด๋žŒ์ฃผ์˜๋กœ ์„ค์ •" @@ -130,14 +116,13 @@ editWidgetsExit: "ํŽธ์ง‘ ์ข…๋ฃŒ" customEmojis: "์ปค์Šคํ…€ ์ด๋ชจ์ง€" emoji: "์ด๋ชจ์ง€" emojis: "์ด๋ชจ์ง€" -emojiName: "์ด๋ชจ์ง€ ์ด๋ฆ„" -emojiUrl: "์ด๋ชจ์ง€ URL" addEmoji: "์ด๋ชจ์ง€ ์ถ”๊ฐ€" -settingGuide: "์ถ”์ฒœ ์„ค์ •" cacheRemoteFiles: "๋ฆฌ๋ชจํŠธ ํŒŒ์ผ์„ ์บ์‹œ" -cacheRemoteFilesDescription: "์ด ์„ค์ •์„ ํ•ด์ง€ํ•˜๋ฉด ๋ฆฌ๋ชจํŠธ ํŒŒ์ผ์„ ์บ์‹œํ•˜์ง€ ์•Š๊ณ  ํ•ด๋‹น ํŒŒ์ผ์„ ์ง์ ‘ ๋งํฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ์— ๋”ฐ๋ผ ์„œ๋ฒ„์˜ ์ €์žฅ ๊ณต๊ฐ„์„ ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ธ๋„ค์ผ์ด ์ƒ์„ฑ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ํ†ต์‹ ๋Ÿ‰์ด ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค." +cacheRemoteFilesDescription: "์ด ์„ค์ •์„ ํ•ด์ง€ํ•˜๋ฉด ๋ฆฌ๋ชจํŠธ ํŒŒ์ผ์„ ์บ์‹œํ•˜์ง€ ์•Š๊ณ  ํ•ด๋‹น ํŒŒ์ผ์„ ์ง์ ‘ ๋งํฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ์— ๋”ฐ๋ผ\ + \ ์„œ๋ฒ„์˜ ์ €์žฅ ๊ณต๊ฐ„์„ ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ธ๋„ค์ผ์ด ์ƒ์„ฑ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ํ†ต์‹ ๋Ÿ‰์ด ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค." flagAsBot: "๋‚˜๋Š” ๋ด‡์ž…๋‹ˆ๋‹ค" -flagAsBotDescription: "์ด ๊ณ„์ •์„ ์ž๋™ํ™”๋œ ์ˆ˜๋‹จ์œผ๋กœ ์šด์šฉํ•  ๊ฒฝ์šฐ์— ํ™œ์„ฑํ™”ํ•ด ์ฃผ์„ธ์š”. ์ด ํ”Œ๋ž˜๊ทธ๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ฉด, ๋‹ค๋ฅธ ๋ด‡์ด ์ด๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ๋ด‡ ๋ผ๋ฆฌ์˜ ๋ฌดํ•œ ์—ฐ์‡„ ๋ฐ˜์‘์„ ํšŒํ”ผํ•˜๊ฑฐ๋‚˜, ์ด ๊ณ„์ •์˜ ์‹œ์Šคํ…œ ์ƒ์—์„œ์˜ ์ทจ๊ธ‰์ด Bot ์šด์˜์— ์ตœ์ ํ™”๋˜๋Š” ๋“ฑ์˜ ๋ณ€ํ™”๊ฐ€ ์ƒ๊น๋‹ˆ๋‹ค." +flagAsBotDescription: "์ด ๊ณ„์ •์„ ์ž๋™ํ™”๋œ ์ˆ˜๋‹จ์œผ๋กœ ์šด์šฉํ•  ๊ฒฝ์šฐ์— ํ™œ์„ฑํ™”ํ•ด ์ฃผ์„ธ์š”. ์ด ํ”Œ๋ž˜๊ทธ๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ฉด, ๋‹ค๋ฅธ ๋ด‡์ด ์ด๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ\ + \ ๋ด‡ ๋ผ๋ฆฌ์˜ ๋ฌดํ•œ ์—ฐ์‡„ ๋ฐ˜์‘์„ ํšŒํ”ผํ•˜๊ฑฐ๋‚˜, ์ด ๊ณ„์ •์˜ ์‹œ์Šคํ…œ ์ƒ์—์„œ์˜ ์ทจ๊ธ‰์ด Bot ์šด์˜์— ์ตœ์ ํ™”๋˜๋Š” ๋“ฑ์˜ ๋ณ€ํ™”๊ฐ€ ์ƒ๊น๋‹ˆ๋‹ค." flagAsCat: "๋‚˜๋Š” ๊ณ ์–‘์ด๋‹ค๋ƒฅ" flagAsCatDescription: "์ด ๊ณ„์ •์ด ๊ณ ์–‘์ด๋ผ๋ฉด ํ™œ์„ฑํ™” ํ•ด์ฃผ์„ธ์š”." flagShowTimelineReplies: "ํƒ€์ž„๋ผ์ธ์— ๋…ธํŠธ์˜ ๋‹ต๊ธ€์„ ํ‘œ์‹œํ•˜๊ธฐ" @@ -147,40 +132,32 @@ addAccount: "๊ณ„์ • ์ถ”๊ฐ€" loginFailed: "๋กœ๊ทธ์ธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค" showOnRemote: "๋ฆฌ๋ชจํŠธ์—์„œ ๋ณด๊ธฐ" general: "์ผ๋ฐ˜" -wallpaper: "๋ฐฐ๊ฒฝ" setWallpaper: "๋ฐฐ๊ฒฝํ™”๋ฉด ์„ค์ •" removeWallpaper: "๋ฐฐ๊ฒฝ ์ œ๊ฑฐ" -searchWith: "๊ฒ€์ƒ‰: {q}" youHaveNoLists: "๋ฆฌ์ŠคํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค" followConfirm: "{name}๋‹˜์„ ํŒ”๋กœ์šฐ ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" proxyAccount: "ํ”„๋ก์‹œ ๊ณ„์ •" -proxyAccountDescription: "ํ”„๋ก์‹œ ๊ณ„์ •์€ ํŠน์ • ์กฐ๊ฑด ํ•˜์—์„œ ์œ ์ €์˜ ๋ฆฌ๋ชจํŠธ ํŒ”๋กœ์šฐ๋ฅผ ๋Œ€ํ–‰ํ•˜๋Š” ๊ณ„์ •์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด, ์œ ์ €๊ฐ€ ๋ฆฌ๋ชจํŠธ ์œ ์ €๋ฅผ ๋ฆฌ์ŠคํŠธ์— ๋„ฃ์—ˆ์„ ๋•Œ, ๋ฆฌ์ŠคํŠธ์— ๋“ค์–ด๊ฐ„ ์œ ์ €๋ฅผ ์•„๋ฌด๋„ ํŒ”๋กœ์šฐํ•œ ์ ์ด ์—†๋‹ค๋ฉด ์•กํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ์ธ์Šคํ„ด์Šค๋กœ ๋ฐฐ๋‹ฌ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, ๋Œ€์‹  ํ”„๋ก์‹œ ๊ณ„์ •์ด ํ•ด๋‹น ์œ ์ €๋ฅผ ํŒ”๋กœ์šฐํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค." +proxyAccountDescription: "ํ”„๋ก์‹œ ๊ณ„์ •์€ ํŠน์ • ์กฐ๊ฑด ํ•˜์—์„œ ์œ ์ €์˜ ๋ฆฌ๋ชจํŠธ ํŒ”๋กœ์šฐ๋ฅผ ๋Œ€ํ–‰ํ•˜๋Š” ๊ณ„์ •์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด, ์œ ์ €๊ฐ€ ๋ฆฌ๋ชจํŠธ\ + \ ์œ ์ €๋ฅผ ๋ฆฌ์ŠคํŠธ์— ๋„ฃ์—ˆ์„ ๋•Œ, ๋ฆฌ์ŠคํŠธ์— ๋“ค์–ด๊ฐ„ ์œ ์ €๋ฅผ ์•„๋ฌด๋„ ํŒ”๋กœ์šฐํ•œ ์ ์ด ์—†๋‹ค๋ฉด ์•กํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ์ธ์Šคํ„ด์Šค๋กœ ๋ฐฐ๋‹ฌ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, ๋Œ€์‹  ํ”„๋ก์‹œ ๊ณ„์ •์ด\ + \ ํ•ด๋‹น ์œ ์ €๋ฅผ ํŒ”๋กœ์šฐํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค." host: "ํ˜ธ์ŠคํŠธ" selectUser: "์œ ์ € ์„ ํƒ" recipient: "์ˆ˜์‹ ์ธ" annotation: "๋‚ด์šฉ์— ๋Œ€ํ•œ ์ฃผ์„" federation: "์—ฐํ•ฉ" -instances: "์ธ์Šคํ„ด์Šค" registeredAt: "๋“ฑ๋ก ๋‚ ์งœ" latestRequestSentAt: "๋งˆ์ง€๋ง‰์œผ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ธ ์‹œ๊ฐ„" latestRequestReceivedAt: "๋งˆ์ง€๋ง‰์œผ๋กœ ์š”์ฒญ์„ ๋ฐ›์€ ์‹œ๊ฐ„" latestStatus: "๋งˆ์ง€๋ง‰ ์ƒํƒœ" -storageUsage: "์Šคํ† ๋ฆฌ์ง€ ์‚ฌ์šฉ๋Ÿ‰" charts: "์ฐจํŠธ" perHour: "1์‹œ๊ฐ„๋งˆ๋‹ค" perDay: "1์ผ๋งˆ๋‹ค" stopActivityDelivery: "์•กํ‹ฐ๋น„ํ‹ฐ ๋ณด๋‚ด์ง€ ์•Š๊ธฐ" blockThisInstance: "์ด ์ธ์Šคํ„ด์Šค๋ฅผ ์ฐจ๋‹จ" -operations: "์ž‘์—…" software: "์†Œํ”„ํŠธ์›จ์–ด" version: "๋ฒ„์ „" -metadata: "๋ฉ”ํƒ€๋ฐ์ดํ„ฐ" withNFiles: "{n}๊ฐœ์˜ ํŒŒ์ผ" -monitor: "๋ชจ๋‹ˆํ„ฐ" jobQueue: "์ž‘์—… ๋Œ€๊ธฐ์—ด" -cpuAndMemory: "CPU์™€ ๋ฉ”๋ชจ๋ฆฌ" -network: "๋„คํŠธ์›Œํฌ" -disk: "๋””์Šคํฌ" instanceInfo: "์ธ์Šคํ„ด์Šค ์ •๋ณด" statistics: "ํ†ต๊ณ„" clearQueue: "๋Œ€๊ธฐ์—ด ๋น„์šฐ๊ธฐ" @@ -189,7 +166,8 @@ clearQueueConfirmText: "๋Œ€๊ธฐ์—ด์— ๋‚จ์•„ ์žˆ๋Š” ๋…ธํŠธ๋Š” ๋”์ด์ƒ ์—ฐํ•ฉ๋˜ clearCachedFiles: "์บ์‹œ ๋น„์šฐ๊ธฐ" clearCachedFilesConfirm: "์บ์‹œ๋œ ๋ฆฌ๋ชจํŠธ ํŒŒ์ผ์„ ๋ชจ๋‘ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" blockedInstances: "์ฐจ๋‹จ๋œ ์ธ์Šคํ„ด์Šค" -blockedInstancesDescription: "์ฐจ๋‹จํ•˜๋ ค๋Š” ์ธ์Šคํ„ด์Šค์˜ ํ˜ธ์ŠคํŠธ ์ด๋ฆ„์„ ์ค„๋ฐ”๊ฟˆ์œผ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์ฐจ๋‹จ๋œ ์ธ์Šคํ„ด์Šค๋Š” ์ด ์ธ์Šคํ„ด์Šค์™€ ํ†ต์‹ ํ•  ์ˆ˜ ์—†๊ฒŒ ๋ฉ๋‹ˆ๋‹ค." +blockedInstancesDescription: "์ฐจ๋‹จํ•˜๋ ค๋Š” ์ธ์Šคํ„ด์Šค์˜ ํ˜ธ์ŠคํŠธ ์ด๋ฆ„์„ ์ค„๋ฐ”๊ฟˆ์œผ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์ฐจ๋‹จ๋œ ์ธ์Šคํ„ด์Šค๋Š” ์ด ์ธ์Šคํ„ด์Šค์™€\ + \ ํ†ต์‹ ํ•  ์ˆ˜ ์—†๊ฒŒ ๋ฉ๋‹ˆ๋‹ค." muteAndBlock: "๋ฎคํŠธ ๋ฐ ์ฐจ๋‹จ" mutedUsers: "๋ฎคํŠธํ•œ ์œ ์ €" blockedUsers: "์ฐจ๋‹จํ•œ ์œ ์ €" @@ -211,9 +189,6 @@ all: "์ „์ฒด" subscribing: "๊ตฌ๋… ์ค‘" publishing: "๋ฐฐํฌ ์ค‘" notResponding: "์‘๋‹ต ์—†์Œ" -instanceFollowing: "์ธ์Šคํ„ด์Šค์˜ ํŒ”๋กœ์ž‰" -instanceFollowers: "์ธ์Šคํ„ด์Šค์˜ ํŒ”๋กœ์›Œ" -instanceUsers: "์ธ์Šคํ„ด์Šค์˜ ์œ ์ €" changePassword: "๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ" security: "๋ณด์•ˆ" retypedNotMatch: "์ž…๋ ฅ์ด ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." @@ -229,7 +204,6 @@ lookup: "์กฐํšŒ" announcements: "๊ณต์ง€์‚ฌํ•ญ" imageUrl: "์ด๋ฏธ์ง€ URL" remove: "์‚ญ์ œ" -removed: "์‚ญ์ œํ•˜์˜€์Šต๋‹ˆ๋‹ค" removeAreYouSure: "\"{x}\" ์„(๋ฅผ) ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" deleteAreYouSure: "\"{x}\" ์„(๋ฅผ) ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" resetAreYouSure: "์ดˆ๊ธฐํ™” ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" @@ -237,7 +211,8 @@ saved: "์ €์žฅํ•˜์˜€์Šต๋‹ˆ๋‹ค" messaging: "๋Œ€ํ™”" upload: "์—…๋กœ๋“œ" keepOriginalUploading: "์›๋ณธ ์ด๋ฏธ์ง€๋ฅผ ์œ ์ง€" -keepOriginalUploadingDescription: "์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•  ๋•Œ์— ์›๋ณธ์„ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๋น„ํ™œ์„ฑํ™”ํ•˜๋ฉด ์—…๋กœ๋“œํ•  ๋•Œ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์›น ๊ณต๊ฐœ์šฉ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค." +keepOriginalUploadingDescription: "์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•  ๋•Œ์— ์›๋ณธ์„ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๋น„ํ™œ์„ฑํ™”ํ•˜๋ฉด ์—…๋กœ๋“œํ•  ๋•Œ ๋ธŒ๋ผ์šฐ์ €์—์„œ\ + \ ์›น ๊ณต๊ฐœ์šฉ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค." fromDrive: "๋“œ๋ผ์ด๋ธŒ์—์„œ" fromUrl: "URL๋กœ๋ถ€ํ„ฐ" uploadFromUrl: "URL ์—…๋กœ๋“œ" @@ -269,7 +244,6 @@ lightThemes: "๋ฐ์€ ํ…Œ๋งˆ" darkThemes: "์–ด๋‘์šด ํ…Œ๋งˆ" syncDeviceDarkMode: "๋””๋ฐ”์ด์Šค์˜ ๋‹คํฌ ๋ชจ๋“œ ์„ค์ •๊ณผ ๋™๊ธฐํ™”" drive: "๋“œ๋ผ์ด๋ธŒ" -fileName: "ํŒŒ์ผ๋ช…" selectFile: "ํŒŒ์ผ ์„ ํƒ" selectFiles: "ํŒŒ์ผ ์„ ํƒ" selectFolder: "ํด๋” ์„ ํƒ" @@ -280,8 +254,6 @@ createFolder: "ํด๋” ๋งŒ๋“ค๊ธฐ" renameFolder: "ํด๋” ์ด๋ฆ„ ๋ฐ”๊พธ๊ธฐ" deleteFolder: "ํด๋” ์‚ญ์ œ" addFile: "ํŒŒ์ผ ์ถ”๊ฐ€" -emptyDrive: "๋“œ๋ผ์ด๋ธŒ๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค" -emptyFolder: "ํด๋”๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค" unableToDelete: "์‚ญ์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" inputNewFileName: "๋ฐ”๊ฟ€ ํŒŒ์ผ๋ช…์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”" inputNewDescription: "์ƒˆ ์บก์…˜์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”" @@ -318,7 +290,6 @@ pages: "ํŽ˜์ด์ง€" enableLocalTimeline: "๋กœ์ปฌ ํƒ€์ž„๋ผ์ธ ํ™œ์„ฑํ™”" enableGlobalTimeline: "๊ธ€๋กœ๋ฒŒ ํƒ€์ž„๋ผ์ธ ํ™œ์„ฑํ™”" disablingTimelinesInfo: "ํŠน์ • ํƒ€์ž„๋ผ์ธ์„ ๋น„ํ™œ์„ฑํ™”ํ•˜๋”๋ผ๋„ ๊ด€๋ฆฌ์ž ๋ฐ ๋ชจ๋”๋ ˆ์ดํ„ฐ๋Š” ๊ณ„์† ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." -registration: "๋“ฑ๋ก" enableRegistration: "์‹ ๊ทœ ํšŒ์›๊ฐ€์ž…์„ ํ™œ์„ฑํ™”" invite: "์ดˆ๋Œ€" driveCapacityPerLocalAccount: "๋กœ์ปฌ ์œ ์ € ํ•œ ๋ช…๋‹น ๋“œ๋ผ์ด๋ธŒ ์šฉ๋Ÿ‰" @@ -327,22 +298,12 @@ inMb: "๋ฉ”๊ฐ€๋ฐ”์ดํŠธ ๋‹จ์œ„" iconUrl: "์•„์ด์ฝ˜ URL" bannerUrl: "๋ฐฐ๋„ˆ ์ด๋ฏธ์ง€ URL" backgroundImageUrl: "๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ URL" -basicInfo: "๊ธฐ๋ณธ ์ •๋ณด" pinnedUsers: "๊ณ ์ •๋œ ์œ ์ €" pinnedUsersDescription: "\"๋ฐœ๊ฒฌํ•˜๊ธฐ\" ํŽ˜์ด์ง€ ๋“ฑ์— ๊ณ ์ •ํ•˜๊ณ  ์‹ถ์€ ์œ ์ €๋ฅผ ํ•œ ์ค„์— ํ•œ ๋ช…์”ฉ ์ ์Šต๋‹ˆ๋‹ค." -pinnedPages: "๊ณ ์ •ํ•œ ํŽ˜์ด์ง€" -pinnedPagesDescription: "์ธ์Šคํ„ด์Šค์˜ ๋Œ€๋ฌธ์— ๊ณ ์ •ํ•˜๊ณ  ์‹ถ์€ ํŽ˜์ด์ง€์˜ ๊ฒฝ๋กœ๋ฅผ ํ•œ ์ค„์— ํ•˜๋‚˜์”ฉ ์ ์Šต๋‹ˆ๋‹ค." -pinnedClipId: "๊ณ ์ •ํ•  ํด๋ฆฝ์˜ ID" -pinnedNotes: "๊ณ ์ •ํ•ด๋†“์€ ๋…ธํŠธ" -hcaptcha: "hCaptcha" -enableHcaptcha: "hCaptcha ํ™œ์„ฑํ™”" hcaptchaSiteKey: "์‚ฌ์ดํŠธ ํ‚ค" hcaptchaSecretKey: "์‹œํฌ๋ฆฟ ํ‚ค" -recaptcha: "reCAPTCHA" -enableRecaptcha: "reCAPTCHA ํ™œ์„ฑํ™”" recaptchaSiteKey: "์‚ฌ์ดํŠธ ํ‚ค" recaptchaSecretKey: "์‹œํฌ๋ฆฟ ํ‚ค" -avoidMultiCaptchaConfirm: "์—ฌ๋Ÿฌ Captcha๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๊ฐ„์„ญ์ด ๋ฐœ์ƒํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ Captcha๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? ์ทจ์†Œ๋ฅผ ๋ˆŒ๋Ÿฌ ์—ฌ๋Ÿฌ Captcha๋ฅผ ํ™œ์„ฑํ™”ํ•œ ์ƒํƒœ๋กœ ๋‘๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค." antennas: "์•ˆํ…Œ๋‚˜" manageAntennas: "์•ˆํ…Œ๋‚˜ ๊ด€๋ฆฌ" name: "์ด๋ฆ„" @@ -352,7 +313,6 @@ antennaExcludeKeywords: "์ œ์™ธํ•  ํ‚ค์›Œ๋“œ" antennaKeywordsDescription: "๊ณต๋ฐฑ์œผ๋กœ ๊ตฌ๋ถ„ํ•˜๋Š” ๊ฒฝ์šฐ AND, ์ค„๋ฐ”๊ฟˆ์œผ๋กœ ๊ตฌ๋ถ„ํ•˜๋Š” ๊ฒฝ์šฐ OR๋กœ ์ง€์ •๋ฉ๋‹ˆ๋‹ค" notifyAntenna: "์ƒˆ๋กœ์šด ๋…ธํŠธ๋ฅผ ์•Œ๋ฆผ" withFileAntenna: "ํŒŒ์ผ์ด ์ฒจ๋ถ€๋œ ๋…ธํŠธ๋งŒ" -enableServiceworker: "ServiceWorker ์‚ฌ์šฉ" antennaUsersDescription: "์œ ์ €๋ช…์„ ํ•œ ์ค„์— ํ•œ ๋ช…์”ฉ ์ ์Šต๋‹ˆ๋‹ค" caseSensitive: "๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๊ตฌ๋ถ„" withReplies: "๋‹ต๊ธ€ ํฌํ•จ" @@ -367,11 +327,8 @@ popularUsers: "์ธ๊ธฐ ์œ ์ €" recentlyUpdatedUsers: "์ตœ๊ทผ ํ™œ๋™ํ•œ ์œ ์ €" recentlyRegisteredUsers: "์ตœ๊ทผ ๊ฐ€์ž…ํ•œ ์œ ์ €" recentlyDiscoveredUsers: "์ตœ๊ทผ ๋ฐœ๊ฒฌํ•œ ์œ ์ €" -exploreUsersCount: "{count}๋ช…์˜ ์œ ์ €๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค" -exploreFediverse: "์—ฐํ•ฉ์šฐ์ฃผ๋ฅผ ํƒ์ƒ‰" popularTags: "์ธ๊ธฐ ํƒœ๊ทธ" userList: "๋ฆฌ์ŠคํŠธ" -about: "์ •๋ณด" aboutMisskey: "FoundKey์— ๋Œ€ํ•˜์—ฌ" administrator: "๊ด€๋ฆฌ์ž" token: "ํ† ํฐ" @@ -391,7 +348,6 @@ share: "๊ณต์œ " notFound: "์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" notFoundDescription: "์ง€์ •ํ•œ URL์— ํ•ด๋‹นํ•˜๋Š” ํŽ˜์ด์ง€๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." uploadFolder: "๊ธฐ๋ณธ ์—…๋กœ๋“œ ์œ„์น˜" -cacheClear: "์บ์‹œ ์ง€์šฐ๊ธฐ" markAsReadAllNotifications: "๋ชจ๋“  ์•Œ๋ฆผ์„ ์ฝ์€ ์ƒํƒœ๋กœ ํ‘œ์‹œ" markAsReadAllUnreadNotes: "๋ชจ๋“  ๊ธ€์„ ์ฝ์€ ์ƒํƒœ๋กœ ํ‘œ์‹œ" markAsReadAllTalkMessages: "๋ชจ๋“  ๋Œ€ํ™”๋ฅผ ์ฝ์€ ์ƒํƒœ๋กœ ํ‘œ์‹œ" @@ -422,7 +378,6 @@ noMessagesYet: "์•„์ง ๋Œ€ํ™”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค" newMessageExists: "์ƒˆ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค" onlyOneFileCanBeAttached: "๋ฉ”์‹œ์ง€์— ์ฒจ๋ถ€ํ•  ์ˆ˜ ์žˆ๋Š” ํŒŒ์ผ์€ ํ•˜๋‚˜๊นŒ์ง€์ž…๋‹ˆ๋‹ค" signinRequired: "๋กœ๊ทธ์ธ ํ•ด์ฃผ์„ธ์š”" -invitations: "์ดˆ๋Œ€" invitationCode: "์ดˆ๋Œ€ ์ฝ”๋“œ" checking: "ํ™•์ธํ•˜๋Š” ์ค‘์ž…๋‹ˆ๋‹ค" available: "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" @@ -441,7 +396,6 @@ or: "ํ˜น์€" language: "์–ธ์–ด" uiLanguage: "UI ํ‘œ์‹œ ์–ธ์–ด" groupInvited: "๊ทธ๋ฃน์— ์ดˆ๋Œ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค" -aboutX: "{x}์— ๋Œ€ํ•˜์—ฌ" useOsNativeEmojis: "OS ๊ธฐ๋ณธ ์ด๋ชจ์ง€๋ฅผ ์‚ฌ์šฉ" disableDrawer: "๋“œ๋กœ์–ด ๋ฉ”๋‰ด๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ธฐ" youHaveNoGroups: "๊ทธ๋ฃน์ด ์—†์Šต๋‹ˆ๋‹ค" @@ -449,47 +403,42 @@ joinOrCreateGroup: "๋‹ค๋ฅธ ๊ทธ๋ฃน์˜ ์ดˆ๋Œ€๋ฅผ ๋ฐ›๊ฑฐ๋‚˜, ์ง์ ‘ ์ƒˆ ๊ทธ๋ฃน์„ noHistory: "๊ธฐ๋ก์ด ์—†์Šต๋‹ˆ๋‹ค" signinHistory: "๋กœ๊ทธ์ธ ๊ธฐ๋ก" disableAnimatedMfm: "์›€์ง์ž„์ด ์žˆ๋Š” MFM์„ ๋น„ํ™œ์„ฑํ™”" -doing: "์ž ์‹œ๋งŒ์š”" category: "์นดํ…Œ๊ณ ๋ฆฌ" tags: "ํƒœ๊ทธ" -docSource: "์ด ๋ฌธ์„œ์˜ ์†Œ์Šค" createAccount: "๊ณ„์ • ๋งŒ๋“ค๊ธฐ" existingAccount: "๊ธฐ์กด ๊ณ„์ •" -regenerate: "์žฌ์ƒ์„ฑ" fontSize: "๊ธ€์ž ํฌ๊ธฐ" noFollowRequests: "์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ํŒ”๋กœ์šฐ ์š”์ฒญ์ด ์—†์Šต๋‹ˆ๋‹ค" openImageInNewTab: "์ƒˆ ํƒญ์—์„œ ์ด๋ฏธ์ง€ ์—ด๊ธฐ" dashboard: "๋Œ€์‹œ๋ณด๋“œ" local: "๋กœ์ปฌ" remote: "๋ฆฌ๋ชจํŠธ" -total: "ํ•ฉ๊ณ„" -weekOverWeekChanges: "์ง€๋‚œ์ฃผ๋ณด๋‹ค" dayOverDayChanges: "์–ด์ œ๋ณด๋‹ค" appearance: "๋ชจ์–‘" clientSettings: "ํด๋ผ์ด์–ธํŠธ ์„ค์ •" -accountSettings: "๊ณ„์ • ์„ค์ •" -numberOfDays: "๋ฉฐ์น ๋™์•ˆ" -hideThisNote: "์ด ๋…ธํŠธ๋ฅผ ์ˆจ๊ธฐ๊ธฐ" showFeaturedNotesInTimeline: "ํƒ€์ž„๋ผ์ธ์— ์ถ”์ฒœ ๋…ธํŠธ๋ฅผ ํ‘œ์‹œ" objectStorage: "์˜ค๋ธŒ์ ํŠธ ์Šคํ† ๋ฆฌ์ง€" useObjectStorage: "์˜ค๋ธŒ์ ํŠธ ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์‚ฌ์šฉ" objectStorageBaseUrl: "Base URL" -objectStorageBaseUrlDesc: "์˜ค๋ธŒ์ ํŠธ (๋ฏธ๋””์–ด) ์ฐธ์กฐ URL ์„ ๋งŒ๋“ค ๋•Œ ์‚ฌ์šฉ๋˜๋Š” URL์ž…๋‹ˆ๋‹ค. CDN ๋˜๋Š” ํ”„๋ก์‹œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๊ทธ URL์„ ์ง€์ •ํ•˜๊ณ , ๊ทธ ์™ธ์˜ ๊ฒฝ์šฐ ์‚ฌ์šฉํ•  ์„œ๋น„์Šค์˜ ๊ฐ€์ด๋“œ์— ๋”ฐ๋ผ ๊ณต๊ฐœ์ ์œผ๋กœ ์•ก์„ธ์Šค ํ•  ์ˆ˜ ์žˆ๋Š” ์ฃผ์†Œ๋ฅผ ์ง€์ •ํ•ด ์ฃผ์„ธ์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, AWS S3์˜ ๊ฒฝ์šฐ 'https://.s3.amazonaws.com', GCS๋“ฑ์˜ ๊ฒฝ์šฐ 'https://storage.googleapis.com/' ์™€ ๊ฐ™์ด ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค." +objectStorageBaseUrlDesc: "์˜ค๋ธŒ์ ํŠธ (๋ฏธ๋””์–ด) ์ฐธ์กฐ URL ์„ ๋งŒ๋“ค ๋•Œ ์‚ฌ์šฉ๋˜๋Š” URL์ž…๋‹ˆ๋‹ค. CDN ๋˜๋Š” ํ”„๋ก์‹œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”\ + \ ๊ฒฝ์šฐ ๊ทธ URL์„ ์ง€์ •ํ•˜๊ณ , ๊ทธ ์™ธ์˜ ๊ฒฝ์šฐ ์‚ฌ์šฉํ•  ์„œ๋น„์Šค์˜ ๊ฐ€์ด๋“œ์— ๋”ฐ๋ผ ๊ณต๊ฐœ์ ์œผ๋กœ ์•ก์„ธ์Šค ํ•  ์ˆ˜ ์žˆ๋Š” ์ฃผ์†Œ๋ฅผ ์ง€์ •ํ•ด ์ฃผ์„ธ์š”. ์˜ˆ๋ฅผ ๋“ค์–ด,\ + \ AWS S3์˜ ๊ฒฝ์šฐ 'https://.s3.amazonaws.com', GCS๋“ฑ์˜ ๊ฒฝ์šฐ 'https://storage.googleapis.com/'\ + \ ์™€ ๊ฐ™์ด ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค." objectStorageBucket: "Bucket" objectStorageBucketDesc: "์‚ฌ์šฉ ์„œ๋น„์Šค์˜ bucket๋ช…์„ ์ง€์ •ํ•ด์ฃผ์„ธ์š”." objectStoragePrefix: "Prefix" objectStoragePrefixDesc: "์ด Prefix ์˜ ๋””๋ ‰ํ† ๋ฆฌ ์•„๋ž˜์— ํŒŒ์ผ์ด ์ €์žฅ๋ฉ๋‹ˆ๋‹ค." objectStorageEndpoint: "Endpoint" -objectStorageEndpointDesc: "AWS S3์˜ ๊ฒฝ์šฐ ๊ณต๋ž€, ๋‹ค๋ฅธ ์„œ๋น„์Šค์˜ ๊ฒฝ์šฐ ๊ฐ ์„œ๋น„์Šค์˜ ๊ฐ€์ด๋“œ์— ๋งž๊ฒŒ endpoint๋ฅผ ์„ค์ •ํ•ด์ฃผ์„ธ์š”. '' ํ˜น์€ ':' ์™€ ๊ฐ™์ด ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค." +objectStorageEndpointDesc: "AWS S3์˜ ๊ฒฝ์šฐ ๊ณต๋ž€, ๋‹ค๋ฅธ ์„œ๋น„์Šค์˜ ๊ฒฝ์šฐ ๊ฐ ์„œ๋น„์Šค์˜ ๊ฐ€์ด๋“œ์— ๋งž๊ฒŒ endpoint๋ฅผ ์„ค์ •ํ•ด์ฃผ์„ธ์š”.\ + \ '' ํ˜น์€ ':' ์™€ ๊ฐ™์ด ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค." objectStorageRegion: "Region" -objectStorageRegionDesc: "'xx-east-1'์™€ ๊ฐ™์ด region์„ ์ง€์ •ํ•ด์ฃผ์„ธ์š”. ์‚ฌ์šฉํ•˜๋Š” ์„œ๋น„์Šค์— region ๊ฐœ๋…์ด ์—†๋Š” ๊ฒฝ์šฐ, ๋น„์›Œ ๋‘๊ฑฐ๋‚˜ 'us-east-1'์œผ๋กœ ์„ค์ •ํ•ด ์ฃผ์„ธ์š”." +objectStorageRegionDesc: "'xx-east-1'์™€ ๊ฐ™์ด region์„ ์ง€์ •ํ•ด์ฃผ์„ธ์š”. ์‚ฌ์šฉํ•˜๋Š” ์„œ๋น„์Šค์— region ๊ฐœ๋…์ด ์—†๋Š”\ + \ ๊ฒฝ์šฐ, ๋น„์›Œ ๋‘๊ฑฐ๋‚˜ 'us-east-1'์œผ๋กœ ์„ค์ •ํ•ด ์ฃผ์„ธ์š”." objectStorageUseSSL: "SSL ์‚ฌ์šฉ" objectStorageUseSSLDesc: "API ํ˜ธ์ถœ์‹œ HTTPS ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ OFF ๋กœ ์„ค์ •ํ•ด ์ฃผ์„ธ์š”" objectStorageUseProxy: "์—ฐ๊ฒฐ์— ํ”„๋ก์‹œ๋ฅผ ์‚ฌ์šฉ" objectStorageUseProxyDesc: "์˜ค๋ธŒ์ ํŠธ ์Šคํ† ๋ฆฌ์ง€ API ํ˜ธ์ถœ์‹œ ํ”„๋ก์‹œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ OFF ๋กœ ์„ค์ •ํ•ด ์ฃผ์„ธ์š”" objectStorageSetPublicRead: "์—…๋กœ๋“œํ•  ๋•Œ 'public-read'๋ฅผ ์„ค์ •ํ•˜๊ธฐ" -serverLogs: "์„œ๋ฒ„ ๋กœ๊ทธ" -deleteAll: "๋ชจ๋‘ ์‚ญ์ œ" showFixedPostForm: "ํƒ€์ž„๋ผ์ธ ์ƒ๋‹จ์— ๊ธ€ ์ž‘์„ฑ๋ž€์„ ํ‘œ์‹œ" newNoteRecived: "์ƒˆ ๋…ธํŠธ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค" sounds: "์†Œ๋ฆฌ" @@ -500,7 +449,6 @@ popout: "์ƒˆ ์ฐฝ์œผ๋กœ ์—ด๊ธฐ" volume: "์Œ๋Ÿ‰" masterVolume: "๋งˆ์Šคํ„ฐ ๋ณผ๋ฅจ" details: "์ž์„ธํžˆ" -chooseEmoji: "์ด๋ชจ์ง€ ์„ ํƒ" unableToProcess: "์ž‘์—…์„ ์™„๋ฃŒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" recentUsed: "์ตœ๊ทผ ์‚ฌ์šฉ" install: "์„ค์น˜" @@ -514,28 +462,27 @@ sort: "์ •๋ ฌ" ascendingOrder: "์˜ค๋ฆ„์ฐจ์ˆœ" descendingOrder: "๋‚ด๋ฆผ์ฐจ์ˆœ" scratchpad: "์Šคํฌ๋ž˜์น˜ ํŒจ๋“œ" -scratchpadDescription: "์Šคํฌ๋ž˜์น˜ ํŒจ๋“œ๋Š” AiScript ์˜ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. FoundKey ์™€ ์ƒํ˜ธ ์ž‘์šฉํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ, ์‹คํ–‰ ๋ฐ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." +scratchpadDescription: "์Šคํฌ๋ž˜์น˜ ํŒจ๋“œ๋Š” AiScript ์˜ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. FoundKey ์™€ ์ƒํ˜ธ ์ž‘์šฉํ•˜๋Š” ์ฝ”๋“œ๋ฅผ\ + \ ์ž‘์„ฑ, ์‹คํ–‰ ๋ฐ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." output: "์ถœ๋ ฅ" -script: "์Šคํฌ๋ฆฝํŠธ" updateRemoteUser: "๋ฆฌ๋ชจํŠธ ์œ ์ € ์ •๋ณด ๊ฐฑ์‹ " deleteAllFiles: "๋ชจ๋“  ํŒŒ์ผ ์‚ญ์ œ" deleteAllFilesConfirm: "๋ชจ๋“  ํŒŒ์ผ์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" removeAllFollowing: "๋ชจ๋“  ํŒ”๋กœ์ž‰ ํ•ด์ œ" -removeAllFollowingDescription: "{host}(์œผ)๋กœ๋ถ€ํ„ฐ ๋ชจ๋“  ํŒ”๋กœ์ž‰์„ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ์ธ์Šคํ„ด์Šค๊ฐ€ ๋” ์ด์ƒ ์กด์žฌํ•˜์ง€ ์•Š๊ฒŒ ๋œ ๊ฒฝ์šฐ ๋“ฑ์— ์‹คํ–‰ํ•ด ์ฃผ์„ธ์š”." +removeAllFollowingDescription: "{host}(์œผ)๋กœ๋ถ€ํ„ฐ ๋ชจ๋“  ํŒ”๋กœ์ž‰์„ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ์ธ์Šคํ„ด์Šค๊ฐ€ ๋” ์ด์ƒ ์กด์žฌํ•˜์ง€ ์•Š๊ฒŒ\ + \ ๋œ ๊ฒฝ์šฐ ๋“ฑ์— ์‹คํ–‰ํ•ด ์ฃผ์„ธ์š”." userSuspended: "์ด ๊ณ„์ •์€ ์ •์ง€๋œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค." userSilenced: "์ด ๊ณ„์ •์€ ์‚ฌ์ผ๋Ÿฐ์Šค๋œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค." yourAccountSuspendedTitle: "๊ณ„์ •์ด ์ •์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค" -yourAccountSuspendedDescription: "์ด ๊ณ„์ •์€ ์„œ๋ฒ„์˜ ์ด์šฉ ์•ฝ๊ด€์„ ์œ„๋ฐ˜ํ•˜๊ฑฐ๋‚˜, ๊ธฐํƒ€ ๋‹ค๋ฅธ ์ด์œ ๋กœ ์ธํ•ด ์ •์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ž์„ธํ•œ ์‚ฌํ•ญ์€ ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•ด ์ฃผ์‹ญ์‹œ์˜ค. ๊ณ„์ •์„ ์ƒˆ๋กœ ์ƒ์„ฑํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค." +yourAccountSuspendedDescription: "์ด ๊ณ„์ •์€ ์„œ๋ฒ„์˜ ์ด์šฉ ์•ฝ๊ด€์„ ์œ„๋ฐ˜ํ•˜๊ฑฐ๋‚˜, ๊ธฐํƒ€ ๋‹ค๋ฅธ ์ด์œ ๋กœ ์ธํ•ด ์ •์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ž์„ธํ•œ\ + \ ์‚ฌํ•ญ์€ ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•ด ์ฃผ์‹ญ์‹œ์˜ค. ๊ณ„์ •์„ ์ƒˆ๋กœ ์ƒ์„ฑํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค." menu: "๋ฉ”๋‰ด" divider: "๊ตฌ๋ถ„์„ " addItem: "ํ•ญ๋ชฉ ์ถ”๊ฐ€" relays: "๋ฆด๋ ˆ์ด" addRelay: "๋ฆด๋ ˆ์ด ์ถ”๊ฐ€" inboxUrl: "Inbox ์ฃผ์†Œ" -addedRelays: "์ถ”๊ฐ€๋œ ๋ฆด๋ ˆ์ด" -serviceworkerInfo: "ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ์ˆ˜ํ–‰ํ•˜๋ ค๋ฉด ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." deletedNote: "์‚ญ์ œ๋œ ๋…ธํŠธ" -invisibleNote: "๋น„๊ณต๊ฐœ ๋…ธํŠธ" enableInfiniteScroll: "์ž๋™์œผ๋กœ ์ข€ ๋” ๋ณด๊ธฐ" visibility: "๊ณต๊ฐœ ๋ฒ”์œ„" poll: "ํˆฌํ‘œ" @@ -545,15 +492,12 @@ disablePlayer: "ํ”Œ๋ ˆ์ด์–ด ๋‹ซ๊ธฐ" themeEditor: "ํ…Œ๋งˆ ์—๋””ํ„ฐ" description: "์„ค๋ช…" describeFile: "์บก์…˜ ์ถ”๊ฐ€" -enterFileDescription: "์บก์…˜ ์ž…๋ ฅ" author: "์ž‘์„ฑ์ž" leaveConfirm: "์ €์žฅํ•˜์ง€ ์•Š์€ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ทจ์†Œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" manage: "๊ด€๋ฆฌ" plugins: "ํ”Œ๋Ÿฌ๊ทธ์ธ" deck: "๋ฑ" -undeck: "๋ฑ ํ•ด์ œ" useBlurEffectForModal: "๋ชจ๋‹ฌ์— ํ๋ฆผ ํšจ๊ณผ ์‚ฌ์šฉ" -useFullReactionPicker: "๋ชจ๋“  ๊ธฐ๋Šฅ์ด ํฌํ•จ๋œ ๋ฆฌ์•ก์…˜ ์„ ํƒ๊ธฐ ์‚ฌ์šฉ" width: "ํญ" height: "๋†’์ด" large: "ํฌ๊ฒŒ" @@ -565,7 +509,6 @@ enableAll: "์ „์ฒด ์„ ํƒ" disableAll: "์ „์ฒด ํ•ด์ œ" tokenRequested: "๊ณ„์ • ์ ‘๊ทผ ํ—ˆ์šฉ" pluginTokenRequestedDescription: "์ด ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ์—ฌ๊ธฐ์„œ ์„ค์ •ํ•œ ๊ถŒํ•œ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค." -notificationType: "์•Œ๋ฆผ ์œ ํ˜•" edit: "ํŽธ์ง‘" useStarForReactionFallback: "์•Œ ์ˆ˜ ์—†๋Š” ๋ฆฌ์•ก์…˜ ์ด๋ชจ์ง€ ๋Œ€์‹  โ˜… ์‚ฌ์šฉ" emailServer: "๋ฉ”์ผ ์„œ๋ฒ„" @@ -590,10 +533,7 @@ userSaysSomething: "{name}๋‹˜์ด ๋ฌด์–ธ๊ฐ€๋ฅผ ๋งํ–ˆ์Šต๋‹ˆ๋‹ค" makeActive: "ํ™œ์„ฑํ™”" display: "ํ‘œ์‹œ" copy: "๋ณต์‚ฌ" -metrics: "ํ†ต๊ณ„" overview: "์š”์•ฝ" -logs: "๋กœ๊ทธ" -delayed: "์ง€์—ฐ" database: "๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค" channel: "์ฑ„๋„" create: "์ƒ์„ฑ" @@ -603,11 +543,11 @@ useGlobalSetting: "๊ธ€๋กœ๋ฒŒ ์„ค์ •์„ ์‚ฌ์šฉํ•˜๊ธฐ" useGlobalSettingDesc: "ํ™œ์„ฑํ™”ํ•˜๋ฉด ๊ณ„์ •์˜ ์•Œ๋ฆผ ์„ค์ •์ด ์ ์šฉ๋˜๋‹ˆ๋‹ค. ๋น„ํ™œ์„ฑํ™”ํ•˜๋ฉด ๊ฐœ๋ณ„์ ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค." other: "๊ธฐํƒ€" regenerateLoginToken: "๋กœ๊ทธ์ธ ํ† ํฐ์„ ์žฌ์ƒ์„ฑ" -regenerateLoginTokenDescription: "๋กœ๊ทธ์ธํ•  ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ๋‚ด๋ถ€ ํ† ํฐ์„ ์žฌ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ์ด ์ž‘์—…์„ ์‹คํ–‰ํ•  ํ•„์š”๋Š” ์—†์Šต๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธํ•œ ๋ชจ๋“  ๊ธฐ๊ธฐ์—์„œ ๋กœ๊ทธ์•„์›ƒ๋ฉ๋‹ˆ๋‹ค." +regenerateLoginTokenDescription: "๋กœ๊ทธ์ธํ•  ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ๋‚ด๋ถ€ ํ† ํฐ์„ ์žฌ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ์ด ์ž‘์—…์„ ์‹คํ–‰ํ•  ํ•„์š”๋Š” ์—†์Šต๋‹ˆ๋‹ค.\ + \ ์ด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธํ•œ ๋ชจ๋“  ๊ธฐ๊ธฐ์—์„œ ๋กœ๊ทธ์•„์›ƒ๋ฉ๋‹ˆ๋‹ค." setMultipleBySeparatingWithSpace: "๊ณต๋ฐฑ์œผ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์—ฌ๋Ÿฌ ๊ฐœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." fileIdOrUrl: "ํŒŒ์ผ ID ๋˜๋Š” URL" behavior: "๋™์ž‘" -sample: "์˜ˆ์‹œ" abuseReports: "์‹ ๊ณ " reportAbuse: "์‹ ๊ณ " reportAbuseOf: "{name}์„ ์‹ ๊ณ ํ•˜๊ธฐ" @@ -621,12 +561,8 @@ forwardReportIsAnonymous: "๋ฆฌ๋ชจํŠธ ์ธ์Šคํ„ด์Šค์—์„œ๋Š” ๋‚˜์˜ ์ •๋ณด๋ฅผ ๋ณผ send: "์ „์†ก" abuseMarkAsResolved: "ํ•ด๊ฒฐ๋จ์œผ๋กœ ํ‘œ์‹œ" openInNewTab: "์ƒˆ ํƒญ์—์„œ ์—ด๊ธฐ" -openInSideView: "์‚ฌ์ด๋“œ๋ทฐ๋กœ ์—ด๊ธฐ" defaultNavigationBehaviour: "๊ธฐ๋ณธ ํƒ์ƒ‰ ๋™์ž‘" -editTheseSettingsMayBreakAccount: "์ด ์„ค์ •์„ ๋ณ€๊ฒฝํ•˜๋ฉด ๊ณ„์ •์ด ์†์ƒ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." instanceTicker: "๋…ธํŠธ์˜ ์ธ์Šคํ„ด์Šค ์ •๋ณด" -waitingFor: "{x}์„(๋ฅผ) ๊ธฐ๋‹ค๋ฆฌ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค" -random: "๋žœ๋ค" system: "์‹œ์Šคํ…œ" switchUi: "UI ์ „ํ™˜" desktop: "๋ฐ์Šคํฌํƒ‘" @@ -660,16 +596,12 @@ alwaysMarkSensitive: "๋ฏธ๋””์–ด๋ฅผ ํ•ญ์ƒ ์—ด๋žŒ ์ฃผ์˜๋กœ ์„ค์ •" loadRawImages: "์ฒจ๋ถ€ํ•œ ์ด๋ฏธ์ง€์˜ ์ธ๋„ค์ผ์„ ์›๋ณธํ™”์งˆ๋กœ ํ‘œ์‹œ" disableShowingAnimatedImages: "์›€์ง์ด๋Š” ์ด๋ฏธ์ง€๋ฅผ ์ž๋™์œผ๋กœ ์žฌ์ƒํ•˜์ง€ ์•Š์Œ" verificationEmailSent: "ํ™•์ธ ๋ฉ”์ผ์„ ๋ฐœ์†กํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์„ค์ •์„ ์™„๋ฃŒํ•˜๋ ค๋ฉด ๋ฉ”์ผ์— ์ฒจ๋ถ€๋œ ๋งํฌ๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š”." -notSet: "์„ค์ •๋˜์ง€ ์•Š์Œ" emailVerified: "๋ฉ”์ผ ์ฃผ์†Œ๊ฐ€ ํ™•์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." -noteFavoritesCount: "์ฆ๊ฒจ์ฐพ๊ธฐํ•œ ๋…ธํŠธ ์ˆ˜" pageLikesCount: "์ข‹์•„์š” ํ•œ Page ์ˆ˜" pageLikedCount: "Page์— ๋ฐ›์€ ์ข‹์•„์š” ์ˆ˜" contact: "์—ฐ๋ฝ์ฒ˜" useSystemFont: "์‹œ์Šคํ…œ ๊ธฐ๋ณธ ๊ธ€๊ผด์„ ์‚ฌ์šฉ" clips: "ํด๋ฆฝ" -experimentalFeatures: "์‹คํ—˜์‹ค" -developer: "๊ฐœ๋ฐœ์ž" makeExplorable: "\"๋ฐœ๊ฒฌํ•˜๊ธฐ\"์— ๋‚ด ๊ณ„์ • ๋ณด์ด๊ธฐ" makeExplorableDescription: "๋น„ํ™œ์„ฑํ™”ํ•˜๋ฉด \"๋ฐœ๊ฒฌํ•˜๊ธฐ\"์— ๋‚˜์˜ ๊ณ„์ •์„ ํ‘œ์‹œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." showGapBetweenNotesInTimeline: "ํƒ€์ž„๋ผ์ธ์˜ ๋…ธํŠธ ์‚ฌ์ด๋ฅผ ๋„์›Œ์„œ ํ‘œ์‹œ" @@ -680,28 +612,16 @@ wide: "๋„“๊ฒŒ" narrow: "์ข๊ฒŒ" reloadToApplySetting: "์ด ์„ค์ •์„ ์ ์šฉํ•˜๋ ค๋ฉด ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ”๋กœ ์ƒˆ๋กœ๊ณ ์นจํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" needReloadToApply: "๋ณ€๊ฒฝ ์‚ฌํ•ญ์€ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค." -showTitlebar: "ํƒ€์ดํ‹€ ๋ฐ”๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ" clearCache: "์บ์‹œ ๋น„์šฐ๊ธฐ" onlineUsersCount: "{n}๋ช…์ด ์ ‘์† ์ค‘" -nUsers: "{n} ์œ ์ €" -nNotes: "{n} ๋…ธํŠธ" -myTheme: "๋‚ด ํ…Œ๋งˆ" backgroundColor: "๋ฐฐ๊ฒฝ ์ƒ‰" accentColor: "๊ฐ•์กฐ ์ƒ‰์ƒ" textColor: "๋ฌธ์ž ์ƒ‰" saveAs: "๋‹ค๋ฅธ ์ด๋ฆ„์œผ๋กœ ์ €์žฅ" -advanced: "๊ณ ๊ธ‰" -value: "๊ฐ’" createdAt: "์ƒ์„ฑ๋œ ๋‚ ์งœ" updatedAt: "์ˆ˜์ •ํ•œ ๋‚ ์งœ" -saveConfirm: "์ €์žฅํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" deleteConfirm: "์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" -invalidValue: "์˜ฌ๋ฐ”๋ฅธ ๊ฐ’์ด ์•„๋‹™๋‹ˆ๋‹ค." -registry: "๋ ˆ์ง€์ŠคํŠธ๋ฆฌ" closeAccount: "๊ณ„์ • ํ์‡„" -currentVersion: "ํ˜„์žฌ ๋ฒ„์ „" -latestVersion: "์ตœ์‹  ๋ฒ„์ „" -youAreRunningUpToDateClient: "์‚ฌ์šฉ ์ค‘์ธ ํด๋ผ์ด์–ธํŠธ๋Š” ์ตœ์‹ ์ž…๋‹ˆ๋‹ค." newVersionOfClientAvailable: "์ƒˆ๋กœ์šด ๋ฒ„์ „์˜ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." usageAmount: "์‚ฌ์šฉ๋Ÿ‰" capacity: "์šฉ๋Ÿ‰" @@ -710,12 +630,9 @@ editCode: "์ฝ”๋“œ ์ˆ˜์ •" apply: "์ ์šฉ" receiveAnnouncementFromInstance: "์ด ์ธ์Šคํ„ด์Šค์˜ ์•Œ๋ฆผ์„ ์ด๋ฉ”์ผ๋กœ ์ˆ˜์‹ ํ• ๊ฒŒ์š”" emailNotification: "๋ฉ”์ผ ์•Œ๋ฆผ" -publish: "๊ฒŒ์‹œ" -inChannelSearch: "์ฑ„๋„์—์„œ ๊ฒ€์ƒ‰" useReactionPickerForContextMenu: "์šฐํด๋ฆญํ•˜์—ฌ ๋ฆฌ์•ก์…˜ ์„ ํƒ๊ธฐ ์—ด๊ธฐ" typingUsers: "{users} ๋‹˜์ด ์ž…๋ ฅํ•˜๊ณ  ์žˆ์–ด์š”.." jumpToSpecifiedDate: "ํŠน์ • ๋‚ ์งœ๋กœ ์ด๋™" -showingPastTimeline: "๊ณผ๊ฑฐ์˜ ํƒ€์ž„๋ผ์ธ์„ ํ‘œ์‹œํ•˜๊ณ  ์žˆ์–ด์š”" clear: "์ง€์šฐ๊ธฐ" markAllAsRead: "๋ชจ๋‘ ์ฝ์€ ์ƒํƒœ๋กœ ํ‘œ์‹œ" goBack: "๋’ค๋กœ" @@ -728,7 +645,6 @@ notSpecifiedMentionWarning: "์ˆ˜์‹ ์ž๊ฐ€ ์„ ํƒ๋˜์ง€ ์•Š์€ ๋ฉ˜์…˜์ด ์žˆ์–ด info: "์ •๋ณด" userInfo: "์œ ์ € ์ •๋ณด" unknown: "์•Œ ์ˆ˜ ์—†์Œ" -onlineStatus: "์˜จ๋ผ์ธ ์ƒํƒœ" hideOnlineStatus: "์˜จ๋ผ์ธ ์ƒํƒœ ์ˆจ๊ธฐ๊ธฐ" hideOnlineStatusDescription: "์˜จ๋ผ์ธ ์ƒํƒœ๋ฅผ ์ˆจ๊ธฐ๋ฉด, ๊ฒ€์ƒ‰๊ณผ ๊ฐ™์€ ์ผ๋ถ€ ๊ธฐ๋Šฅ์— ์˜ํ–ฅ์„ ๋ฏธ์น  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." online: "์˜จ๋ผ์ธ" @@ -749,26 +665,15 @@ switch: "์ „ํ™˜" noMaintainerInformationWarning: "๊ด€๋ฆฌ์ž ์ •๋ณด๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค." noBotProtectionWarning: "Bot ๋ฐฉ์–ด๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค." configure: "์„ค์ •ํ•˜๊ธฐ" -postToGallery: "๊ฐค๋Ÿฌ๋ฆฌ์— ์—…๋กœ๋“œ" -gallery: "๊ฐค๋Ÿฌ๋ฆฌ" recentPosts: "์ตœ๊ทผ ํฌ์ŠคํŠธ" -popularPosts: "์ธ๊ธฐ ํฌ์ŠคํŠธ" shareWithNote: "๋…ธํŠธ๋กœ ๊ณต์œ " -expiration: "๊ธฐํ•œ" -memo: "๋ฉ”๋ชจ" -priority: "์šฐ์„ ์ˆœ์œ„" -high: "๋†’์Œ" -middle: "๋ณดํ†ต" -low: "๋‚ฎ์Œ" emailNotConfiguredWarning: "๋ฉ”์ผ ์ฃผ์†Œ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค." ratio: "๋น„์œจ" previewNoteText: "๋ณธ๋ฌธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ" customCss: "CSS ์‚ฌ์šฉ์žํ™”" -customCssWarn: "์ด ์„ค์ •์€ ๊ธฐ๋Šฅ์„ ์•Œ๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ž˜๋ชป๋œ ๊ฐ’์„ ์ž…๋ ฅํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." -global: "๊ธ€๋กœ๋ฒŒ" +customCssWarn: "์ด ์„ค์ •์€ ๊ธฐ๋Šฅ์„ ์•Œ๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ž˜๋ชป๋œ ๊ฐ’์„ ์ž…๋ ฅํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•˜์ง€ ์•Š์„ ์ˆ˜\ + \ ์žˆ์Šต๋‹ˆ๋‹ค." squareAvatars: "ํ”„๋กœํ•„ ์•„์ด์ฝ˜์„ ์‚ฌ๊ฐํ˜•์œผ๋กœ ํ‘œ์‹œ" -sent: "์ „์†ก" -received: "์ˆ˜์‹ " searchResult: "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ" hashtags: "ํ•ด์‹œํƒœ๊ทธ" troubleshooting: "๋ฌธ์ œ ํ•ด๊ฒฐ" @@ -779,7 +684,8 @@ whatIsNew: "ํŒจ์น˜ ์ •๋ณด ๋ณด๊ธฐ" translate: "๋ฒˆ์—ญ" translatedFrom: "{x}์—์„œ ๋ฒˆ์—ญ" accountDeletionInProgress: "๊ณ„์ • ์‚ญ์ œ ์ž‘์—…์„ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค" -usernameInfo: "์„œ๋ฒ„์ƒ์—์„œ ๊ณ„์ •์„ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•œ ์ด๋ฆ„. ์•ŒํŒŒ๋ฒณ(a~z, A~Z), ์ˆซ์ž(0~9) ๋ฐ ์–ธ๋”๋ฐ”(_)๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋ช…์€ ๋‚˜์ค‘์— ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." +usernameInfo: "์„œ๋ฒ„์ƒ์—์„œ ๊ณ„์ •์„ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•œ ์ด๋ฆ„. ์•ŒํŒŒ๋ฒณ(a~z, A~Z), ์ˆซ์ž(0~9) ๋ฐ ์–ธ๋”๋ฐ”(_)๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\ + \ ์‚ฌ์šฉ์ž๋ช…์€ ๋‚˜์ค‘์— ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." keepCw: "CW ์œ ์ง€ํ•˜๊ธฐ" pubSub: "Pub/Sub ๊ณ„์ •" lastCommunication: "๋งˆ์ง€๋ง‰ ํ†ต์‹ " @@ -842,8 +748,9 @@ _ffVisibility: private: "๋น„๊ณต๊ฐœ" _signup: almostThere: "๊ฑฐ์˜ ๋‹ค ๋๋‚ฌ์Šต๋‹ˆ๋‹ค" - emailAddressInfo: "๋‹น์‹ ์ด ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”. ์ด๋ฉ”์ผ ์ฃผ์†Œ๋Š” ๋‹ค๋ฅธ ์œ ์ €์—๊ฒŒ ๊ณต๊ฐœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." - emailSent: "์ž…๋ ฅํ•˜์‹  ๋ฉ”์ผ ์ฃผ์†Œ({email})๋กœ ํ™•์ธ ๋ฉ”์ผ์„ ๋ณด๋‚ด๋“œ๋ ธ์Šต๋‹ˆ๋‹ค. ๊ฐ€์ž…์„ ์™„๋ฃŒํ•˜์‹œ๋ ค๋ฉด ๋ณด๋‚ด๋“œ๋ฆฐ ๋ฉ”์ผ์— ์žˆ๋Š” ๋งํฌ๋กœ ์ ‘์†ํ•ด ์ฃผ์„ธ์š”." + emailAddressInfo: "๋‹น์‹ ์ด ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”. ์ด๋ฉ”์ผ ์ฃผ์†Œ๋Š” ๋‹ค๋ฅธ ์œ ์ €์—๊ฒŒ ๊ณต๊ฐœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." + emailSent: "์ž…๋ ฅํ•˜์‹  ๋ฉ”์ผ ์ฃผ์†Œ({email})๋กœ ํ™•์ธ ๋ฉ”์ผ์„ ๋ณด๋‚ด๋“œ๋ ธ์Šต๋‹ˆ๋‹ค. ๊ฐ€์ž…์„ ์™„๋ฃŒํ•˜์‹œ๋ ค๋ฉด ๋ณด๋‚ด๋“œ๋ฆฐ ๋ฉ”์ผ์— ์žˆ๋Š” ๋งํฌ๋กœ ์ ‘์†ํ•ด\ + \ ์ฃผ์„ธ์š”." _accountDelete: accountDelete: "๊ณ„์ • ์‚ญ์ œ" mayTakeTime: "๊ณ„์ • ์‚ญ์ œ๋Š” ์„œ๋ฒ„์— ๋ถ€ํ•˜๋ฅผ ๊ฐ€ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ž‘์„ฑํ•œ ์ฝ˜ํ…์ธ ๋‚˜ ์—…๋กœ๋“œํ•œ ํŒŒ์ผ์˜ ์ˆ˜๊ฐ€ ๋งŽ์œผ๋ฉด ์™„๋ฃŒ๊นŒ์ง€ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." @@ -851,18 +758,10 @@ _accountDelete: requestAccountDelete: "๊ณ„์ • ์‚ญ์ œ ์š”์ฒญ" started: "์‚ญ์ œ ์ž‘์—…์ด ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค." inProgress: "์‚ญ์ œ ์ง„ํ–‰ ์ค‘" -_ad: - back: "๋’ค๋กœ" - reduceFrequencyOfThisAd: "์ด ๊ด‘๊ณ ์˜ ํ‘œ์‹œ ๋นˆ๋„ ๋‚ฎ์ถ”๊ธฐ" _forgotPassword: enterEmail: "์—ฌ๊ธฐ์— ๊ณ„์ •์— ๋“ฑ๋กํ•œ ๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”. ์ž…๋ ฅํ•œ ๋ฉ”์ผ ์ฃผ์†Œ๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๋งํฌ๋ฅผ ๋ฐœ์†กํ•ฉ๋‹ˆ๋‹ค." ifNoEmail: "๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ๋“ฑ๋กํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ, ๊ด€๋ฆฌ์ž์— ๋ฌธ์˜ํ•ด ์ฃผ์‹ญ์‹œ์˜ค." contactAdmin: "์ด ์ธ์Šคํ„ด์Šค์—์„œ๋Š” ๋ฉ”์ผ ๊ธฐ๋Šฅ์ด ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žฌ์„ค์ •ํ•˜๋ ค๋ฉด ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•ด ์ฃผ์‹ญ์‹œ์˜ค." -_gallery: - my: "๋‚ด ๊ฐค๋Ÿฌ๋ฆฌ" - liked: "์ข‹์•„์š” ํ•œ ๊ฐค๋Ÿฌ๋ฆฌ" - like: "์ข‹์•„์š”!" - unlike: "์ข‹์•„์š” ์ทจ์†Œ" _email: _follow: title: "์ƒˆ๋กœ์šด ํŒ”๋กœ์›Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค" @@ -871,7 +770,6 @@ _email: _plugin: install: "ํ”Œ๋Ÿฌ๊ทธ์ธ ์„ค์น˜" installWarn: "์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ์„ค์น˜ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค." - manage: "ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ด€๋ฆฌ" _registry: scope: "๋ฒ”์œ„" key: "ํ‚ค" @@ -880,17 +778,16 @@ _registry: createKey: "ํ‚ค ์ƒ์„ฑ" _aboutMisskey: about: "FoundKey๋Š” syuilo์— ์˜ํ•ด์„œ 2014๋…„๋ถ€ํ„ฐ ๊ฐœ๋ฐœ๋˜์–ด ์˜จ ์˜คํ”ˆ์†Œ์Šค ์†Œํ”„ํŠธ์›จ์–ด ์ž…๋‹ˆ๋‹ค." - contributors: "์ฃผ์š” ๊ธฐ์—ฌ์ž" allContributors: "๋ชจ๋“  ๊ธฐ์—ฌ์ž" source: "์†Œ์Šค ์ฝ”๋“œ" - translation: "FoundKey๋ฅผ ๋ฒˆ์—ญํ•˜๊ธฐ" _nsfw: respect: "์—ด๋žŒ์ฃผ์˜๋กœ ์„ค์ •๋œ ๋ฏธ๋””์–ด ์ˆจ๊ธฐ๊ธฐ" ignore: "์—ด๋žŒ ์ฃผ์˜ ๋ฏธ๋””์–ด ํ•ญ์ƒ ํ‘œ์‹œ" force: "๋ฏธ๋””์–ด ํ•ญ์ƒ ์ˆจ๊ธฐ๊ธฐ" _mfm: cheatSheet: "MFM ๋„์›€๋ง" - intro: "MFM๋Š” FoundKey์˜ ๋‹ค์–‘ํ•œ ๊ณณ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ „์šฉ ๋งˆํฌ์—… ์–ธ์–ด์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—์„œ๋Š” MFM์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ๋ฌธ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." + intro: "MFM๋Š” FoundKey์˜ ๋‹ค์–‘ํ•œ ๊ณณ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ „์šฉ ๋งˆํฌ์—… ์–ธ์–ด์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—์„œ๋Š” MFM์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ๋ฌธ์„ ํ™•์ธํ• \ + \ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." dummy: "FoundKey๋กœ ์—ฐํ•ฉ์šฐ์ฃผ์˜ ์„ธ๊ณ„๊ฐ€ ํŽผ์ณ์ง‘๋‹ˆ๋‹ค" mention: "๋ฉ˜์…˜" mentionDescription: "๊ณจ๋ฑ…์ดํ‘œ(@) ๋’ค์— ์‚ฌ์šฉ์ž๋ช…์„ ๋„ฃ์–ด ํŠน์ • ์œ ์ €๋ฅผ ๋‚˜ํƒ€๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." @@ -1001,68 +898,6 @@ _theme: alreadyInstalled: "์ด๋ฏธ ์„ค์น˜๋œ ํ…Œ๋งˆ์ž…๋‹ˆ๋‹ค" invalid: "ํ…Œ๋งˆ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค" make: "ํ…Œ๋งˆ ๋งŒ๋“ค๊ธฐ" - base: "๋ฒ ์ด์Šค" - addConstant: "์ƒ์ˆ˜ ์ถ”๊ฐ€" - constant: "์ƒ์ˆ˜" - defaultValue: "๊ธฐ๋ณธ๊ฐ’" - color: "์ƒ‰" - refProp: "ํ”„๋กœํผํ‹ฐ๋ฅผ ์ฐธ์กฐ" - refConst: "์ƒ์ˆ˜๋ฅผ ์ฐธ์กฐ" - key: "ํ‚ค" - func: "ํ•จ์ˆ˜" - funcKind: "ํ•จ์ˆ˜ ์ข…๋ฅ˜" - argument: "๋งค๊ฐœ๋ณ€์ˆ˜" - basedProp: "๊ธฐ์ค€์œผ๋กœ ํ•  ์†์„ฑ ์ด๋ฆ„" - alpha: "๋ถˆํˆฌ๋ช…๋„" - darken: "์–ด๋‘์›€" - lighten: "๋ฐ์Œ" - inputConstantName: "์ƒ์ˆ˜ ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”" - importInfo: "์—ฌ๊ธฐ์— ํ…Œ๋งˆ ์ฝ”๋“œ๋ฅผ ๋ถ™์—ฌ ๋„ฃ์–ด ์—๋””ํ„ฐ๋กœ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." - deleteConstantConfirm: "์ƒ์ˆ˜ {const}๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" - keys: - accent: "๊ฐ•์กฐ ์ƒ‰์ƒ" - bg: "๋ฐฐ๊ฒฝ" - fg: "ํ…์ŠคํŠธ" - focus: "ํฌ์ปค์Šค" - indicator: "์ธ๋””์ผ€์ดํ„ฐ" - panel: "ํŒจ๋„" - shadow: "๊ทธ๋ฆผ์ž" - header: "ํ—ค๋”" - navBg: "์‚ฌ์ด๋“œ๋ฐ” ๋ฐฐ๊ฒฝ" - navFg: "์‚ฌ์ด๋“œ๋ฐ” ํ…์ŠคํŠธ" - navHoverFg: "์‚ฌ์ด๋“œ๋ฐ” ํ…์ŠคํŠธ (ํ˜ธ๋ฒ„)" - navActive: "์‚ฌ์ด๋“œ๋ฐ” ํ…์ŠคํŠธ (ํ™œ์„ฑ)" - navIndicator: "์‚ฌ์ด๋“œ๋ฐ” ์ธ๋””์ผ€์ดํ„ฐ" - link: "๋งํฌ" - hashtag: "ํ•ด์‹œํƒœ๊ทธ" - mention: "๋ฉ˜์…˜" - mentionMe: "๋‚˜์—๊ฒŒ ๋ณด๋‚ธ ๋ฉ˜์…˜" - renote: "Renote" - modalBg: "๋ชจ๋‹ฌ ๋ฐฐ๊ฒฝ" - divider: "๊ตฌ๋ถ„์„ " - scrollbarHandle: "์Šคํฌ๋กค๋ฐ” ํ•ธ๋“ค" - scrollbarHandleHover: "์Šคํฌ๋กค๋ฐ” ํ•ธ๋“ค (ํ˜ธ๋ฒ„)" - dateLabelFg: "๋‚ ์งœ ๋ ˆ์ด๋ธ” ํ…์ŠคํŠธ" - infoBg: "์ •๋ณด์ฐฝ ๋ฐฐ๊ฒฝ" - infoFg: "์ •๋ณด์ฐฝ ํ…์ŠคํŠธ" - infoWarnBg: "๊ฒฝ๊ณ ์ฐฝ ๋ฐฐ๊ฒฝ" - infoWarnFg: "๊ฒฝ๊ณ ์ฐฝ ํ…์ŠคํŠธ" - cwBg: "CW ๋ฒ„ํŠผ ๋ฐฐ๊ฒฝ" - cwFg: "CW ๋ฒ„ํŠผ ํ…์ŠคํŠธ" - cwHoverBg: "CW ๋ฒ„ํŠผ ๋ฐฐ๊ฒฝ (ํ˜ธ๋ฒ„)" - toastBg: "์•Œ๋ฆผ์ฐฝ ๋ฐฐ๊ฒฝ" - toastFg: "์•Œ๋ฆผ์ฐฝ ํ…์ŠคํŠธ" - buttonBg: "๋ฒ„ํŠผ ๋ฐฐ๊ฒฝ" - buttonHoverBg: "๋ฒ„ํŠผ ๋ฐฐ๊ฒฝ (ํ˜ธ๋ฒ„)" - inputBorder: "์ž…๋ ฅ ํ•„๋“œ ํ…Œ๋‘๋ฆฌ" - listItemHoverBg: "๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ ๋ฐฐ๊ฒฝ (ํ˜ธ๋ฒ„)" - driveFolderBg: "๋“œ๋ผ์ด๋ธŒ ํด๋” ๋ฐฐ๊ฒฝ" - wallpaperOverlay: "๋ฐฐ๊ฒฝํ™”๋ฉด ์˜ค๋ฒ„๋ ˆ์ด" - badge: "๋ฐฐ์ง€" - messageBg: "์ฑ„ํŒ… ๋ฐฐ๊ฒฝ" - accentDarken: "๊ฐ•์กฐ ์ƒ‰์ƒ (์–ด๋‘์›€)" - accentLighten: "๊ฐ•์กฐ ์ƒ‰์ƒ (๋ฐ์Œ)" - fgHighlighted: "๊ฐ•์กฐ๋œ ํ…์ŠคํŠธ" _sfx: note: "์ƒˆ ๋…ธํŠธ" noteMy: "๋‚ด ๋…ธํŠธ" @@ -1100,7 +935,8 @@ _tutorial: step4_1: "๋…ธํŠธ ์ž‘์„ฑ์„ ๋๋‚ด์…จ๋‚˜์š”?" step4_2: "๋‹น์‹ ์˜ ๋…ธํŠธ๊ฐ€ ํƒ€์ž„๋ผ์ธ์— ํ‘œ์‹œ๋˜์–ด ์žˆ๋‹ค๋ฉด ์„ฑ๊ณต์ž…๋‹ˆ๋‹ค." step5_1: "์ด์ œ, ๋‹ค๋ฅธ ์‚ฌ๋žŒ์„ ํŒ”๋กœ์šฐํ•˜์—ฌ ํƒ€์ž„๋ผ์ธ์„ ํ™œ๊ธฐ์ฐจ๊ฒŒ ๋งŒ๋“ค์–ด๋ณด๋„๋ก ํ•ฉ์‹œ๋‹ค." - step5_2: "{featured}์—์„œ ์ด ์ธ์Šคํ„ด์Šค์˜ ์ธ๊ธฐ ๋…ธํŠธ๋ฅผ ๋ณด์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. {explore}์—์„œ๋Š” ์ธ๊ธฐ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ๊ตฌ์š”. ๋งˆ์Œ์— ๋“œ๋Š” ์‚ฌ๋žŒ์„ ๊ณจ๋ผ ํŒ”๋กœ์šฐํ•ด ๋ณด์„ธ์š”!" + step5_2: "{featured}์—์„œ ์ด ์ธ์Šคํ„ด์Šค์˜ ์ธ๊ธฐ ๋…ธํŠธ๋ฅผ ๋ณด์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. {explore}์—์„œ๋Š” ์ธ๊ธฐ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ๊ตฌ์š”.\ + \ ๋งˆ์Œ์— ๋“œ๋Š” ์‚ฌ๋žŒ์„ ๊ณจ๋ผ ํŒ”๋กœ์šฐํ•ด ๋ณด์„ธ์š”!" step5_3: "๋‹ค๋ฅธ ์œ ์ €๋ฅผ ํŒ”๋กœ์šฐํ•˜๋ ค๋ฉด ํ•ด๋‹น ์œ ์ €์˜ ์•„์ด์ฝ˜์„ ํด๋ฆญํ•˜์—ฌ ํ”„๋กœํ•„ ํŽ˜์ด์ง€๋ฅผ ๋„์šด ํ›„, ํŒ”๋กœ์šฐ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์ฃผ์„ธ์š”." step5_4: "์‚ฌ์šฉ์ž์— ๋”ฐ๋ผ ํŒ”๋กœ์šฐ๊ฐ€ ์Šน์ธ๋  ๋•Œ๊นŒ์ง€ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." step6_1: "ํƒ€์ž„๋ผ์ธ์— ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์˜ ๋…ธํŠธ๊ฐ€ ๋‚˜ํƒ€๋‚œ๋‹ค๋ฉด ์„ฑ๊ณต์ž…๋‹ˆ๋‹ค." @@ -1108,7 +944,7 @@ _tutorial: step6_3: "๋ฆฌ์•ก์…˜์„ ๋ถ™์ด๋ ค๋ฉด, ๋…ธํŠธ์˜ \"+\" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๊ณ  ์›ํ•˜๋Š” ์ด๋ชจ์ง€๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค." step7_1: "์ด๊ฒƒ์œผ๋กœ FoundKey์˜ ๊ธฐ๋ณธ ํŠœํ† ๋ฆฌ์–ผ์„ ๋งˆ์น˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์ˆ˜๊ณ ํ•˜์…จ์Šต๋‹ˆ๋‹ค!" step7_2: "FoundKey์— ๋Œ€ํ•ด ๋” ์•Œ๊ณ  ์‹ถ์œผ์‹œ๋‹ค๋ฉด {help}๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”." - step7_3: "๊ทธ๋Ÿผ FoundKey๋ฅผ ์ฆ๊ธฐ์„ธ์š”! ๐Ÿš€" + step7_3: "๊ทธ๋Ÿผ FoundKey๋ฅผ ์ฆ๊ธฐ์„ธ์š”! \U0001F680" _2fa: alreadyRegistered: "์ด๋ฏธ ์„ค์ •์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." registerDevice: "๋””๋ฐ”์ด์Šค ๋“ฑ๋ก" @@ -1118,7 +954,8 @@ _2fa: step2Url: "๋ฐ์Šคํฌํ†ฑ ์•ฑ์—์„œ๋Š” ๋‹ค์Œ URL์„ ์ž…๋ ฅํ•˜์„ธ์š”:" step3: "์•ฑ์— ํ‘œ์‹œ๋œ ํ† ํฐ์„ ์ž…๋ ฅํ•˜์‹œ๋ฉด ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค." step4: "๋‹ค์Œ ๋กœ๊ทธ์ธ๋ถ€ํ„ฐ๋Š” ํ† ํฐ์„ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." - securityKeyInfo: "FIDO2๋ฅผ ์ง€์›ํ•˜๋Š” ํ•˜๋“œ์›จ์–ด ๋ณด์•ˆ ํ‚ค ํ˜น์€ ๋””๋ฐ”์ด์Šค์˜ ์ง€๋ฌธ์ธ์‹์ด๋‚˜ ํ™”๋ฉด์ž ๊ธˆ PIN์„ ์ด์šฉํ•ด์„œ ๋กœ๊ทธ์ธํ•˜๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." + securityKeyInfo: "FIDO2๋ฅผ ์ง€์›ํ•˜๋Š” ํ•˜๋“œ์›จ์–ด ๋ณด์•ˆ ํ‚ค ํ˜น์€ ๋””๋ฐ”์ด์Šค์˜ ์ง€๋ฌธ์ธ์‹์ด๋‚˜ ํ™”๋ฉด์ž ๊ธˆ PIN์„ ์ด์šฉํ•ด์„œ ๋กœ๊ทธ์ธํ•˜๋„๋ก ์„ค์ •ํ• \ + \ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." _permissions: "read:account": "๊ณ„์ •์˜ ์ •๋ณด๋ฅผ ๋ด…๋‹ˆ๋‹ค" "write:account": "๊ณ„์ •์˜ ์ •๋ณด๋ฅผ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค" @@ -1148,10 +985,6 @@ _permissions: "write:user-groups": "์œ ์ € ๊ทธ๋ฃน์„ ๋งŒ๋“ค๊ฑฐ๋‚˜, ์ดˆ๋Œ€ํ•˜๊ฑฐ๋‚˜, ์ด๋ฆ„์„ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜, ์–‘๋„ํ•˜๊ฑฐ๋‚˜, ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค" "read:channels": "์ฑ„๋„์„ ๋ณด๊ธฐ" "write:channels": "์ฑ„๋„์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค" - "read:gallery": "๊ฐค๋Ÿฌ๋ฆฌ๋ฅผ ๋ด…๋‹ˆ๋‹ค" - "write:gallery": "๊ฐค๋Ÿฌ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค" - "read:gallery-likes": "๊ฐค๋Ÿฌ๋ฆฌ์˜ ์ข‹์•„์š”๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค" - "write:gallery-likes": "๊ฐค๋Ÿฌ๋ฆฌ์— ์ข‹์•„์š”๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์ทจ์†Œํ•ฉ๋‹ˆ๋‹ค" _auth: shareAccess: "\"{name}\" ์ด ๊ณ„์ •์— ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์„ ํ—ˆ์šฉํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" shareAccessAsk: "์ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๊ณ„์ •์— ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์„ ํ—ˆ์šฉํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" @@ -1328,7 +1161,6 @@ _relayStatus: accepted: "์Šน์ธ๋จ" rejected: "๊ฑฐ์ ˆ๋จ" _notification: - fileUploaded: "ํŒŒ์ผ์ด ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" youGotMention: "{name}๋‹˜์ด ๋ฉ˜์…˜ํ•จ" youGotReply: "{name}๋‹˜์ด ๋‹ต๊ธ€ํ•จ" youGotQuote: "{name}๋‹˜์ด ์ธ์šฉํ•จ" @@ -1343,7 +1175,6 @@ _notification: pollEnded: "ํˆฌํ‘œ ๊ฒฐ๊ณผ๊ฐ€ ๋ฐœํ‘œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" emptyPushNotificationMessage: "ํ‘ธ์‹œ ์•Œ๋ฆผ์ด ๊ฐฑ์‹ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" _types: - all: "์ „๋ถ€" follow: "ํŒ”๋กœ์ž‰" mention: "๋ฉ˜์…˜" reply: "๋‹ต๊ธ€" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 83d71406c..a56ad8947 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -32,9 +32,6 @@ signup: "Registreren" save: "Opslaan" users: "Gebruikers" addUser: "Toevoegen gebruiker" -favorite: "Favorieten" -favorites: "Toevoegen aan favorieten" -unfavorite: "Verwijderen uit favorieten" pin: "Vastmaken aan profielpagina" unpin: "Losmaken van profielpagina" copyContent: "Kopiรซren inhoud" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 50af17e43..4b5d1eae9 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -33,9 +33,6 @@ signup: "Zarejestruj siฤ™" save: "Zapisz" users: "Uลผytkownicy" addUser: "Dodaj uลผytkownika" -favorite: "Dodaj do ulubionych" -favorites: "Ulubione" -unfavorite: "Usuล„ z ulubionych" pin: "Przypnij do profilu" unpin: "Odepnij z profilu" copyContent: "Skopiuj zawartoล›ฤ‡" @@ -604,7 +601,6 @@ disableShowingAnimatedImages: "Nie odtwarzaj animowanych obrazรณw" verificationEmailSent: "Wiadomoล›ฤ‡ weryfikacyjna zostaล‚a wysล‚ana. Odwiedลบ uwzglฤ™dniony\ \ odnoล›nik, aby ukoล„czyฤ‡ weryfikacjฤ™." emailVerified: "Adres e-mail zostaล‚ potwierdzony" -noteFavoritesCount: "Liczba polubionych wpisรณw" pageLikesCount: "Liczba otrzymanych polubieล„ stron" pageLikedCount: "Liczba polubionych stron" contact: "Kontakt" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 604e74b9f..4940370b5 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -32,9 +32,6 @@ signup: "Registrar-se" save: "Guardar" users: "Usuรกrios" addUser: "Adicionar usuรกrio" -favorite: "Favoritar" -favorites: "Favoritar" -unfavorite: "Remover dos favoritos" pin: "Afixar no perfil" unpin: "Desafixar do perfil" copyContent: "Copiar conteรบdos" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 9ac0445f0..0b5a6d9de 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -32,9 +32,6 @@ signup: "รŽnregistreazฤƒ-te" save: "Salveazฤƒ" users: "Utilizatori" addUser: "Adฤƒugฤƒ utilizator" -favorite: "Adaugฤƒ la favorite" -favorites: "Favorite" -unfavorite: "Eliminฤƒ din favorite" pin: "Fixeazฤƒ pe profil" unpin: "Anulati fixare" copyContent: "Copiazฤƒ conศ›inutul" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index dd20877dd..a9c16f7cd 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -32,9 +32,6 @@ signup: "ะ ะตะณะธัั‚ั€ะฐั†ะธั" save: "ะกะพั…ั€ะฐะฝะธั‚ัŒ" users: "ะŸะพะปัŒะทะพะฒะฐั‚ะตะปะธ" addUser: "ะ”ะพะฑะฐะฒะธั‚ัŒ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั" -favorite: "ะ’ ะธะทะฑั€ะฐะฝะฝะพะต" -favorites: "ะ˜ะทะฑั€ะฐะฝะฝะพะต" -unfavorite: "ะฃะฑั€ะฐั‚ัŒ ะธะท ะธะทะฑั€ะฐะฝะฝะพะณะพ" pin: "ะ—ะฐะบั€ะตะฟะธั‚ัŒ ะฒ ะฟั€ะพั„ะธะปะต" unpin: "ะžั‚ะบั€ะตะฟะธั‚ัŒ ะพั‚ ะฟั€ะพั„ะธะปั" copyContent: "ะกะบะพะฟะธั€ะพะฒะฐั‚ัŒ ัะพะดะตั€ะถะธะผะพะต" @@ -631,7 +628,6 @@ disableShowingAnimatedImages: "ะะต ะฟั€ะพะธะณั€ั‹ะฒะฐั‚ัŒ ะฐะฝะธะผะฐั†ะธัŽ" verificationEmailSent: "ะ’ะฐะผ ะพั‚ะฟั€ะฐะฒะปะตะฝะพ ะฟะธััŒะผะพ ะดะปั ะฟะพะดั‚ะฒะตั€ะถะดะตะฝะธั. ะŸั€ะพะนะดะธั‚ะต, ะฟะพะถะฐะปัƒะนัั‚ะฐ,\ \ ะฟะพ ััั‹ะปะบะต ะธะท ะฟะธััŒะผะฐ, ั‡ั‚ะพะฑั‹ ะทะฐะฒะตั€ัˆะธั‚ัŒ ะฟั€ะพะฒะตั€ะบัƒ." emailVerified: "ะะดั€ะตั ัะปะตะบั‚ั€ะพะฝะฝะพะน ะฟะพั‡ั‚ั‹ ะฟะพะดั‚ะฒะตั€ะถะดั‘ะฝ." -noteFavoritesCount: "ะšะพะปะธั‡ะตัั‚ะฒะพ ะดะพะฑะฐะฒะปะตะฝะฝะพะณะพ ะฒ ะธะทะฑั€ะฐะฝะฝะพะต" pageLikesCount: "ะšะพะปะธั‡ะตัั‚ะฒะพ ะฟะพะฝั€ะฐะฒะธะฒัˆะธั…ัั ัั‚ั€ะฐะฝะธั†" pageLikedCount: "ะšะพะปะธั‡ะตัั‚ะฒะพ ัั‚ั€ะฐะฝะธั†, ะฟะพะฝั€ะฐะฒะธะฒัˆะธั…ัั ะดั€ัƒะณะธะผ" contact: "ะšะฐะบ ัะฒัะทะฐั‚ัŒัั" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index b5f4d0059..729927812 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -32,9 +32,6 @@ signup: "Registrovaลฅ" save: "Uloลพiลฅ" users: "Pouลพรญvatelia" addUser: "Pridaลฅ pouลพรญvateฤพa" -favorite: "Pรกฤi sa mi" -favorites: "Obฤพรบbenรฉ" -unfavorite: "Nepรกฤi sa mi" pin: "Pripnรบลฅ" unpin: "Odopnรบลฅ" copyContent: "Kopรญrovaลฅ obsah" @@ -623,7 +620,6 @@ disableShowingAnimatedImages: "Neprehrรกvaลฅ animovanรฉ obrรกzky" verificationEmailSent: "Odoslali sme overovacรญ email. Overenie dokonฤรญte kliknutรญm\ \ na odkaz v emaili." emailVerified: "Email overenรฝ" -noteFavoritesCount: "Poฤet obฤพรบbenรฝch poznรกmok" pageLikesCount: "Poฤet obฤพรบbenรฝch strรกnok" pageLikedCount: "Poฤet prijatรฝch \"pรกฤi sa mi\"" contact: "Kontakt" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index 2965e968a..90bfa58ef 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -32,9 +32,6 @@ signup: "Registrera" save: "Spara" users: "Anvรคndare" addUser: "Lรคgg till anvรคndare" -favorite: "Lรคgg till i favoriter" -favorites: "Favoriter" -unfavorite: "Avfavorisera" pin: "Fรคst till profil" unpin: "Lossa frรฅn profil" copyContent: "Kopiera innehรฅll" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 12bd05821..0d1df8c5d 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -28,9 +28,6 @@ logout: "ร‡ฤฑkฤฑลŸ Yap" signup: "Kayฤฑt Ol" users: "Kullanฤฑcฤฑ" addUser: "Kullanฤฑcฤฑ Ekle" -favorite: "Favoriler" -favorites: "Favoriler" -unfavorite: "Favorilerden Kaldฤฑr" pin: "SabitlenmiลŸ" unpin: "Sabitlemeyi kaldฤฑr" copyContent: "ฤฐรงeriฤŸi kopyala" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index b53de0c81..17b338818 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -32,9 +32,6 @@ signup: "ะ ะตั”ัั‚ั€ะฐั†ั–ั" save: "ะ—ะฑะตั€ะตะณั‚ะธ" users: "ะšะพั€ะธัั‚ัƒะฒะฐั‡ั–" addUser: "ะ”ะพะดะฐั‚ะธ ะบะพั€ะธัั‚ัƒะฒะฐั‡ะฐ" -favorite: "ะžะฑั€ะฐะฝะต" -favorites: "ะžะฑั€ะฐะฝะต" -unfavorite: "ะ’ะธะดะฐะปะธั‚ะธ ะท ะพะฑั€ะฐะฝะพะณะพ" pin: "ะ—ะฐะบั€ั–ะฟะธั‚ะธ" unpin: "ะ’ั–ะดะบั€ั–ะฟะธั‚ะธ" copyContent: "ะกะบะพะฟั–ัŽะฒะฐั‚ะธ ะบะพะฝั‚ะตะฝั‚" @@ -631,7 +628,6 @@ disableShowingAnimatedImages: "ะะต ะฟั€ะพะณั€ะฐะฒะฐั‚ะธ ะฐะฝั–ะผะพะฒะฐะฝั– ะทะพ verificationEmailSent: "ะ•ะปะตะบั‚ั€ะพะฝะฝะธะน ะปะธัั‚ ะท ะฟั–ะดั‚ะฒะตั€ะดะถะตะฝะฝัะผ ะฒั–ะดั–ัะปะฐะฝะธะน. ะ‘ัƒะดัŒ ะปะฐัะบะฐ ะฟะตั€ะตะนะดั–ั‚ัŒ\ \ ะฟะพ ะฟะพัะธะปะฐะฝะฝัŽ ะฒ ะปะธัั‚ั– ะดะปั ะฟั–ะดั‚ะฒะตั€ะดะถะตะฝะฝั." emailVerified: "ะ•ะปะตะบั‚ั€ะพะฝะฝัƒ ะฟะพัˆั‚ัƒ ะฟั–ะดั‚ะฒะตั€ะดะถะตะฝะพ." -noteFavoritesCount: "ะšั–ะปัŒะบั–ัั‚ัŒ ัƒะปัŽะฑะปะตะฝะธั… ะฝะพั‚ะฐั‚ะพะบ" pageLikesCount: "ะšั–ะปัŒะบั–ัั‚ัŒ ะพั‚ั€ะธะผะฐะฝะธั… ะฒะฟะพะดะพะฑะฐะฝัŒ ัั‚ะพั€ั–ะฝะบะธ" pageLikedCount: "ะšั–ะปัŒะบั–ัั‚ัŒ ะฒะฟะพะดะพะฑะฐะฝะธั… ัั‚ะพั€ั–ะฝะพะบ" contact: "ะšะพะฝั‚ะฐะบั‚" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 2a9bcab74..cc21255ad 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -32,9 +32,6 @@ signup: "ฤฤƒng kรฝ" save: "Lฦฐu" users: "Ngฦฐแปi dรนng" addUser: "Thรชm ngฦฐแปi dรนng" -favorite: "Thรชm vร o yรชu thรญch" -favorites: "Lฦฐแปฃt thรญch" -unfavorite: "Bแป thรญch" pin: "Ghim" unpin: "Bแป ghim" copyContent: "Chรฉp nแป™i dung" @@ -628,7 +625,6 @@ disableShowingAnimatedImages: "Khรดng phรกt แบฃnh ฤ‘แป™ng" verificationEmailSent: "Mแป™t email xรกc minh ฤ‘รฃ ฤ‘ฦฐแปฃc gแปญi. Vui lรฒng nhแบฅn vร o liรชn kแบฟt\ \ ฤ‘รญnh kรจm ฤ‘แปƒ hoร n tแบฅt xรกc minh." emailVerified: "Email ฤ‘รฃ ฤ‘ฦฐแปฃc xรกc minh" -noteFavoritesCount: "Sแป‘ lฦฐแปฃng tรบt yรชu thรญch" pageLikesCount: "Sแป‘ lฦฐแปฃng trang ฤ‘รฃ thรญch" pageLikedCount: "Sแป‘ lฦฐแปฃng thรญch trang ฤ‘รฃ nhแบญn" contact: "Liรชn hแป‡" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index df3c768af..acb6dc301 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -30,9 +30,6 @@ signup: "ๆ–ฐ็”จๆˆทๆณจๅ†Œ" save: "ไฟๅญ˜" users: "็”จๆˆท" addUser: "ๆทปๅŠ ็”จๆˆท" -favorite: "ๆ”ถ่—" -favorites: "ๆ”ถ่—" -unfavorite: "ๅ–ๆถˆๆ”ถ่—" pin: "็ฝฎ้กถ" unpin: "ๅ–ๆถˆ็ฝฎ้กถ" copyContent: "ๅคๅˆถๅ†…ๅฎน" @@ -583,7 +580,6 @@ loadRawImages: "ๆทปๅŠ ้™„ไปถๅ›พๅƒ็š„็ผฉ็•ฅๅ›พๆ—ถไฝฟ็”จๅŽŸๅง‹ๅ›พๅƒ่ดจ้‡" disableShowingAnimatedImages: "ไธๆ’ญๆ”พๅŠจ็”ป" verificationEmailSent: "ๅทฒๅ‘้€็กฎ่ฎค็”ตๅญ้‚ฎไปถใ€‚่ฏท่ฎฟ้—ฎ็”ตๅญ้‚ฎไปถไธญ็š„้“พๆŽฅไปฅๅฎŒๆˆ่ฎพ็ฝฎใ€‚" emailVerified: "็”ตๅญ้‚ฎไปถๅœฐๅ€ๅทฒ้ชŒ่ฏ" -noteFavoritesCount: "ๆ”ถ่—็š„ๅธ–ๅญๆ•ฐ" pageLikesCount: "้กต้ข็‚น่ตžๆฌกๆ•ฐ" pageLikedCount: "้กต้ข่ขซ็‚น่ตžๆฌกๆ•ฐ" contact: "่”็ณปไบบ" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 706d4a777..0d3ae4629 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -30,9 +30,6 @@ signup: "่จปๅ†Š" save: "ๅ„ฒๅญ˜" users: "ไฝฟ็”จ่€…" addUser: "ๆ–ฐๅขžไฝฟ็”จ่€…" -favorite: "ๆˆ‘็š„ๆœ€ๆ„›" -favorites: "ๆˆ‘็š„ๆœ€ๆ„›" -unfavorite: "ๅพžๆˆ‘็š„ๆœ€ๆ„›ไธญ็งป้™ค" pin: "็ฝฎ้ ‚" unpin: "ๅ–ๆถˆ็ฝฎ้ ‚" copyContent: "่ค‡่ฃฝๅ…งๅฎน" @@ -582,7 +579,6 @@ loadRawImages: "ไปฅๅŽŸๅง‹ๅœ–ๆช”้กฏ็คบ้™„ไปถๅœ–ๆช”็š„็ธฎๅœ–" disableShowingAnimatedImages: "ไธๆ’ญๆ”พๅ‹•ๆ…‹ๅœ–ๆช”" verificationEmailSent: "ๅทฒ็™ผ้€้ฉ—่ญ‰้›ปๅญ้ƒตไปถใ€‚่ซ‹้ปžๆ“Š้€ฒๅ…ฅ้›ปๅญ้ƒตไปถไธญ็š„้ˆๆŽฅๅฎŒๆˆ้ฉ—่ญ‰ใ€‚" emailVerified: "ๅทฒๆˆๅŠŸ้ฉ—่ญ‰ๆ‚จ็š„้›ป้ƒต" -noteFavoritesCount: "ๆˆ‘็š„ๆœ€ๆ„›่ฒผๆ–‡็š„ๆ•ธ็›ฎ" pageLikesCount: "้ ้ข่ขซๆŒ‰่ฎšๆฌกๆ•ธ" pageLikedCount: "้ ้ข่ขซๆŒ‰่ฎšๆฌกๆ•ธ" contact: "่ฏ็ตกไบบ" diff --git a/package.json b/package.json index f83572c9a..831964a0f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "foundkey", - "version": "13.0.0-preview4", + "version": "13.0.0-preview5", "repository": { "type": "git", "url": "https://akkoma.dev/FoundKeyGang/FoundKey.git" diff --git a/packages/backend/migration/1673201544000-deletion-progress.js b/packages/backend/migration/1673201544000-deletion-progress.js new file mode 100644 index 000000000..90aa5cbf8 --- /dev/null +++ b/packages/backend/migration/1673201544000-deletion-progress.js @@ -0,0 +1,18 @@ +export class deletionProgress1673201544000 { + name = 'deletionProgress1673201544000'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" RENAME COLUMN "isDeleted" TO "isDeletedOld"`); + await queryRunner.query(`ALTER TABLE "user" ADD "isDeleted" integer`); + await queryRunner.query(`UPDATE "user" SET "isDeleted" = CASE WHEN "host" IS NULL THEN -1 ELSE 0 END WHERE "isDeletedOld"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isDeletedOld"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" RENAME COLUMN "isDeleted" TO "isDeletedOld"`); + await queryRunner.query(`ALTER TABLE "user" ADD "isDeleted" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`UPDATE "user" SET "isDeleted" = "isDeletedOld" IS NOT NULL`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isDeletedOld"`); + } +} + diff --git a/packages/backend/migration/1684536337602-ffVisibilityNobody.js b/packages/backend/migration/1684536337602-ffVisibilityNobody.js new file mode 100644 index 000000000..8998e7d24 --- /dev/null +++ b/packages/backend/migration/1684536337602-ffVisibilityNobody.js @@ -0,0 +1,21 @@ +export class ffVisibilityNobody1684536337602 { + name = 'ffVisibilityNobody1684536337602'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TYPE "public"."user_profile_ffvisibility_enum" RENAME TO "user_profile_ffvisibility_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum" AS ENUM('public', 'followers', 'private', 'nobody')`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "ffVisibility" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "ffVisibility" TYPE "public"."user_profile_ffvisibility_enum" USING "ffVisibility"::"text"::"public"."user_profile_ffvisibility_enum"`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "ffVisibility" SET DEFAULT 'public'`); + await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum_old"`); + } + + async down(queryRunner) { + await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum_old" AS ENUM('public', 'followers', 'private')`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "ffVisibility" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "ffVisibility" TYPE "public"."user_profile_ffvisibility_enum_old" USING "ffVisibility"::"text"::"public"."user_profile_ffvisibility_enum_old"`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "ffVisibility" SET DEFAULT 'public'`); + await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum"`); + await queryRunner.query(`ALTER TYPE "public"."user_profile_ffvisibility_enum_old" RENAME TO "user_profile_ffvisibility_enum"`); + } +} diff --git a/packages/backend/migration/1685126322423-remove-favourites.js b/packages/backend/migration/1685126322423-remove-favourites.js new file mode 100644 index 000000000..57a31e126 --- /dev/null +++ b/packages/backend/migration/1685126322423-remove-favourites.js @@ -0,0 +1,33 @@ +export class removeFavourites1685126322423 { + name = 'removeFavourites1685126322423'; + + async up(queryRunner) { + await queryRunner.query(` + WITH "new_clips" AS ( + INSERT INTO "clip" ("id", "createdAt", "userId", "name") + SELECT + LEFT(MD5(RANDOM()::text), 10), + NOW(), + "userId", + 'โญ' + FROM "note_favorite" + GROUP BY "userId" + RETURNING "id", "userId" + ) + INSERT INTO "clip_note" ("id", "noteId", "clipId") + SELECT + "note_favorite"."id", + "noteId", + "new_clips"."id" + FROM "note_favorite" + JOIN "new_clips" ON "note_favorite"."userId" = "new_clips"."userId" + `); + await queryRunner.query(`DROP TABLE "note_favorite"`); + } + + async down(queryRunner) { + // can't revert the migration to clips, can only recreate the database table + await queryRunner.query(`CREATE TABLE "note_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_af0da35a60b9fa4463a62082b36" PRIMARY KEY ("id"))`); + } +} + diff --git a/packages/backend/package.json b/packages/backend/package.json index 5d7df7cfa..097f6ee73 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "backend", - "version": "13.0.0-preview4", + "version": "13.0.0-preview5", "main": "./index.js", "private": true, "type": "module", @@ -67,7 +67,7 @@ "koa-send": "5.0.1", "koa-slow": "2.1.0", "koa-views": "7.0.2", - "mfm-js": "0.22.1", + "mfm-js": "0.23.3", "mime-types": "2.1.35", "mocha": "10.2.0", "multer": "1.4.5-lts.1", @@ -100,7 +100,6 @@ "stringz": "2.1.0", "style-loader": "3.3.1", "summaly": "2.7.0", - "syslog-pro": "1.0.0", "systeminformation": "5.11.22", "tinycolor2": "1.4.2", "tmp": "0.2.1", @@ -158,7 +157,6 @@ "@types/sinon": "^10.0.13", "@types/sinonjs__fake-timers": "8.1.2", "@types/speakeasy": "2.0.7", - "@types/syslog-pro": "^1.0.0", "@types/tinycolor2": "1.4.3", "@types/tmp": "0.2.3", "@types/uuid": "8.3.4", diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/index.ts index 46dc2d258..2580920f1 100644 --- a/packages/backend/src/boot/index.ts +++ b/packages/backend/src/boot/index.ts @@ -3,7 +3,7 @@ import chalk from 'chalk'; import Xev from 'xev'; import Logger from '@/services/logger.js'; -import { envOption } from '@/env.js'; +import { envOption, LOG_LEVELS } from '@/env.js'; // for typeorm import 'reflect-metadata'; @@ -66,7 +66,7 @@ cluster.on('exit', worker => { }); // Display detail of unhandled promise rejection -if (!envOption.quiet) { +if (envOption.logLevel !== LOG_LEVELS.quiet) { process.on('unhandledRejection', console.dir); } diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 6f64715f8..c53dc12b1 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -8,10 +8,10 @@ import chalkTemplate from 'chalk-template'; import semver from 'semver'; import Logger from '@/services/logger.js'; -import loadConfig from '@/config/load.js'; +import { loadConfig } from '@/config/load.js'; import { Config } from '@/config/types.js'; import { showMachineInfo } from '@/misc/show-machine-info.js'; -import { envOption } from '@/env.js'; +import { envOption, LOG_LEVELS } from '@/env.js'; import { db, initDb } from '@/db/postgre.js'; const _filename = fileURLToPath(import.meta.url); @@ -25,7 +25,7 @@ const bootLogger = logger.createSubLogger('boot', 'magenta', false); const themeColor = chalk.hex('#86b300'); function greet(): void { - if (!envOption.quiet) { + if (envOption.logLevel !== LOG_LEVELS.quiet) { //#region FoundKey logo console.log(themeColor(' ___ _ _ __ ')); console.log(themeColor(' | __|__ _ _ _ _ __| | |/ /___ _ _ ')); @@ -41,7 +41,7 @@ function greet(): void { } bootLogger.info('Welcome to FoundKey!'); - bootLogger.info(`FoundKey v${meta.version}`, null, true); + bootLogger.info(`FoundKey v${meta.version}`, true); } /** @@ -59,7 +59,7 @@ export async function masterMain(): Promise { config = loadConfigBoot(); await connectDb(); } catch (e) { - bootLogger.error('Fatal error occurred during initialization', {}, true); + bootLogger.error('Fatal error occurred during initialization', true); process.exit(1); } @@ -69,7 +69,7 @@ export async function masterMain(): Promise { await spawnWorkers(config.clusterLimits); } - bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); + bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, true); if (!envOption.noDaemons) { import('../daemons/server-stats.js').then(x => x.serverStats()); @@ -84,7 +84,7 @@ function showEnvironment(): void { if (env !== 'production') { logger.warn('The environment is not in production mode.'); - logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', {}, true); + logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', true); } } @@ -109,7 +109,7 @@ function loadConfigBoot(): Config { } catch (exception) { const e = exception as Partial | Error; if ('code' in e && e.code === 'ENOENT') { - configLogger.error('Configuration file not found', {}, true); + configLogger.error('Configuration file not found', true); process.exit(1); } else if (e instanceof Error) { configLogger.error(e.message); @@ -133,7 +133,7 @@ async function connectDb(): Promise { const v = await db.query('SHOW server_version').then(x => x[0].server_version); dbLogger.succ(`Connected: v${v}`); } catch (e) { - dbLogger.error('Cannot connect', {}, true); + dbLogger.error('Cannot connect', true); dbLogger.error(e as Error | string); process.exit(1); } @@ -141,15 +141,24 @@ async function connectDb(): Promise { async function spawnWorkers(clusterLimits: Required): Promise { const modes = ['web' as const, 'queue' as const]; + + const clusters = structuredClone(clusterLimits); + + if (envOption.onlyQueue) { + clusters.web = 0; + } else if (envOption.onlyServer) { + clusters.queue = 0; + } + const cpus = os.cpus().length; - for (const mode of modes.filter(mode => clusterLimits[mode] > cpus)) { + for (const mode of modes.filter(mode => clusters[mode] > cpus)) { bootLogger.warn(`configuration warning: cluster limit for ${mode} exceeds number of cores (${cpus})`); } - const total = modes.reduce((acc, mode) => acc + clusterLimits[mode], 0); + const total = modes.reduce((acc, mode) => acc + clusters[mode], 0); const workers = new Array(total); - workers.fill('web', 0, clusterLimits.web); - workers.fill('queue', clusterLimits.web); + workers.fill('web', 0, clusters.web); + workers.fill('queue', clusters.web); bootLogger.info(`Starting ${total} workers...`); await Promise.all(workers.map(mode => spawnWorker(mode))); diff --git a/packages/backend/src/config/index.ts b/packages/backend/src/config/index.ts index 3e53b0003..6b407f269 100644 --- a/packages/backend/src/config/index.ts +++ b/packages/backend/src/config/index.ts @@ -1,3 +1,3 @@ -import load from './load.js'; +import { loadConfig } from './load.js'; -export default load(); +export default loadConfig(); diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts index a6431162d..f16fb850b 100644 --- a/packages/backend/src/config/load.ts +++ b/packages/backend/src/config/load.ts @@ -23,7 +23,7 @@ const path = process.env.NODE_ENV === 'test' ? `${dir}/test.yml` : `${dir}/default.yml`; -export default function load(): Config { +export function loadConfig(): Config { const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); const clientManifest = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/_client_dist_/manifest.json`, 'utf-8')); let config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts index 55226ca47..686a8c242 100644 --- a/packages/backend/src/config/types.ts +++ b/packages/backend/src/config/types.ts @@ -59,11 +59,6 @@ export type Source = { deliverJobMaxAttempts?: number; inboxJobMaxAttempts?: number; - syslog?: { - host: string; - port: number; - }; - mediaProxy?: string; proxyRemoteFiles?: boolean; internalStoragePath?: string; diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index b59142597..d8c31ae96 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -33,7 +33,6 @@ import { UserGroup } from '@/models/entities/user-group.js'; import { UserGroupJoining } from '@/models/entities/user-group-joining.js'; import { UserGroupInvitation } from '@/models/entities/user-group-invitation.js'; import { Hashtag } from '@/models/entities/hashtag.js'; -import { NoteFavorite } from '@/models/entities/note-favorite.js'; import { AbuseUserReport } from '@/models/entities/abuse-user-report.js'; import { RegistrationTicket } from '@/models/entities/registration-tickets.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js'; @@ -134,7 +133,6 @@ export const entities = [ RenoteMuting, Blocking, Note, - NoteFavorite, NoteReaction, NoteWatching, NoteThreadMuting, diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts index 1b678edc4..e89b83567 100644 --- a/packages/backend/src/env.ts +++ b/packages/backend/src/env.ts @@ -1,20 +1,38 @@ -const envOption = { +export const LOG_LEVELS = { + quiet: 6, + error: 5, + warning: 4, + success: 3, + info: 2, + debug: 1, +}; + +export const envOption = { onlyQueue: false, onlyServer: false, noDaemons: false, disableClustering: false, - verbose: false, withLogTime: false, - quiet: false, slow: false, + logLevel: LOG_LEVELS.info, }; for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) { - if (process.env['MK_' + key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()]) envOption[key] = true; + const value = process.env['FK_' + key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()]; + if (value) { + if (key === 'logLevel') { + if (value.toLowerCase() in LOG_LEVELS) { + envOption.logLevel = LOG_LEVELS[value.toLowerCase()]; + } + console.log('Unknown log level ' + JSON.stringify(value.toLowerCase()) + ', defaulting to "info"'); + } else { + envOption[key] = true; + } + } } -if (process.env.NODE_ENV === 'test') envOption.disableClustering = true; -if (process.env.NODE_ENV === 'test') envOption.quiet = true; -if (process.env.NODE_ENV === 'test') envOption.noDaemons = true; - -export { envOption }; +if (process.env.NODE_ENV === 'test') { + envOption.disableClustering = true; + envOption.logLevel = LOG_LEVELS.quiet; + envOption.noDaemons = true; +} diff --git a/packages/backend/src/misc/api-permissions.ts b/packages/backend/src/misc/api-permissions.ts index d7c115a50..17ae0d99d 100644 --- a/packages/backend/src/misc/api-permissions.ts +++ b/packages/backend/src/misc/api-permissions.ts @@ -5,8 +5,6 @@ export const kinds = [ 'write:blocks', 'read:drive', 'write:drive', - 'read:favorites', - 'write:favorites', 'read:following', 'write:following', 'read:messaging', diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts index 1bb07d14e..6b5fcf0f5 100644 --- a/packages/backend/src/misc/schema.ts +++ b/packages/backend/src/misc/schema.ts @@ -23,7 +23,6 @@ import { packedHashtagSchema } from '@/models/schema/hashtag.js'; import { packedPageSchema } from '@/models/schema/page.js'; import { packedUserGroupSchema } from '@/models/schema/user-group.js'; import { packedUserGroupInvitationSchema } from '@/models/schema/user-group-invitation.js'; -import { packedNoteFavoriteSchema } from '@/models/schema/note-favorite.js'; import { packedChannelSchema } from '@/models/schema/channel.js'; import { packedAntennaSchema } from '@/models/schema/antenna.js'; import { packedClipSchema } from '@/models/schema/clip.js'; @@ -47,7 +46,6 @@ export const refs = { MessagingMessage: packedMessagingMessageSchema, Note: packedNoteSchema, NoteReaction: packedNoteReactionSchema, - NoteFavorite: packedNoteFavoriteSchema, Notification: packedNotificationSchema, DriveFile: packedDriveFileSchema, DriveFolder: packedDriveFolderSchema, diff --git a/packages/backend/src/models/entities/note-favorite.ts b/packages/backend/src/models/entities/note-favorite.ts deleted file mode 100644 index 8b4449c3e..000000000 --- a/packages/backend/src/models/entities/note-favorite.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './note.js'; -import { User } from './user.js'; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class NoteFavorite { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the NoteFavorite.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(() => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; -} diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts index 6cfca187a..f2d53a984 100644 --- a/packages/backend/src/models/entities/user.ts +++ b/packages/backend/src/models/entities/user.ts @@ -163,11 +163,11 @@ export class User { // Indicates the user was deleted by an admin. // The users' data is not deleted from the database to keep them from reappearing. // A hard delete of the record may follow if we receive a matching Delete activity. - @Column('boolean', { - default: false, - comment: 'Whether the User is deleted.', + @Column('integer', { + nullable: true, + comment: 'How many delivery jobs are outstanding before the deletion is completed.', }) - public isDeleted: boolean; + public isDeleted: number | null; @Column('varchar', { length: 128, array: true, default: '{}', @@ -260,9 +260,3 @@ export interface IRemoteUser extends User { host: string; token: null; } - -export type CacheableLocalUser = ILocalUser; - -export type CacheableRemoteUser = IRemoteUser; - -export type CacheableUser = CacheableLocalUser | CacheableRemoteUser; diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index f4aa84d5b..7ba383c4a 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -29,7 +29,6 @@ import { RenoteMutingRepository } from './repositories/renote-muting.js'; import { BlockingRepository } from './repositories/blocking.js'; import { NoteReactionRepository } from './repositories/note-reaction.js'; import { NotificationRepository } from './repositories/notification.js'; -import { NoteFavoriteRepository } from './repositories/note-favorite.js'; import { UserPublickey } from './entities/user-publickey.js'; import { UserKeypair } from './entities/user-keypair.js'; import { AppRepository } from './repositories/app.js'; @@ -64,7 +63,6 @@ export const Announcements = db.getRepository(Announcement); export const AnnouncementReads = db.getRepository(AnnouncementRead); export const Apps = (AppRepository); export const Notes = (NoteRepository); -export const NoteFavorites = (NoteFavoriteRepository); export const NoteWatchings = db.getRepository(NoteWatching); export const NoteThreadMutings = db.getRepository(NoteThreadMuting); export const NoteReactions = (NoteReactionRepository); diff --git a/packages/backend/src/models/repositories/note-favorite.ts b/packages/backend/src/models/repositories/note-favorite.ts deleted file mode 100644 index 47d549455..000000000 --- a/packages/backend/src/models/repositories/note-favorite.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { NoteFavorite } from '@/models/entities/note-favorite.js'; -import { User } from '@/models/entities/user.js'; -import { Notes } from '../index.js'; - -export const NoteFavoriteRepository = db.getRepository(NoteFavorite).extend({ - async pack( - src: NoteFavorite['id'] | NoteFavorite, - me?: { id: User['id'] } | null | undefined, - ) { - const favorite = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return { - id: favorite.id, - createdAt: favorite.createdAt.toISOString(), - noteId: favorite.noteId, - // may throw error - note: await Notes.pack(favorite.note || favorite.noteId, me), - }; - }, - - packMany( - favorites: any[], - me: { id: User['id'] }, - ) { - return Promise.allSettled(favorites.map(x => this.pack(x, me))) - .then(promises => promises.flatMap(result => result.status === 'fulfilled' ? [result.value] : [])); - }, -}); diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 10969a034..d8772c97d 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -230,6 +230,36 @@ export const UserRepository = db.getRepository(User).extend({ return `${config.url}/identicon/${userId}`; }, + /** + * Determines whether the followers/following of user `user` are visibile to user `me`. + */ + async areFollowersVisibleTo(user: User, me: { id: User['id'] } | null | undefined): Promise { + const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); + + switch (profile.ffVisibility) { + case 'public': + return true; + case 'followers': + if (me == null) { + return false; + } else if (me.id === user.id) { + return true; + } else { + return await Followings.count({ + where: { + followerId: me.id, + followeeId: user.id, + }, + take: 1, + }).then(n => n > 0); + } + case 'private': + return me?.id === user.id; + case 'nobody': + return false; + } + }, + async pack( src: User['id'] | User, me?: { id: User['id'] } | null | undefined, @@ -270,15 +300,13 @@ export const UserRepository = db.getRepository(User).extend({ .getMany() : []; const profile = opts.detail ? await UserProfiles.findOneByOrFail({ userId: user.id }) : null; - const followingCount = profile == null ? null : - (profile.ffVisibility === 'public') || isMe ? user.followingCount : - (profile.ffVisibility === 'followers') && relation?.isFollowing ? user.followingCount : - null; + const ffVisible = await this.areFollowersVisibleTo(user, me); - const followersCount = profile == null ? null : - (profile.ffVisibility === 'public') || isMe ? user.followersCount : - (profile.ffVisibility === 'followers') && relation?.isFollowing ? user.followersCount : - null; + const followingCount = !opts.detail ? null : + ffVisible ? user.followingCount : null; + + const followersCount = !opts.detail ? null : + ffVisible ? user.followersCount : null; const packed = { id: user.id, @@ -353,7 +381,7 @@ export const UserRepository = db.getRepository(User).extend({ autoAcceptFollowed: profile!.autoAcceptFollowed, noCrawle: profile!.noCrawle, isExplorable: user.isExplorable, - isDeleted: user.isDeleted, + isDeleted: user.isDeleted != null, hideOnlineStatus: user.hideOnlineStatus, hasUnreadSpecifiedNotes: NoteUnreads.count({ where: { userId: user.id, isSpecified: true }, diff --git a/packages/backend/src/models/schema/note-favorite.ts b/packages/backend/src/models/schema/note-favorite.ts deleted file mode 100644 index d133f7367..000000000 --- a/packages/backend/src/models/schema/note-favorite.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const packedNoteFavoriteSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - createdAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - note: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - noteId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - }, -} as const; diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index cc125a3de..182202260 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -1,11 +1,12 @@ import httpSignature from '@peertube/http-signature'; import { v4 as uuid } from 'uuid'; +import Bull from 'bull'; import config from '@/config/index.js'; +import { Users } from '@/models/index.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js'; import { IActivity } from '@/remote/activitypub/type.js'; -import { envOption } from '@/env.js'; import { MINUTE } from '@/const.js'; import processDeliver from './processors/deliver.js'; @@ -18,7 +19,7 @@ import { endedPollNotification } from './processors/ended-poll-notification.js'; import { queueLogger } from './logger.js'; import { getJobInfo } from './get-job-info.js'; import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js'; -import { ThinUser } from './types.js'; +import { DeliverJobData, ThinUser } from './types.js'; function renderError(e: Error): any { return { @@ -35,44 +36,56 @@ const inboxLogger = queueLogger.createSubLogger('inbox'); const dbLogger = queueLogger.createSubLogger('db'); const objectStorageLogger = queueLogger.createSubLogger('objectStorage'); +async function deletionRefCount(job: Bull.Job): Promise { + if (job.data.deletingUserId) { + await Users.decrement({ id: job.data.deletingUserId }, 'isDeleted', 1); + } +} + systemQueue .on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => systemLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`)) + .on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`)) .on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`)); deliverQueue .on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) - .on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('completed', async (job, result) => { + deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`); + await deletionRefCount(job); + }) + .on('failed', async (job, err) => { + deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`); + await deletionRefCount(job); + }) + .on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`)) .on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); inboxQueue .on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) - .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`)) + .on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`)) .on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`)); dbQueue .on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => dbLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`)) + .on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`)) .on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`)); objectStorageQueue .on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`)) + .on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`)) .on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`)); webhookDeliverQueue @@ -80,10 +93,10 @@ webhookDeliverQueue .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) - .on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`)) .on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); -export function deliver(user: ThinUser, content: unknown, to: string | null) { +export function deliver(user: ThinUser, content: unknown, to: string | null, deletingUserId?: string) { if (content == null) return null; if (to == null) return null; @@ -93,6 +106,7 @@ export function deliver(user: ThinUser, content: unknown, to: string | null) { }, content, to, + deletingUserId, }; return deliverQueue.add(data, { @@ -289,8 +303,6 @@ export function webhookDeliver(webhook: Webhook, type: typeof webhookEventTypes[ } export default function() { - if (envOption.onlyServer) return; - deliverQueue.process(config.deliverJobConcurrency, processDeliver); inboxQueue.process(config.inboxJobConcurrency, processInbox); endedPollNotificationQueue.process(endedPollNotification); @@ -326,8 +338,9 @@ export default function() { } export function destroy() { - deliverQueue.once('cleaned', (jobs, status) => { + deliverQueue.once('cleaned', async (jobs, status) => { deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); + await Promise.all(jobs.map(job => deletionRefCount(job))); }); deliverQueue.clean(0, 'delayed'); diff --git a/packages/backend/src/queue/processors/db/delete-account.ts b/packages/backend/src/queue/processors/db/delete-account.ts index 84e28f25d..0bdf8c5af 100644 --- a/packages/backend/src/queue/processors/db/delete-account.ts +++ b/packages/backend/src/queue/processors/db/delete-account.ts @@ -46,29 +46,17 @@ export async function deleteAccount(job: Bull.Job): Promise } { // Delete files - let cursor: DriveFile['id'] | null = null; + const files = await DriveFiles.find({ + where: { + userId: user.id, + }, + order: { + id: 1, + }, + }) as DriveFile[]; - while (true) { - const files = await DriveFiles.find({ - where: { - userId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 10, - order: { - id: 1, - }, - }) as DriveFile[]; - - if (files.length === 0) { - break; - } - - cursor = files[files.length - 1].id; - - for (const file of files) { - await deleteFileSync(file); - } + for (const file of files) { + await deleteFileSync(file); } logger.succ('All of files deleted'); diff --git a/packages/backend/src/queue/processors/db/export-custom-emojis.ts b/packages/backend/src/queue/processors/db/export-custom-emojis.ts index f31531db4..9aa503e43 100644 --- a/packages/backend/src/queue/processors/db/export-custom-emojis.ts +++ b/packages/backend/src/queue/processors/db/export-custom-emojis.ts @@ -71,7 +71,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi try { await downloadUrl(emoji.originalUrl, emojiPath); downloaded = true; - } catch (e) { // TODO: ไฝ•ๅบฆใ‹ๅ†่ฉฆ่กŒ + } catch (e) { // TODO: retry logger.error(e instanceof Error ? e : new Error(e as string)); } diff --git a/packages/backend/src/queue/processors/system/check-expired.ts b/packages/backend/src/queue/processors/system/check-expired.ts index eeb6149bb..71bd498e9 100644 --- a/packages/backend/src/queue/processors/system/check-expired.ts +++ b/packages/backend/src/queue/processors/system/check-expired.ts @@ -1,6 +1,6 @@ import Bull from 'bull'; import { In, LessThan } from 'typeorm'; -import { AttestationChallenges, AuthSessions, Mutings, Notifications, PasswordResetRequests, Signins } from '@/models/index.js'; +import { AttestationChallenges, AuthSessions, Mutings, Notifications, PasswordResetRequests, Signins, Users } from '@/models/index.js'; import { publishUserEvent } from '@/services/stream.js'; import { MINUTE, MONTH } from '@/const.js'; import { queueLogger } from '@/queue/logger.js'; @@ -52,6 +52,11 @@ export async function checkExpired(job: Bull.Job>, done: createdAt: OlderThan(3 * MONTH), }); + await Users.delete({ + // delete users where the deletion status reference count has come down to zero + isDeleted: 0, + }); + logger.succ('Deleted expired data.'); done(); diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 82bd28703..d745a1fc7 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -12,6 +12,8 @@ export type DeliverJobData = { content: unknown; /** inbox URL to deliver */ to: string; + /** set if this job is part of a user deletion, on completion or failure the isDeleted field needs to be decremented */ + deletingUserId?: string; }; export type InboxJobData = { diff --git a/packages/backend/src/remote/activitypub/audience.ts b/packages/backend/src/remote/activitypub/audience.ts index 9c04ecb6d..9125660f3 100644 --- a/packages/backend/src/remote/activitypub/audience.ts +++ b/packages/backend/src/remote/activitypub/audience.ts @@ -1,5 +1,5 @@ import promiseLimit from 'promise-limit'; -import { CacheableRemoteUser, CacheableUser } from '@/models/entities/user.js'; +import { IRemoteUser, User } from '@/models/entities/user.js'; import { unique, concat } from '@/prelude/array.js'; import { resolvePerson } from './models/person.js'; import { Resolver } from './resolver.js'; @@ -9,20 +9,20 @@ type Visibility = 'public' | 'home' | 'followers' | 'specified'; type AudienceInfo = { visibility: Visibility, - mentionedUsers: CacheableUser[], - visibleUsers: CacheableUser[], + mentionedUsers: User[], + visibleUsers: User[], }; -export async function parseAudience(actor: CacheableRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { +export async function parseAudience(actor: IRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { const toGroups = groupingAudience(getApIds(to), actor); const ccGroups = groupingAudience(getApIds(cc), actor); const others = unique(concat([toGroups.other, ccGroups.other])); - const limit = promiseLimit(2); + const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( others.map(id => limit(() => resolvePerson(id, resolver).catch(() => null))), - )).filter((x): x is CacheableUser => x != null); + )).filter((x): x is User => x != null); if (toGroups.public.length > 0) { return { @@ -55,7 +55,7 @@ export async function parseAudience(actor: CacheableRemoteUser, to?: ApObject, c }; } -function groupingAudience(ids: string[], actor: CacheableRemoteUser) { +function groupingAudience(ids: string[], actor: IRemoteUser) { const groups = { public: [] as string[], followers: [] as string[], @@ -85,7 +85,7 @@ function isPublic(id: string) { ].includes(id); } -function isFollowers(id: string, actor: CacheableRemoteUser) { +function isFollowers(id: string, actor: IRemoteUser) { return ( id === (actor.followersUri || `${actor.uri}/followers`) ); diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts index dc1b47fef..4684b8931 100644 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ b/packages/backend/src/remote/activitypub/db-resolver.ts @@ -1,7 +1,7 @@ import escapeRegexp from 'escape-regexp'; import config from '@/config/index.js'; import { Note } from '@/models/entities/note.js'; -import { CacheableUser } from '@/models/entities/user.js'; +import { User } from '@/models/entities/user.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { Notes, MessagingMessages } from '@/models/index.js'; import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; @@ -89,7 +89,7 @@ export class DbResolver { /** * AP Person => FoundKey User in DB */ - public async getUserFromApId(value: string | IObject): Promise { + public async getUserFromApId(value: string | IObject): Promise { const parsed = parseUri(value); if (parsed.local) { diff --git a/packages/backend/src/remote/activitypub/deliver-manager.ts b/packages/backend/src/remote/activitypub/deliver-manager.ts index 4bc651c98..469464cec 100644 --- a/packages/backend/src/remote/activitypub/deliver-manager.ts +++ b/packages/backend/src/remote/activitypub/deliver-manager.ts @@ -88,10 +88,10 @@ export class DeliverManager { /** * Execute delivers */ - public async execute() { + public async execute(deletingUserId?: string) { if (!Users.isLocalUser(this.actor)) return; - const inboxes = new Set(); + let inboxes = new Set(); /* build inbox list @@ -150,13 +150,17 @@ export class DeliverManager { )), ); - // deliver - for (const inbox of inboxes) { - // skip instances as indicated - if (instancesToSkip.includes(new URL(inbox).host)) continue; + const filteredInboxes = Array.from(inboxes) + .filter(inbox => !instancesToSkip.includes(new URL(inbox).host)); - deliver(this.actor, this.activity, inbox); + if (deletingUserId) { + await Users.update(deletingUserId, { + // set deletion job count for reference counting before queueing jobs + isDeleted: filteredInboxes.length, + }); } + + filteredInboxes.forEach(inbox => deliver(this.actor, this.activity, inbox, deletingUserId)); } } diff --git a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts b/packages/backend/src/remote/activitypub/kernel/accept/follow.ts index 037f660c6..05c621e6d 100644 --- a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/accept/follow.ts @@ -1,11 +1,11 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { acceptFollowRequest } from '@/services/following/requests/accept.js'; import { relayAccepted } from '@/services/relay.js'; import { IFollow } from '@/remote/activitypub/type.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js'; -export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { - // โ€ป activityใฏใ“ใฃใกใ‹ใ‚‰ๆŠ•ใ’ใŸใƒ•ใ‚ฉใƒญใƒผใƒชใ‚ฏใ‚จใ‚นใƒˆใชใฎใงใ€activity.actorใฏๅญ˜ๅœจใ™ใ‚‹ใƒญใƒผใ‚ซใƒซใƒฆใƒผใ‚ถใƒผใงใ‚ใ‚‹ๅฟ…่ฆใŒใ‚ใ‚‹ +export default async (actor: IRemoteUser, activity: IFollow): Promise => { + // activity is a follow request started by this server, so activity.actor must be an existing local user. const dbResolver = new DbResolver(); const follower = await dbResolver.getUserFromApId(activity.actor); diff --git a/packages/backend/src/remote/activitypub/kernel/accept/index.ts b/packages/backend/src/remote/activitypub/kernel/accept/index.ts index be9b80096..1a61011e6 100644 --- a/packages/backend/src/remote/activitypub/kernel/accept/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/accept/index.ts @@ -1,10 +1,10 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { apLogger } from '@/remote/activitypub/logger.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { IAccept, isFollow, getApType } from '@/remote/activitypub/type.js'; import acceptFollow from './follow.js'; -export default async (actor: CacheableRemoteUser, activity: IAccept, resolver: Resolver): Promise => { +export default async (actor: IRemoteUser, activity: IAccept, resolver: Resolver): Promise => { const uri = activity.id || activity; apLogger.info(`Accept: ${uri}`); diff --git a/packages/backend/src/remote/activitypub/kernel/add/index.ts b/packages/backend/src/remote/activitypub/kernel/add/index.ts index 3fd5f4723..3d685dae4 100644 --- a/packages/backend/src/remote/activitypub/kernel/add/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/add/index.ts @@ -1,10 +1,10 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { addPinned } from '@/services/i/pin.js'; import { resolveNote } from '@/remote/activitypub/models/note.js'; import { IAdd } from '@/remote/activitypub/type.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; -export default async (actor: CacheableRemoteUser, activity: IAdd, resolver: Resolver): Promise => { +export default async (actor: IRemoteUser, activity: IAdd, resolver: Resolver): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/packages/backend/src/remote/activitypub/kernel/announce/index.ts b/packages/backend/src/remote/activitypub/kernel/announce/index.ts index e4d77e1c5..25bc8c73c 100644 --- a/packages/backend/src/remote/activitypub/kernel/announce/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/announce/index.ts @@ -1,10 +1,10 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { apLogger } from '@/remote/activitypub/logger.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { IAnnounce, getApId } from '@/remote/activitypub/type.js'; import announceNote from './note.js'; -export default async (actor: CacheableRemoteUser, activity: IAnnounce, resolver: Resolver): Promise => { +export default async (actor: IRemoteUser, activity: IAnnounce, resolver: Resolver): Promise => { const uri = getApId(activity); apLogger.info(`Announce: ${uri}`); diff --git a/packages/backend/src/remote/activitypub/kernel/announce/note.ts b/packages/backend/src/remote/activitypub/kernel/announce/note.ts index 254ef2727..e0861024a 100644 --- a/packages/backend/src/remote/activitypub/kernel/announce/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/announce/note.ts @@ -1,5 +1,5 @@ import post from '@/services/note/create.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { extractDbHost } from '@/misc/convert-host.js'; import { getApLock } from '@/misc/app-lock.js'; import { StatusError } from '@/misc/fetch.js'; @@ -11,13 +11,9 @@ import { Resolver } from '@/remote/activitypub/resolver.js'; import { IAnnounce, getApId } from '@/remote/activitypub/type.js'; import { shouldBlockInstance } from '@/misc/should-block-instance.js'; -export default async function(resolver: Resolver, actor: CacheableRemoteUser, activity: IAnnounce, targetUri: string): Promise { +export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, targetUri: string): Promise { const uri = getApId(activity); - if (actor.isSuspended) { - return; - } - // Cancel if the announced from host is blocked. if (await shouldBlockInstance(extractDbHost(uri))) return; diff --git a/packages/backend/src/remote/activitypub/kernel/block/index.ts b/packages/backend/src/remote/activitypub/kernel/block/index.ts index 7095a36a5..08b7ee517 100644 --- a/packages/backend/src/remote/activitypub/kernel/block/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/block/index.ts @@ -1,11 +1,11 @@ import block from '@/services/blocking/create.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { Users } from '@/models/index.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js'; import { IBlock } from '@/remote/activitypub/type.js'; -export default async (actor: CacheableRemoteUser, activity: IBlock): Promise => { - // โ€ป activity.objectใซใƒ–ใƒญใƒƒใ‚ฏๅฏพ่ฑกใŒใ‚ใ‚Šใ€ใใ‚Œใฏๅญ˜ๅœจใ™ใ‚‹ใƒญใƒผใ‚ซใƒซใƒฆใƒผใ‚ถใƒผใฎใฏใš +export default async (actor: IRemoteUser, activity: IBlock): Promise => { + // There is a block target in activity.object, which should be a local user that exists. const dbResolver = new DbResolver(); const blockee = await dbResolver.getUserFromApId(activity.object); @@ -15,7 +15,7 @@ export default async (actor: CacheableRemoteUser, activity: IBlock): Promise => { +export default async (actor: IRemoteUser, activity: ICreate, resolver: Resolver): Promise => { const uri = getApId(activity); apLogger.info(`Create: ${uri}`); diff --git a/packages/backend/src/remote/activitypub/kernel/create/note.ts b/packages/backend/src/remote/activitypub/kernel/create/note.ts index 892dbb26a..6fc7b6c2d 100644 --- a/packages/backend/src/remote/activitypub/kernel/create/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/create/note.ts @@ -1,4 +1,4 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { getApLock } from '@/misc/app-lock.js'; import { extractDbHost } from '@/misc/convert-host.js'; import { StatusError } from '@/misc/fetch.js'; @@ -9,7 +9,7 @@ import { getApId, IObject } from '@/remote/activitypub/type.js'; /** * ๆŠ•็จฟไฝœๆˆใ‚ขใ‚ฏใƒ†ใ‚ฃใƒ“ใƒ†ใ‚ฃใ‚’ๆŒใใพใ™ */ -export default async function(resolver: Resolver, actor: CacheableRemoteUser, note: IObject, silent = false): Promise { +export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false): Promise { const uri = getApId(note); if (typeof note === 'object') { diff --git a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts index ea75a9739..a191c626a 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts @@ -1,9 +1,9 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { Users } from '@/models/index.js'; import { apLogger } from '@/remote/activitypub/logger.js'; import { deleteAccount } from '@/services/delete-account.js'; -export async function deleteActor(actor: CacheableRemoteUser, uri: string): Promise { +export async function deleteActor(actor: IRemoteUser, uri: string): Promise { apLogger.info(`Deleting the Actor: ${uri}`); if (actor.uri !== uri) { @@ -16,7 +16,7 @@ export async function deleteActor(actor: CacheableRemoteUser, uri: string): Prom // anyway, the user is gone now so dont care return 'ok: gone'; } - if (user.isDeleted) { + if (user.isDeleted != null) { // the actual deletion already happened by an admin, just delete the record await Users.delete(actor.id); } else { diff --git a/packages/backend/src/remote/activitypub/kernel/delete/index.ts b/packages/backend/src/remote/activitypub/kernel/delete/index.ts index ee05e5327..9e05c9e48 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/index.ts @@ -1,4 +1,4 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { toSingle } from '@/prelude/array.js'; import { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '@/remote/activitypub/type.js'; import { deleteActor } from './actor.js'; @@ -7,7 +7,7 @@ import deleteNote from './note.js'; /** * ๅ‰Š้™คใ‚ขใ‚ฏใƒ†ใ‚ฃใƒ“ใƒ†ใ‚ฃใ‚’ๆŒใใพใ™ */ -export default async (actor: CacheableRemoteUser, activity: IDelete): Promise => { +export default async (actor: IRemoteUser, activity: IDelete): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/packages/backend/src/remote/activitypub/kernel/delete/note.ts b/packages/backend/src/remote/activitypub/kernel/delete/note.ts index 9f9a5cea6..d855f7f92 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/note.ts @@ -1,11 +1,11 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { deleteNotes } from '@/services/note/delete.js'; import { getApLock } from '@/misc/app-lock.js'; import { deleteMessage } from '@/services/messages/delete.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js'; import { apLogger } from '@/remote/activitypub/logger.js'; -export default async function(actor: CacheableRemoteUser, uri: string): Promise { +export default async function(actor: IRemoteUser, uri: string): Promise { apLogger.info(`Deleting the Note: ${uri}`); const unlock = await getApLock(uri); diff --git a/packages/backend/src/remote/activitypub/kernel/flag/index.ts b/packages/backend/src/remote/activitypub/kernel/flag/index.ts index e50bcc2bd..cadb7436d 100644 --- a/packages/backend/src/remote/activitypub/kernel/flag/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/flag/index.ts @@ -1,13 +1,14 @@ import { In } from 'typeorm'; import config from '@/config/index.js'; import { genId } from '@/misc/gen-id.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { AbuseUserReports, Users } from '@/models/index.js'; import { IFlag, getApIds } from '@/remote/activitypub/type.js'; -export default async (actor: CacheableRemoteUser, activity: IFlag): Promise => { - // objectใฏ `(User|Note) | (User|Note)[]` ใ ใ‘ใฉใ€ๅ…จใƒ‘ใ‚ฟใƒผใƒณDBใ‚นใ‚ญใƒผใƒžใจๅฏพๅฟœใ•ใ›ใ‚‰ใ‚Œใชใ„ใฎใง - // ๅฏพ่ฑกใƒฆใƒผใ‚ถใƒผใฏไธ€็•ชๆœ€ๅˆใฎใƒฆใƒผใ‚ถใƒผ ใจใ—ใฆ ใ‚ใจใฏใ‚ณใƒกใƒณใƒˆใจใ—ใฆๆ ผ็ดใ™ใ‚‹ +export default async (actor: IRemoteUser, activity: IFlag): Promise => { + // The object is `(User|Note) | (User|Note)[]`, but since the database schema + // cannot be made to handle every possible case, the target user is the first user + // and everything else is stored by URL. const uris = getApIds(activity.object); const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop()!); diff --git a/packages/backend/src/remote/activitypub/kernel/follow.ts b/packages/backend/src/remote/activitypub/kernel/follow.ts index 8125b4606..99dbb369c 100644 --- a/packages/backend/src/remote/activitypub/kernel/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/follow.ts @@ -1,9 +1,9 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import follow from '@/services/following/create.js'; import { IFollow } from '../type.js'; import { DbResolver } from '../db-resolver.js'; -export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { +export default async (actor: IRemoteUser, activity: IFollow): Promise => { const dbResolver = new DbResolver(); const followee = await dbResolver.getUserFromApId(activity.object); diff --git a/packages/backend/src/remote/activitypub/kernel/index.ts b/packages/backend/src/remote/activitypub/kernel/index.ts index 46a972a7e..2a0918a4d 100644 --- a/packages/backend/src/remote/activitypub/kernel/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/index.ts @@ -1,4 +1,4 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { toArray } from '@/prelude/array.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { extractDbHost } from '@/misc/convert-host.js'; @@ -21,7 +21,7 @@ import block from './block/index.js'; import flag from './flag/index.js'; import { move } from './move/index.js'; -export async function performActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise { +export async function performActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise { if (isCollectionOrOrderedCollection(activity)) { for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { const act = await resolver.resolve(item); @@ -38,7 +38,7 @@ export async function performActivity(actor: CacheableRemoteUser, activity: IObj } } -async function performOneActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise { +async function performOneActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise { if (actor.isSuspended) return; if (typeof activity.id !== 'undefined') { diff --git a/packages/backend/src/remote/activitypub/kernel/like.ts b/packages/backend/src/remote/activitypub/kernel/like.ts index 9650312b3..51878e454 100644 --- a/packages/backend/src/remote/activitypub/kernel/like.ts +++ b/packages/backend/src/remote/activitypub/kernel/like.ts @@ -1,9 +1,10 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { createReaction } from '@/services/note/reaction/create.js'; import { ILike, getApId } from '../type.js'; -import { fetchNote, extractEmojis } from '../models/note.js'; +import { fetchNote } from '../models/note.js'; +import { extractEmojis } from '../models/tag.js'; -export default async (actor: CacheableRemoteUser, activity: ILike) => { +export default async (actor: IRemoteUser, activity: ILike) => { const targetUri = getApId(activity.object); const note = await fetchNote(targetUri); diff --git a/packages/backend/src/remote/activitypub/kernel/move/index.ts b/packages/backend/src/remote/activitypub/kernel/move/index.ts index e64656e09..8f233d869 100644 --- a/packages/backend/src/remote/activitypub/kernel/move/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/move/index.ts @@ -1,12 +1,12 @@ import { IsNull } from 'typeorm'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { resolvePerson } from '@/remote/activitypub/models/person.js'; import { Followings, Users } from '@/models/index.js'; import { createNotification } from '@/services/create-notification.js'; import Resolver from '../../resolver.js'; import { IMove, isActor, getApId } from '../../type.js'; -export async function move(actor: CacheableRemoteUser, activity: IMove, resolver: Resolver): Promise { +export async function move(actor: IRemoteUser, activity: IMove, resolver: Resolver): Promise { // actor is not move origin if (activity.object == null || getApId(activity.object) !== actor.uri) return; diff --git a/packages/backend/src/remote/activitypub/kernel/read.ts b/packages/backend/src/remote/activitypub/kernel/read.ts index d367fb669..cb147f2af 100644 --- a/packages/backend/src/remote/activitypub/kernel/read.ts +++ b/packages/backend/src/remote/activitypub/kernel/read.ts @@ -1,10 +1,10 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { isSelfHost, extractDbHost } from '@/misc/convert-host.js'; import { MessagingMessages } from '@/models/index.js'; import { readUserMessagingMessage } from '@/server/api/common/read-messaging-message.js'; import { IRead, getApId } from '../type.js'; -export const performReadActivity = async (actor: CacheableRemoteUser, activity: IRead): Promise => { +export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise => { const id = await getApId(activity.object); if (!isSelfHost(extractDbHost(id))) { diff --git a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts index bd3ad1660..2606b8a5e 100644 --- a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts @@ -1,12 +1,12 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { remoteReject } from '@/services/following/reject.js'; import { relayRejected } from '@/services/relay.js'; import { Users } from '@/models/index.js'; import { IFollow } from '../../type.js'; import { DbResolver } from '../../db-resolver.js'; -export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { - // โ€ป activityใฏใ“ใฃใกใ‹ใ‚‰ๆŠ•ใ’ใŸใƒ•ใ‚ฉใƒญใƒผใƒชใ‚ฏใ‚จใ‚นใƒˆใชใฎใงใ€activity.actorใฏๅญ˜ๅœจใ™ใ‚‹ใƒญใƒผใ‚ซใƒซใƒฆใƒผใ‚ถใƒผใงใ‚ใ‚‹ๅฟ…่ฆใŒใ‚ใ‚‹ +export default async (actor: IRemoteUser, activity: IFollow): Promise => { + // activity is a follow request started by this server, so activity.actor must be an existing local user. const dbResolver = new DbResolver(); const follower = await dbResolver.getUserFromApId(activity.actor); diff --git a/packages/backend/src/remote/activitypub/kernel/reject/index.ts b/packages/backend/src/remote/activitypub/kernel/reject/index.ts index 3a91c8ec7..3eb748f6b 100644 --- a/packages/backend/src/remote/activitypub/kernel/reject/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/reject/index.ts @@ -1,10 +1,10 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { apLogger } from '../../logger.js'; import { IReject, isFollow, getApType } from '../../type.js'; import rejectFollow from './follow.js'; -export default async (actor: CacheableRemoteUser, activity: IReject, resolver: Resolver): Promise => { +export default async (actor: IRemoteUser, activity: IReject, resolver: Resolver): Promise => { const uri = activity.id || activity; apLogger.info(`Reject: ${uri}`); diff --git a/packages/backend/src/remote/activitypub/kernel/remove/index.ts b/packages/backend/src/remote/activitypub/kernel/remove/index.ts index 6591f82b1..3f7ad494e 100644 --- a/packages/backend/src/remote/activitypub/kernel/remove/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/remove/index.ts @@ -1,10 +1,10 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { removePinned } from '@/services/i/pin.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { IRemove } from '../../type.js'; import { resolveNote } from '../../models/note.js'; -export default async (actor: CacheableRemoteUser, activity: IRemove, resolver: Resolver): Promise => { +export default async (actor: IRemoteUser, activity: IRemove, resolver: Resolver): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts index fa4eea44c..b14cec889 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts @@ -1,10 +1,10 @@ import unfollow from '@/services/following/delete.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { Followings } from '@/models/index.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js'; import { IAccept } from '@/remote/activitypub/type.js'; -export default async (actor: CacheableRemoteUser, activity: IAccept): Promise => { +export default async (actor: IRemoteUser, activity: IAccept): Promise => { const dbResolver = new DbResolver(); const follower = await dbResolver.getUserFromApId(activity.object); diff --git a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts index 78981b542..05e0edbb6 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts @@ -1,9 +1,9 @@ import { Notes } from '@/models/index.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { deleteNotes } from '@/services/note/delete.js'; import { IAnnounce, getApId } from '@/remote/activitypub/type.js'; -export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnounce): Promise => { +export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise => { const uri = getApId(activity); const note = await Notes.findOneBy({ diff --git a/packages/backend/src/remote/activitypub/kernel/undo/block.ts b/packages/backend/src/remote/activitypub/kernel/undo/block.ts index ae1c9c0b6..f4e0513fb 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/block.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/block.ts @@ -1,10 +1,10 @@ import unblock from '@/services/blocking/delete.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { Users } from '@/models/index.js'; import { IBlock } from '@/remote/activitypub/type.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js'; -export default async (actor: CacheableRemoteUser, activity: IBlock): Promise => { +export default async (actor: IRemoteUser, activity: IBlock): Promise => { const dbResolver = new DbResolver(); const blockee = await dbResolver.getUserFromApId(activity.object); diff --git a/packages/backend/src/remote/activitypub/kernel/undo/follow.ts b/packages/backend/src/remote/activitypub/kernel/undo/follow.ts index c7f99bcf2..172ee8460 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/follow.ts @@ -1,11 +1,11 @@ import unfollow from '@/services/following/delete.js'; import { cancelFollowRequest } from '@/services/following/requests/cancel.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { FollowRequests, Followings } from '@/models/index.js'; import { IFollow } from '@/remote/activitypub/type.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js'; -export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { +export default async (actor: IRemoteUser, activity: IFollow): Promise => { const dbResolver = new DbResolver(); const followee = await dbResolver.getUserFromApId(activity.object); diff --git a/packages/backend/src/remote/activitypub/kernel/undo/index.ts b/packages/backend/src/remote/activitypub/kernel/undo/index.ts index 05382f0f5..139711129 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/index.ts @@ -1,4 +1,4 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { apLogger } from '@/remote/activitypub/logger.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept } from '@/remote/activitypub/type.js'; @@ -8,7 +8,7 @@ import undoLike from './like.js'; import undoAccept from './accept.js'; import { undoAnnounce } from './announce.js'; -export default async (actor: CacheableRemoteUser, activity: IUndo, resolver: Resolver): Promise => { +export default async (actor: IRemoteUser, activity: IUndo, resolver: Resolver): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/packages/backend/src/remote/activitypub/kernel/undo/like.ts b/packages/backend/src/remote/activitypub/kernel/undo/like.ts index 6c7b8d18b..717c8aa2a 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/like.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/like.ts @@ -1,4 +1,4 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { deleteReaction } from '@/services/note/reaction/delete.js'; import { ILike, getApId } from '@/remote/activitypub/type.js'; import { fetchNote } from '@/remote/activitypub/models/note.js'; @@ -6,7 +6,7 @@ import { fetchNote } from '@/remote/activitypub/models/note.js'; /** * Process Undo.Like activity */ -export default async (actor: CacheableRemoteUser, activity: ILike) => { +export default async (actor: IRemoteUser, activity: ILike) => { const targetUri = getApId(activity.object); const note = await fetchNote(targetUri); diff --git a/packages/backend/src/remote/activitypub/kernel/update/index.ts b/packages/backend/src/remote/activitypub/kernel/update/index.ts index 73085b181..d34965db2 100644 --- a/packages/backend/src/remote/activitypub/kernel/update/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/update/index.ts @@ -1,4 +1,4 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { getApId, getApType, IUpdate, isActor } from '@/remote/activitypub/type.js'; import { apLogger } from '@/remote/activitypub/logger.js'; import { updateQuestion } from '@/remote/activitypub/models/question.js'; @@ -8,7 +8,7 @@ import { updatePerson } from '@/remote/activitypub/models/person.js'; /** * Updateใ‚ขใ‚ฏใƒ†ใ‚ฃใƒ“ใƒ†ใ‚ฃใ‚’ๆŒใใพใ™ */ -export default async (actor: CacheableRemoteUser, activity: IUpdate, resolver: Resolver): Promise => { +export default async (actor: IRemoteUser, activity: IUpdate, resolver: Resolver): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { return 'skip: invalid actor'; } diff --git a/packages/backend/src/remote/activitypub/misc/auth-user.ts b/packages/backend/src/remote/activitypub/misc/auth-user.ts index 4705bb791..e140d2fa1 100644 --- a/packages/backend/src/remote/activitypub/misc/auth-user.ts +++ b/packages/backend/src/remote/activitypub/misc/auth-user.ts @@ -1,12 +1,12 @@ import { Cache } from '@/misc/cache.js'; import { UserPublickeys } from '@/models/index.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { UserPublickey } from '@/models/entities/user-publickey.js'; import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; import { createPerson } from '@/remote/activitypub/models/person.js'; export type AuthUser = { - user: CacheableRemoteUser; + user: IRemoteUser; key: UserPublickey; }; diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts index 281cbdf9a..aaf4b90d7 100644 --- a/packages/backend/src/remote/activitypub/models/image.ts +++ b/packages/backend/src/remote/activitypub/models/image.ts @@ -1,5 +1,5 @@ import { uploadFromUrl } from '@/services/drive/upload-from-url.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFiles } from '@/models/index.js'; @@ -11,7 +11,7 @@ import { apLogger } from '../logger.js'; /** * Imageใ‚’ไฝœๆˆใ—ใพใ™ใ€‚ */ -export async function createImage(actor: CacheableRemoteUser, value: any, resolver: Resolver): Promise { +export async function createImage(actor: IRemoteUser, value: any, resolver: Resolver): Promise { // ๆŠ•็จฟ่€…ใŒๅ‡็ตใ•ใ‚Œใฆใ„ใŸใ‚‰ใ‚นใ‚ญใƒƒใƒ— if (actor.isSuspended) { throw new Error('actor has been suspended'); @@ -58,7 +58,7 @@ export async function createImage(actor: CacheableRemoteUser, value: any, resolv * If the target Image is registered in FoundKey, return it; otherwise, fetch it from the remote server and return it. * Fetch the image from the remote server, register it in FoundKey and return it. */ -export async function resolveImage(actor: CacheableRemoteUser, value: any, resolver: Resolver): Promise { +export async function resolveImage(actor: IRemoteUser, value: any, resolver: Resolver): Promise { // TODO // Fetch from remote server and register it. diff --git a/packages/backend/src/remote/activitypub/models/mention.ts b/packages/backend/src/remote/activitypub/models/mention.ts index 183ab841a..c42fd197a 100644 --- a/packages/backend/src/remote/activitypub/models/mention.ts +++ b/packages/backend/src/remote/activitypub/models/mention.ts @@ -1,17 +1,17 @@ import promiseLimit from 'promise-limit'; import { toArray, unique } from '@/prelude/array.js'; -import { CacheableUser } from '@/models/entities/user.js'; +import { User } from '@/models/entities/user.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { IObject, isMention, IApMention } from '../type.js'; import { resolvePerson } from './person.js'; -export async function extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise { +export async function extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise { const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string)); - const limit = promiseLimit(2); + const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))), - )).filter((x): x is CacheableUser => x != null); + )).filter((x): x is User => x != null); return mentionedUsers; } diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index 30245a67f..70123bb4d 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -2,13 +2,13 @@ import promiseLimit from 'promise-limit'; import config from '@/config/index.js'; import post from '@/services/note/create.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { unique, toArray, toSingle } from '@/prelude/array.js'; import { vote } from '@/services/note/polls/vote.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { deliverQuestionUpdate } from '@/services/note/polls/update.js'; -import { extractDbHost, toPuny } from '@/misc/convert-host.js'; -import { Emojis, Polls, MessagingMessages } from '@/models/index.js'; +import { extractDbHost } from '@/misc/convert-host.js'; +import { Polls, MessagingMessages } from '@/models/index.js'; import { Note } from '@/models/entities/note.js'; import { Emoji } from '@/models/entities/emoji.js'; import { genId } from '@/misc/gen-id.js'; @@ -19,12 +19,12 @@ import { fromHtml } from '@/mfm/from-html.js'; import { shouldBlockInstance } from '@/misc/should-block-instance.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { parseAudience } from '../audience.js'; -import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js'; +import { IObject, getOneApId, getApId, getOneApHrefNullable, isPost, IPost, getApType } from '../type.js'; import { DbResolver } from '../db-resolver.js'; import { apLogger } from '../logger.js'; import { resolvePerson } from './person.js'; import { resolveImage } from './image.js'; -import { extractApHashtags, extractQuoteUrl } from './tag.js'; +import { extractApHashtags, extractQuoteUrl, extractEmojis } from './tag.js'; import { extractPollFromQuestion } from './question.js'; import { extractApMentions } from './mention.js'; @@ -33,7 +33,7 @@ export function validateNote(object: IObject): Error | null { return new Error('invalid Note: object is null'); } - if (!validPost.includes(getApType(object))) { + if (!isPost(object)) { return new Error(`invalid Note: invalid object type ${getApType(object)}`); } @@ -52,10 +52,63 @@ export function validateNote(object: IObject): Error | null { if (attributedToHost !== expectHost) { return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${attributedToHost}`); } + if (attributedToHost === config.hostname) { + return new Error('invalid Note: by local author'); + } return null; } +/** + * Function to process the content of a note, reusable in createNote and updateNote. + */ +async function processContent(note: IPost, quoteUri: string | null): + Promise<{ + cw: string | null, + files: DriveFile[], + text: string | null, + apEmoji: Emoji[], + apHashtags: string[], + url: string, + name: string + }> +{ + // Attachments handling + // TODO: attachments are not necessarily images + const limit = promiseLimit(2); + + const attachments = toArray(note.attachment); + const files = note.attachment + // If the note is marked as sensitive, the images should be marked sensitive too. + .map(attach => attach.sensitive |= note.sensitive) + ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x, resolver)) as Promise))) + .filter(image => image != null) + : []; + + // text parsing + let text: string | null = null; + if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { + text = note.source.content; + } else if (typeof note.content === 'string') { + text = fromHtml(note.content, quoteUri); + } + + const emojis = await extractEmojis(note.tag || [], extractDbHost(getApId(note))).catch(e => { + apLogger.info(`extractEmojis: ${e}`); + return [] as Emoji[]; + }); + + return { + cw: note.summary === '' ? null : note.summary, + files, + text, + apEmojis: emojis.map(emoji => emoji.name), + apHashtags: await extractApHashtags(note.tag), + url: getOneApHrefNullable(note.url), + name: note.name, + }; +} + /** * Fetch Note. * @@ -74,13 +127,7 @@ export async function createNote(value: string | IObject, resolver: Resolver, si const err = validateNote(object); if (err) { - apLogger.error(`${err.message}`, { - resolver: { - history: resolver.getHistory(), - }, - value, - object, - }); + apLogger.error(`${err.message}`); throw new Error('invalid note'); } @@ -91,7 +138,7 @@ export async function createNote(value: string | IObject, resolver: Resolver, si apLogger.info(`Creating the Note: ${note.id}`); // ๆŠ•็จฟ่€…ใ‚’ใƒ•ใ‚งใƒƒใƒ - const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as CacheableRemoteUser; + const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as IRemoteUser; // ๆŠ•็จฟ่€…ใŒๅ‡็ตใ•ใ‚Œใฆใ„ใŸใ‚‰ใ‚นใ‚ญใƒƒใƒ— if (actor.isSuspended) { @@ -102,33 +149,17 @@ export async function createNote(value: string | IObject, resolver: Resolver, si let visibility = noteAudience.visibility; const visibleUsers = noteAudience.visibleUsers; - // Audience (to, cc) ใŒๆŒ‡ๅฎšใ•ใ‚Œใฆใชใ‹ใฃใŸๅ ดๅˆ + // If audience(to,cc) was not specified if (visibility === 'specified' && visibleUsers.length === 0) { - if (typeof value === 'string') { // ๅ…ฅๅŠ›ใŒstringใชใ‚‰ใฐresolverใงGETใŒ็™บ็”Ÿใ—ใฆใ„ใ‚‹ - // ใ“ใกใ‚‰ใ‹ใ‚‰ๅŒฟๅGETๅ‡บๆฅใŸใ‚‚ใฎใชใ‚‰ใฐpublic - visibility = 'public'; - } + // TODO derive audience from context (e.g. whose inbox this was in?) + throw new Error('audience not understood'); } let isTalk = note._misskey_talk && visibility === 'specified'; const apMentions = await extractApMentions(note.tag, resolver); - const apHashtags = await extractApHashtags(note.tag); - // ๆทปไป˜ใƒ•ใ‚กใ‚คใƒซ - // TODO: attachmentใฏๅฟ…ใšใ—ใ‚‚Imageใงใฏใชใ„ - // TODO: attachmentใฏๅฟ…ใšใ—ใ‚‚้…ๅˆ—ใงใฏใชใ„ - // NoteใŒsensitiveใชใ‚‰ๆทปไป˜ใ‚‚sensitiveใซใ™ใ‚‹ - const limit = promiseLimit(2); - - note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : []; - const files = note.attachment - .map(attach => attach.sensitive = note.sensitive) - ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x, resolver)) as Promise))) - .filter(image => image != null) - : []; - - // ใƒชใƒ—ใƒฉใ‚ค + // Reply handling const reply: Note | null = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver).then(x => { if (x == null) { @@ -138,7 +169,7 @@ export async function createNote(value: string | IObject, resolver: Resolver, si return x; } }).catch(async e => { - // ใƒˆใƒผใ‚ฏใ ใฃใŸใ‚‰inReplyToใฎใ‚จใƒฉใƒผใฏ็„ก่ฆ– + // ignore inReplyTo if it is a messaging message const uri = getApId(note.inReplyTo); if (uri.startsWith(config.url + '/')) { const id = uri.split('/').pop(); @@ -203,16 +234,6 @@ export async function createNote(value: string | IObject, resolver: Resolver, si } } - const cw = note.summary === '' ? null : note.summary; - - // text parsing - let text: string | null = null; - if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { - text = note.source.content; - } else if (typeof note.content === 'string') { - text = fromHtml(note.content, quote?.uri); - } - // vote if (reply && reply.hasPoll) { const poll = await Polls.findOneByOrFail({ noteId: reply.id }); @@ -224,7 +245,7 @@ export async function createNote(value: string | IObject, resolver: Resolver, si apLogger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); await vote(actor, reply, index); - // ใƒชใƒขใƒผใƒˆใƒ•ใ‚ฉใƒญใƒฏใƒผใซUpdate้…ไฟก + // Federate an Update to other servers deliverQuestionUpdate(reply.id); } return null; @@ -235,40 +256,36 @@ export async function createNote(value: string | IObject, resolver: Resolver, si } } - const emojis = await extractEmojis(note.tag || [], actor.host).catch(e => { - apLogger.info(`extractEmojis: ${e}`); - return [] as Emoji[]; - }); - - const apEmojis = emojis.map(emoji => emoji.name); - const poll = await extractPollFromQuestion(note, resolver).catch(() => undefined); + const processedContent = await processContent(note, quote?.uri); + if (isTalk) { for (const recipient of visibleUsers) { - await createMessage(actor, recipient, undefined, text || undefined, (files && files.length > 0) ? files[0] : null, object.id); - return null; + await createMessage( + actor, + recipient, + undefined, + processedContent.text, + processedContent.files[0] ?? null, + object.id + ); } + return null; + } else { + return await post(actor, { + ...processedContent, + createdAt: note.published ? new Date(note.published) : null, + reply, + renote: quote, + localOnly: false, + visibility, + visibleUsers, + apMentions, + poll, + uri: note.id, + }, silent); } - - return await post(actor, { - createdAt: note.published ? new Date(note.published) : null, - files, - reply, - renote: quote, - name: note.name, - cw, - text, - localOnly: false, - visibility, - visibleUsers, - apMentions, - apHashtags, - apEmojis, - poll, - uri: note.id, - url: getOneApHrefNullable(note.url), - }, silent); } /** @@ -307,59 +324,3 @@ export async function resolveNote(value: string | IObject, resolver: Resolver): unlock(); } } - -export async function extractEmojis(tags: IObject | IObject[], idnHost: string): Promise { - const host = toPuny(idnHost); - - if (!tags) return []; - - const eomjiTags = toArray(tags).filter(isEmoji); - - return await Promise.all(eomjiTags.map(async tag => { - const name = tag.name!.replace(/^:/, '').replace(/:$/, ''); - tag.icon = toSingle(tag.icon); - - const exists = await Emojis.findOneBy({ - host, - name, - }); - - if (exists) { - if ((tag.updated != null && exists.updatedAt == null) - || (tag.id != null && exists.uri == null) - || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt) - || (tag.icon!.url !== exists.originalUrl) - ) { - await Emojis.update({ - host, - name, - }, { - uri: tag.id, - originalUrl: tag.icon!.url, - publicUrl: tag.icon!.url, - updatedAt: new Date(), - }); - - return await Emojis.findOneBy({ - host, - name, - }) as Emoji; - } - - return exists; - } - - apLogger.info(`register emoji host=${host}, name=${name}`); - - return await Emojis.insert({ - id: genId(), - host, - name, - uri: tag.id, - originalUrl: tag.icon!.url, - publicUrl: tag.icon!.url, - updatedAt: new Date(), - aliases: [], - } as Partial).then(x => Emojis.findOneByOrFail(x.identifiers[0])); - })); -} diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 17d9858e4..ed6e9fa03 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -6,7 +6,7 @@ import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instanc import { Note } from '@/models/entities/note.js'; import { updateUsertags } from '@/services/update-hashtag.js'; import { Users, Instances, Followings, UserProfiles, UserPublickeys } from '@/models/index.js'; -import { User, IRemoteUser, CacheableUser } from '@/models/entities/user.js'; +import { User, IRemoteUser, User } from '@/models/entities/user.js'; import { Emoji } from '@/models/entities/emoji.js'; import { UserNotePining } from '@/models/entities/user-note-pining.js'; import { genId } from '@/misc/gen-id.js'; @@ -27,8 +27,8 @@ import { fromHtml } from '@/mfm/from-html.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { apLogger } from '../logger.js'; import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, getApType, isActor } from '../type.js'; -import { extractApHashtags } from './tag.js'; -import { resolveNote, extractEmojis } from './note.js'; +import { extractApHashtags, extractEmojis } from './tag.js'; +import { resolveNote } from './note.js'; import { resolveImage } from './image.js'; const nameLength = 128; @@ -121,7 +121,7 @@ async function validateActor(x: IObject, resolver: Resolver): Promise { * * If the target Person is registered in FoundKey, it is returned. */ -export async function fetchPerson(uri: string): Promise { +export async function fetchPerson(uri: string): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); const cached = uriPersonCache.get(uri); @@ -217,7 +217,7 @@ export async function createPerson(value: string | IObject, resolver: Resolver): } catch (e) { // duplicate key error if (isDuplicateKeyValueError(e)) { - // /users/@a => /users/:id ใฎใ‚ˆใ†ใซๅ…ฅๅŠ›ใŒaliasใชใจใใซใ‚จใƒฉใƒผใซใชใ‚‹ใ“ใจใŒใ‚ใ‚‹ใฎใ‚’ๅฏพๅฟœ + // Fix an error when the input is an alias like /users/@a -> /users/:id const u = await Users.findOneBy({ uri: person.id, }); @@ -394,7 +394,7 @@ export async function updatePerson(value: IObject | string, resolver: Resolver): * If the target Person is registered in FoundKey, return it; otherwise, fetch it from a remote server and return it. * Fetch the person from the remote server, register it in FoundKey, and return it. */ -export async function resolvePerson(uri: string, resolver: Resolver, hint?: IObject): Promise { +export async function resolvePerson(uri: string, resolver: Resolver, hint?: IObject): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); //#region ใ“ใฎใ‚ตใƒผใƒใƒผใซๆ—ขใซ็™ป้Œฒใ•ใ‚Œใฆใ„ใŸใ‚‰ใใ‚Œใ‚’่ฟ”ใ™ diff --git a/packages/backend/src/remote/activitypub/models/tag.ts b/packages/backend/src/remote/activitypub/models/tag.ts index ed277fc7c..00322bfd2 100644 --- a/packages/backend/src/remote/activitypub/models/tag.ts +++ b/packages/backend/src/remote/activitypub/models/tag.ts @@ -1,7 +1,11 @@ -import { toArray } from '@/prelude/array.js'; -import { IObject, isHashtag, IApHashtag, isLink, ILink } from '../type.js'; +import { toArray, toSingle } from '@/prelude/array.js'; +import { IObject, isHashtag, IApHashtag, isLink, ILink, isEmoji } from '../type.js'; +import { toPuny } from '@/misc/convert-host.js'; +import { Emojis } from '@/models/index.js'; +import { Emoji } from '@/models/entities/emoji.js'; +import { apLogger } from '@/remote/activitypub/logger.js'; -export function extractApHashtags(tags: IObject | IObject[] | null | undefined) { +export function extractApHashtags(tags: IObject | IObject[] | null | undefined): string[] { if (tags == null) return []; const hashtags = extractApHashtagObjects(tags); @@ -47,3 +51,59 @@ export function extractQuoteUrl(tags: IObject | IObject[] | null | undefined): s // If there is more than one quote, we just pick the first/a random one. else return quotes[0].href; } + +export async function extractEmojis(tags: IObject | IObject[], idnHost: string): Promise { + const host = toPuny(idnHost); + + if (!tags) return []; + + const eomjiTags = toArray(tags).filter(isEmoji); + + return await Promise.all(eomjiTags.map(async tag => { + const name = tag.name!.replace(/^:/, '').replace(/:$/, ''); + tag.icon = toSingle(tag.icon); + + const exists = await Emojis.findOneBy({ + host, + name, + }); + + if (exists) { + if ((tag.updated != null && exists.updatedAt == null) + || (tag.id != null && exists.uri == null) + || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt) + || (tag.icon!.url !== exists.originalUrl) + ) { + await Emojis.update({ + host, + name, + }, { + uri: tag.id, + originalUrl: tag.icon!.url, + publicUrl: tag.icon!.url, + updatedAt: new Date(), + }); + + return await Emojis.findOneBy({ + host, + name, + }) as Emoji; + } + + return exists; + } + + apLogger.info(`register emoji host=${host}, name=${name}`); + + return await Emojis.insert({ + id: genId(), + host, + name, + uri: tag.id, + originalUrl: tag.icon!.url, + publicUrl: tag.icon!.url, + updatedAt: new Date(), + aliases: [], + } as Partial).then(x => Emojis.findOneByOrFail(x.identifiers[0])); + })); +} diff --git a/packages/backend/src/remote/activitypub/perform.ts b/packages/backend/src/remote/activitypub/perform.ts index 8622d43df..23999213c 100644 --- a/packages/backend/src/remote/activitypub/perform.ts +++ b/packages/backend/src/remote/activitypub/perform.ts @@ -1,11 +1,11 @@ import { DAY } from '@/const.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { IObject } from './type.js'; import { performActivity } from './kernel/index.js'; import { updatePerson } from './models/person.js'; -export async function perform(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise { +export async function perform(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise { await performActivity(actor, activity, resolver); // And while I'm at it, I'll update the remote user information if it's out of date. diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts index beb48713a..2c2b6cfb4 100644 --- a/packages/backend/src/server/activitypub/followers.ts +++ b/packages/backend/src/server/activitypub/followers.ts @@ -6,7 +6,7 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; +import { Users, Followings } from '@/models/index.js'; import { Following } from '@/models/entities/following.js'; import { setResponseType } from '../activitypub.js'; @@ -31,19 +31,12 @@ export default async (ctx: Router.RouterContext) => { return; } - //#region Check ff visibility - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.ffVisibility === 'private') { - ctx.status = 403; - ctx.set('Cache-Control', 'public, max-age=30'); - return; - } else if (profile.ffVisibility === 'followers') { + const ffVisible = await Users.areFollowersVisibleTo(user, null); + if (!ffVisible) { ctx.status = 403; ctx.set('Cache-Control', 'public, max-age=30'); return; } - //#endregion const limit = 10; const partOf = `${config.url}/users/${userId}/followers`; diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts index 3a25a6316..4e156a19f 100644 --- a/packages/backend/src/server/activitypub/following.ts +++ b/packages/backend/src/server/activitypub/following.ts @@ -6,7 +6,7 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; +import { Users, Followings } from '@/models/index.js'; import { Following } from '@/models/entities/following.js'; import { setResponseType } from '../activitypub.js'; @@ -31,19 +31,12 @@ export default async (ctx: Router.RouterContext) => { return; } - //#region Check ff visibility - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.ffVisibility === 'private') { - ctx.status = 403; - ctx.set('Cache-Control', 'public, max-age=30'); - return; - } else if (profile.ffVisibility === 'followers') { + const ffVisible = await Users.areFollowersVisibleTo(user, null); + if (!ffVisible) { ctx.status = 403; ctx.set('Cache-Control', 'public, max-age=30'); return; } - //#endregion const limit = 10; const partOf = `${config.url}/users/${userId}/following`; diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts index de9af0890..e5a8dbcf6 100644 --- a/packages/backend/src/server/api/api-handler.ts +++ b/packages/backend/src/server/api/api-handler.ts @@ -1,7 +1,7 @@ import Koa from 'koa'; import { IEndpoint } from './endpoints.js'; -import authenticate, { AuthenticationError } from './authenticate.js'; +import { authenticate, AuthenticationError } from './authenticate.js'; import call from './call.js'; import { ApiError } from './error.js'; @@ -45,7 +45,10 @@ export async function handler(endpoint: IEndpoint, ctx: Koa.Context): Promise => { +export async function authenticate(authorization: string | null | undefined, bodyToken: string | null | undefined): Promise<[ILocalUser | null | undefined, AccessToken | null | undefined]> { let maybeToken: string | null = null; // check if there is an authorization header set @@ -66,4 +66,4 @@ export default async (authorization: string | null | undefined, bodyToken: strin return [user, accessToken]; } -}; +} diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index dc0e790bd..ea8c6086e 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -1,14 +1,14 @@ import { performance } from 'perf_hooks'; import Koa from 'koa'; -import { CacheableLocalUser } from '@/models/entities/user.js'; +import { ILocalUser } from '@/models/entities/user.js'; import { AccessToken } from '@/models/entities/access-token.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; import { limiter } from './limiter.js'; -import endpoints, { IEndpointMeta } from './endpoints.js'; +import { endpoints, IEndpointMeta } from './endpoints.js'; import { ApiError } from './error.js'; import { apiLogger } from './logger.js'; -export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => { +export default async (endpoint: string, user: ILocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => { const isSecure = user != null && token == null; const isModerator = user != null && (user.isModerator || user.isAdmin); @@ -82,15 +82,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi if (e instanceof ApiError) { throw e; } else { - apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`, { - ep: ep.name, - ps: data, - e: { - message: e.message, - code: e.name, - stack: e.stack, - }, - }); + apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`); throw new ApiError('INTERNAL_ERROR', { e: { message: e.message, diff --git a/packages/backend/src/server/api/common/getters.ts b/packages/backend/src/server/api/common/getters.ts index 97059f987..f15ae39f8 100644 --- a/packages/backend/src/server/api/common/getters.ts +++ b/packages/backend/src/server/api/common/getters.ts @@ -32,7 +32,7 @@ export async function getNote(noteId: Note['id'], me: { id: User['id'] } | null) export async function getUser(userId: User['id'], includeSuspended = false) { const user = await Users.findOneBy({ id: userId, - isDeleted: false, + isDeleted: IsNull(), ...(includeSuspended ? {} : {isSuspended: false}), }); @@ -50,7 +50,7 @@ export async function getRemoteUser(userId: User['id'], includeSuspended = false const user = await Users.findOneBy({ id: userId, host: Not(IsNull()), - isDeleted: false, + isDeleted: IsNull(), ...(includeSuspended ? {} : {isSuspended: false}), }); @@ -68,7 +68,7 @@ export async function getLocalUser(userId: User['id'], includeSuspended = false) const user = await Users.findOneBy({ id: userId, host: IsNull(), - isDeleted: false, + isDeleted: IsNull(), ...(includeSuspended ? {} : {isSuspended: false}), }); diff --git a/packages/backend/src/server/api/define.ts b/packages/backend/src/server/api/define.ts index 243b105ae..811a10ae3 100644 --- a/packages/backend/src/server/api/define.ts +++ b/packages/backend/src/server/api/define.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import Ajv from 'ajv'; -import { CacheableLocalUser } from '@/models/entities/user.js'; +import { ILocalUser } from '@/models/entities/user.js'; import { Schema, SchemaType } from '@/misc/schema.js'; import { AccessToken } from '@/models/entities/access-token.js'; import { IEndpointMeta } from './endpoints.js'; @@ -10,7 +10,7 @@ export type Response = Record | void; // TODO: paramsใฎๅž‹ใ‚’T['params']ใฎใ‚นใ‚ญใƒผใƒžๅฎš็พฉใ‹ใ‚‰ๆŽจ่ซ–ใ™ใ‚‹ type executor = - (params: SchemaType, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any) => + (params: SchemaType, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any) => Promise>>; const ajv = new Ajv({ @@ -20,10 +20,10 @@ const ajv = new Ajv({ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); export default function (meta: T, paramDef: Ps, cb: executor) - : (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => Promise { + : (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any) => Promise { const validate = ajv.compile(paramDef); - return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => { + return (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any) => { function cleanup() { fs.unlink(file.path, () => {}); } diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 55763d875..c5249191e 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -162,7 +162,6 @@ import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; import * as ep___i_exportMute from './endpoints/i/export-mute.js'; import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; -import * as ep___i_favorites from './endpoints/i/favorites.js'; import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js'; import * as ep___i_importBlocking from './endpoints/i/import-blocking.js'; import * as ep___i_importFollowing from './endpoints/i/import-following.js'; @@ -215,8 +214,6 @@ import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; -import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; -import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; @@ -459,7 +456,6 @@ const eps = [ ['i/export-mute', ep___i_exportMute], ['i/export-notes', ep___i_exportNotes], ['i/export-user-lists', ep___i_exportUserLists], - ['i/favorites', ep___i_favorites], ['i/get-word-muted-notes-count', ep___i_getWordMutedNotesCount], ['i/import-blocking', ep___i_importBlocking], ['i/import-following', ep___i_importFollowing], @@ -512,8 +508,6 @@ const eps = [ ['notes/conversation', ep___notes_conversation], ['notes/create', ep___notes_create], ['notes/delete', ep___notes_delete], - ['notes/favorites/create', ep___notes_favorites_create], - ['notes/favorites/delete', ep___notes_favorites_delete], ['notes/featured', ep___notes_featured], ['notes/global-timeline', ep___notes_globalTimeline], ['notes/hybrid-timeline', ep___notes_hybridTimeline], @@ -713,7 +707,7 @@ export interface IEndpoint { params: Schema; } -const endpoints: IEndpoint[] = eps.map(([name, ep]) => { +export const endpoints: IEndpoint[] = eps.map(([name, ep]) => { return { name, exec: ep.default, @@ -721,5 +715,3 @@ const endpoints: IEndpoint[] = eps.map(([name, ep]) => { params: ep.paramDef, }; }); - -export default endpoints; diff --git a/packages/backend/src/server/api/endpoints/admin/users/delete.ts b/packages/backend/src/server/api/endpoints/admin/users/delete.ts index 9112f50f5..055e3d98f 100644 --- a/packages/backend/src/server/api/endpoints/admin/users/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/users/delete.ts @@ -1,3 +1,4 @@ +import { IsNull } from 'typeorm'; import { Users } from '@/models/index.js'; import { ApiError } from '@/server/api/error.js'; import { deleteAccount } from '@/services/delete-account.js'; diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 9492cc60a..31b0f1266 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -5,7 +5,7 @@ import { Resolver } from '@/remote/activitypub/resolver.js'; import { extractDbHost } from '@/misc/convert-host.js'; import { Users, Notes } from '@/models/index.js'; import { Note } from '@/models/entities/note.js'; -import { CacheableLocalUser, User } from '@/models/entities/user.js'; +import { ILocalUser, User } from '@/models/entities/user.js'; import { isActor, isPost } from '@/remote/activitypub/type.js'; import { SchemaType } from '@/misc/schema.js'; import { HOUR } from '@/const.js'; @@ -85,7 +85,7 @@ export default define(meta, paramDef, async (ps, me) => { /*** * URIใ‹ใ‚‰Userใ‹Noteใ‚’่งฃๆฑบใ™ใ‚‹ */ -async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise | null> { +async function fetchAny(uri: string, me: ILocalUser | null | undefined): Promise | null> { // Stop if the host is blocked. const host = extractDbHost(uri); if (await shouldBlockInstance(host)) { @@ -122,7 +122,7 @@ async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): ); } -async function mergePack(me: CacheableLocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise | null> { +async function mergePack(me: ILocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise | null> { if (user != null) { return { type: 'User', diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts index f380a5287..b5894c10d 100644 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ b/packages/backend/src/server/api/endpoints/endpoint.ts @@ -1,5 +1,5 @@ import define from '@/server/api/define.js'; -import endpoints from '@/server/api/endpoints.js'; +import { endpoints } from '@/server/api/endpoints.js'; export const meta = { requireCredential: false, diff --git a/packages/backend/src/server/api/endpoints/endpoints.ts b/packages/backend/src/server/api/endpoints/endpoints.ts index 184f74e79..976109887 100644 --- a/packages/backend/src/server/api/endpoints/endpoints.ts +++ b/packages/backend/src/server/api/endpoints/endpoints.ts @@ -1,5 +1,5 @@ import define from '@/server/api/define.js'; -import endpoints from '@/server/api/endpoints.js'; +import { endpoints } from '@/server/api/endpoints.js'; export const meta = { requireCredential: false, diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index eea6e427a..2bae8075b 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -27,7 +27,7 @@ export default define(meta, paramDef, async (ps, user) => { Users.findOneByOrFail({ id: user.id }), ]); - if (userDetailed.isDeleted) { + if (userDetailed.isDeleted != null) { return; } diff --git a/packages/backend/src/server/api/endpoints/i/favorites.ts b/packages/backend/src/server/api/endpoints/i/favorites.ts deleted file mode 100644 index ddda42a6d..000000000 --- a/packages/backend/src/server/api/endpoints/i/favorites.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NoteFavorites } from '@/models/index.js'; -import define from '@/server/api/define.js'; -import { makePaginationQuery } from '@/server/api/common/make-pagination-query.js'; - -export const meta = { - tags: ['account', 'notes', 'favorites'], - - requireCredential: true, - - kind: 'read:favorites', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'NoteFavorite', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(NoteFavorites.createQueryBuilder('favorite'), ps.sinceId, ps.untilId) - .andWhere('favorite.userId = :meId', { meId: user.id }) - .leftJoinAndSelect('favorite.note', 'note'); - - const favorites = await query - .take(ps.limit) - .getMany(); - - return await NoteFavorites.packMany(favorites, user); -}); diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index c7f7d50c8..b5abf56b5 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -1,6 +1,6 @@ import RE2 from 're2'; import * as mfm from 'mfm-js'; -import { notificationTypes } from 'foundkey-js'; +import { ffVisibility, notificationTypes } from 'foundkey-js'; import { publishMainStream, publishUserEvent } from '@/services/stream.js'; import { acceptAllFollowRequests } from '@/services/following/requests/accept-all.js'; import { publishToFollowers } from '@/services/i/update.js'; @@ -67,7 +67,7 @@ export const paramDef = { injectFeaturedNote: { type: 'boolean' }, receiveAnnouncementEmail: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' }, - ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, + ffVisibility: { type: 'string', enum: ffVisibility }, pinnedPageId: { type: 'array', items: { type: 'string', format: 'misskey:id', } }, @@ -178,7 +178,7 @@ export default define(meta, paramDef, async (ps, _user, token) => { const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; if (newName != null) { - const tokens = mfm.parsePlain(newName); + const tokens = mfm.parseSimple(newName); emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); } diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts deleted file mode 100644 index f9f769ace..000000000 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NoteFavorites } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import define from '@/server/api/define.js'; -import { ApiError } from '@/server/api/error.js'; -import { getNote } from '@/server/api/common/getters.js'; - -export const meta = { - tags: ['notes', 'favorites'], - - requireCredential: true, - - kind: 'write:favorites', - - errors: ['NO_SUCH_NOTE', 'ALREADY_FAVORITED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Get favoritee - const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); - throw err; - }); - - // if already favorited - const exist = await NoteFavorites.countBy({ - noteId: note.id, - userId: user.id, - }); - - if (exist) throw new ApiError('ALREADY_FAVORITED'); - - // Create favorite - await NoteFavorites.insert({ - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: user.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts deleted file mode 100644 index 416c70062..000000000 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NoteFavorites } from '@/models/index.js'; -import define from '@/server/api/define.js'; -import { ApiError } from '@/server/api/error.js'; -import { getNote } from '@/server/api/common/getters.js'; - -export const meta = { - tags: ['notes', 'favorites'], - - requireCredential: true, - - kind: 'write:favorites', - - errors: ['NO_SUCH_NOTE', 'NOT_FAVORITED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Get favoritee - const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); - throw err; - }); - - // if already favorited - const exist = await NoteFavorites.findOneBy({ - noteId: note.id, - userId: user.id, - }); - - if (exist == null) throw new ApiError('NOT_FAVORITED'); - - // Delete favorite - await NoteFavorites.delete(exist.id); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index 85ae2aa23..bb7c937a8 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -1,4 +1,4 @@ -import { NoteFavorites, NoteThreadMutings, NoteWatchings } from '@/models/index.js'; +import { NoteThreadMutings, NoteWatchings } from '@/models/index.js'; import { ApiError } from '@/server/api/error.js'; import { getNote } from '@/server/api/common/getters.js'; import define from '@/server/api/define.js'; @@ -12,10 +12,6 @@ export const meta = { type: 'object', optional: false, nullable: false, properties: { - isFavorited: { - type: 'boolean', - optional: false, nullable: false, - }, isWatching: { type: 'boolean', optional: false, nullable: false, @@ -51,14 +47,7 @@ export default define(meta, paramDef, async (ps, user) => { throw err; }); - const [favorite, watching, threadMuting] = await Promise.all([ - NoteFavorites.count({ - where: { - userId: user.id, - noteId: note.id, - }, - take: 1, - }), + const [watching, threadMuting] = await Promise.all([ NoteWatchings.count({ where: { userId: user.id, @@ -76,7 +65,6 @@ export default define(meta, paramDef, async (ps, user) => { ]); return { - isFavorited: favorite !== 0, isWatching: watching !== 0, isMutedThread: threadMuting !== 0, }; diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 2595fbff5..e93851cd8 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -1,5 +1,5 @@ import { IsNull } from 'typeorm'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; +import { Users, Followings } from '@/models/index.js'; import { toPunyNullable } from '@/misc/convert-host.js'; import define from '@/server/api/define.js'; import { ApiError } from '@/server/api/error.js'; @@ -61,25 +61,8 @@ export default define(meta, paramDef, async (ps, me) => { if (user == null) throw new ApiError('NO_SUCH_USER'); - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.ffVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError('ACCESS_DENIED'); - } - } else if (profile.ffVisibility === 'followers') { - if (me == null) { - throw new ApiError('ACCESS_DENIED'); - } else if (me.id !== user.id) { - const following = await Followings.countBy({ - followeeId: user.id, - followerId: me.id, - }); - if (!following) { - throw new ApiError('ACCESS_DENIED'); - } - } - } + const ffVisible = await Users.areFollowersVisibleTo(user, me); + if (!ffVisible) throw new ApiError('ACCESS_DENIED'); const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) .andWhere('following.followeeId = :userId', { userId: user.id }) diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 0bf60c079..406853423 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -1,5 +1,5 @@ import { IsNull } from 'typeorm'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; +import { Users, Followings } from '@/models/index.js'; import { toPunyNullable } from '@/misc/convert-host.js'; import define from '@/server/api/define.js'; import { ApiError } from '@/server/api/error.js'; @@ -61,25 +61,8 @@ export default define(meta, paramDef, async (ps, me) => { if (user == null) throw new ApiError('NO_SUCH_USER'); - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.ffVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError('ACCESS_DENIED'); - } - } else if (profile.ffVisibility === 'followers') { - if (me == null) { - throw new ApiError('ACCESS_DENIED'); - } else if (me.id !== user.id) { - const following = await Followings.countBy({ - followeeId: user.id, - followerId: me.id, - }); - if (!following) { - throw new ApiError('ACCESS_DENIED'); - } - } - } + const ffVisible = await Users.areFollowersVisibleTo(user, me); + if (!ffVisible) throw new ApiError('ACCESS_DENIED'); const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) .andWhere('following.followerId = :userId', { userId: user.id }) diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts index 970308f78..1a0e9031e 100644 --- a/packages/backend/src/server/api/endpoints/users/stats.ts +++ b/packages/backend/src/server/api/endpoints/users/stats.ts @@ -1,4 +1,4 @@ -import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index.js'; +import { DriveFiles, Followings, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index.js'; import { awaitAll } from '@/prelude/await-all.js'; import define from '@/server/api/define.js'; import { ApiError } from '@/server/api/error.js'; @@ -46,27 +46,27 @@ export const meta = { }, localFollowingCount: { type: 'integer', - optional: false, nullable: false, + optional: true, nullable: false, }, remoteFollowingCount: { type: 'integer', - optional: false, nullable: false, + optional: true, nullable: false, }, localFollowersCount: { type: 'integer', - optional: false, nullable: false, + optional: true, nullable: false, }, remoteFollowersCount: { type: 'integer', - optional: false, nullable: false, + optional: true, nullable: false, }, followingCount: { type: 'integer', - optional: false, nullable: false, + optional: true, nullable: false, }, followersCount: { type: 'integer', - optional: false, nullable: false, + optional: true, nullable: false, }, sentReactionsCount: { type: 'integer', @@ -76,10 +76,6 @@ export const meta = { type: 'integer', optional: false, nullable: false, }, - noteFavoritesCount: { - type: 'integer', - optional: false, nullable: false, - }, pageLikesCount: { type: 'integer', optional: false, nullable: false, @@ -110,7 +106,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { +export default define(meta, paramDef, async (ps, me) => { const user = await Users.findOneBy({ id: ps.userId }); if (user == null) { throw new ApiError('NO_SUCH_USER'); @@ -141,22 +137,6 @@ export default define(meta, paramDef, async (ps) => { .innerJoin('vote.note', 'note') .where('note.userId = :userId', { userId: user.id }) .getCount(), - localFollowingCount: Followings.createQueryBuilder('following') - .where('following.followerId = :userId', { userId: user.id }) - .andWhere('following.followeeHost IS NULL') - .getCount(), - remoteFollowingCount: Followings.createQueryBuilder('following') - .where('following.followerId = :userId', { userId: user.id }) - .andWhere('following.followeeHost IS NOT NULL') - .getCount(), - localFollowersCount: Followings.createQueryBuilder('following') - .where('following.followeeId = :userId', { userId: user.id }) - .andWhere('following.followerHost IS NULL') - .getCount(), - remoteFollowersCount: Followings.createQueryBuilder('following') - .where('following.followeeId = :userId', { userId: user.id }) - .andWhere('following.followerHost IS NOT NULL') - .getCount(), sentReactionsCount: NoteReactions.createQueryBuilder('reaction') .where('reaction.userId = :userId', { userId: user.id }) .getCount(), @@ -164,9 +144,6 @@ export default define(meta, paramDef, async (ps) => { .innerJoin('reaction.note', 'note') .where('note.userId = :userId', { userId: user.id }) .getCount(), - noteFavoritesCount: NoteFavorites.createQueryBuilder('favorite') - .where('favorite.userId = :userId', { userId: user.id }) - .getCount(), pageLikesCount: PageLikes.createQueryBuilder('like') .where('like.userId = :userId', { userId: user.id }) .getCount(), @@ -180,8 +157,39 @@ export default define(meta, paramDef, async (ps) => { driveUsage: DriveFiles.calcDriveUsageOf(user.id), }); - result.followingCount = result.localFollowingCount + result.remoteFollowingCount; - result.followersCount = result.localFollowersCount + result.remoteFollowersCount; + const ffVisible = await Users.areFollowersVisibleTo(user, me); + if (ffVisible) { + const follows = await awaitAll({ + localFollowingCount: Followings.createQueryBuilder('following') + .where('following.followerId = :userId', { userId: user.id }) + .andWhere('following.followeeHost IS NULL') + .getCount(), + remoteFollowingCount: Followings.createQueryBuilder('following') + .where('following.followerId = :userId', { userId: user.id }) + .andWhere('following.followeeHost IS NOT NULL') + .getCount(), + localFollowersCount: Followings.createQueryBuilder('following') + .where('following.followeeId = :userId', { userId: user.id }) + .andWhere('following.followerHost IS NULL') + .getCount(), + remoteFollowersCount: Followings.createQueryBuilder('following') + .where('following.followeeId = :userId', { userId: user.id }) + .andWhere('following.followerHost IS NOT NULL') + .getCount(), + }); + + Object.assign(result, follows); + + result.followingCount = result.localFollowingCount + result.remoteFollowingCount; + result.followersCount = result.localFollowersCount + result.remoteFollowersCount; + + // store the updated counts in the user table to potentially fix the cache + Users.update(user.id, { + followersCount: result.followersCount, + followingCount: result.followingCount, + notesCount: result.notesCount, + }); + } return result; }); diff --git a/packages/backend/src/server/api/error.ts b/packages/backend/src/server/api/error.ts index 201c234b7..d5ec24f71 100644 --- a/packages/backend/src/server/api/error.ts +++ b/packages/backend/src/server/api/error.ts @@ -68,10 +68,6 @@ export const errors: Record message: 'That note is already added to that clip.', httpStatusCode: 409, }, - ALREADY_FAVORITED: { - message: 'That note is already favorited.', - httpStatusCode: 409, - }, ALREADY_FOLLOWING: { message: 'You are already following that user.', httpStatusCode: 409, @@ -332,10 +328,6 @@ export const errors: Record message: 'That note is not added to that clip.', httpStatusCode: 409, }, - NOT_FAVORITED: { - message: 'You have not favorited that note.', - httpStatusCode: 409, - }, NOT_FOLLOWING: { message: 'You are not following that user.', httpStatusCode: 409, diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts index fe0c4450a..554596e3a 100644 --- a/packages/backend/src/server/api/index.ts +++ b/packages/backend/src/server/api/index.ts @@ -10,7 +10,7 @@ import cors from '@koa/cors'; import { Instances, AccessTokens, Users } from '@/models/index.js'; import config from '@/config/index.js'; -import endpoints from './endpoints.js'; +import { endpoints } from './endpoints.js'; import { handler } from './api-handler.js'; import signup from './private/signup.js'; import signin from './private/signin.js'; diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index 0a7fcf667..9dc1c89cf 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -2,7 +2,7 @@ import config from '@/config/index.js'; import { kinds } from '@/misc/api-permissions.js'; import { I18n } from '@/misc/i18n.js'; import { errors as errorDefinitions } from '@/server/api/error.js'; -import endpoints from '@/server/api/endpoints.js'; +import { endpoints } from '@/server/api/endpoints.js'; import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; import { httpCodes } from './http-codes.js'; diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts index b09262bb1..75d2a56de 100644 --- a/packages/backend/src/server/api/private/signin.ts +++ b/packages/backend/src/server/api/private/signin.ts @@ -47,7 +47,7 @@ export default async (ctx: Koa.Context) => { const user = await Users.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull(), - isDeleted: false, + isDeleted: IsNull(), }) as ILocalUser; if (user == null) { diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts index 797443afb..5a1c79831 100644 --- a/packages/backend/src/server/api/streaming.ts +++ b/packages/backend/src/server/api/streaming.ts @@ -6,7 +6,7 @@ import { SECOND, MINUTE } from '@/const.js'; import { subscriber as redisClient } from '@/db/redis.js'; import { Users } from '@/models/index.js'; import { Connection } from './stream/index.js'; -import authenticate from './authenticate.js'; +import { authenticate } from './authenticate.js'; export const initializeStreamingServer = (server: http.Server): void => { // Init websocket server @@ -44,18 +44,21 @@ export const initializeStreamingServer = (server: http.Server): void => { const main = new Connection(socket, ev, user, app); // ping/pong mechanism - let pingTimeout = null; - function startHeartbeat() { - if (pingTimeout) clearTimeout(pingTimeout); - + let pingTimeout: NodeJS.Timeout | null = null; + let disconnectTimeout = setTimeout(() => { + socket.terminate(); + }, 60 * SECOND);; + function sendPing() { socket.ping(); pingTimeout = setTimeout(() => { - socket.terminate(); + sendPing(); }, 30 * SECOND); } - startHeartbeat(); - socket.on('ping', () => { startHeartbeat(); }); - socket.on('pong', () => { startHeartbeat(); }); + function onPong() { + disconnectTimeout.refresh() + } + sendPing(); + socket.on('pong', onPong); // keep user "online" while a stream is connected const intervalId = user ? setInterval(() => { @@ -75,6 +78,7 @@ export const initializeStreamingServer = (server: http.Server): void => { redisClient.off('message', onRedisMessage); if (intervalId) clearInterval(intervalId); if (pingTimeout) clearTimeout(pingTimeout); + if (disconnectTimeout) clearTimeout(disconnectTimeout); }); }); }); diff --git a/packages/backend/src/server/file/index.ts b/packages/backend/src/server/file/index.ts index 4c4707e61..56bf14f9a 100644 --- a/packages/backend/src/server/file/index.ts +++ b/packages/backend/src/server/file/index.ts @@ -8,7 +8,7 @@ import { dirname } from 'node:path'; import Koa from 'koa'; import cors from '@koa/cors'; import Router from '@koa/router'; -import sendDriveFile from './send-drive-file.js'; +import { sendDriveFile } from './send-drive-file.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); diff --git a/packages/backend/src/server/file/send-drive-file.ts b/packages/backend/src/server/file/send-drive-file.ts index 0ae16d94c..990dd98fe 100644 --- a/packages/backend/src/server/file/send-drive-file.ts +++ b/packages/backend/src/server/file/send-drive-file.ts @@ -27,8 +27,7 @@ const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => ctx.set('Cache-Control', 'max-age=300'); }; -// eslint-disable-next-line import/no-default-export -export default async function(ctx: Koa.Context) { +export async function sendDriveFile(ctx: Koa.Context) { const key = ctx.params.key; // Fetch drive file @@ -49,7 +48,7 @@ export default async function(ctx: Koa.Context) { const isWebpublic = file.webpublicAccessKey === key; if (!file.storedInternal) { - if (file.isLink && file.uri) { // ๆœŸ้™ๅˆ‡ใ‚Œใƒชใƒขใƒผใƒˆใƒ•ใ‚กใ‚คใƒซ + if (file.isLink && file.uri) { // expired remote file const [path, cleanup] = await createTemp(); try { diff --git a/packages/backend/src/server/web/feed.ts b/packages/backend/src/server/web/feed.ts index b83ccf188..497d677c6 100644 --- a/packages/backend/src/server/web/feed.ts +++ b/packages/backend/src/server/web/feed.ts @@ -4,7 +4,7 @@ import config from '@/config/index.js'; import { User } from '@/models/entities/user.js'; import { Notes, DriveFiles, UserProfiles, Users } from '@/models/index.js'; -export default async function(user: User) { +export async function packFeed(user: User) { const author = { link: `${config.url}/@${user.username}`, name: user.name || user.username, diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 7fe462d43..2f9a76c84 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -26,7 +26,7 @@ import { MINUTE, DAY } from '@/const.js'; import { genOpenapiSpec } from '../api/openapi/gen-spec.js'; import { urlPreviewHandler } from './url-preview.js'; import { manifestHandler } from './manifest.js'; -import packFeed from './feed.js'; +import { packFeed } from './feed.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -223,7 +223,7 @@ const getFeed = async (acct: string) => { usernameLower: username.toLowerCase(), host: host ?? IsNull(), isSuspended: false, - isDeleted: false, + isDeleted: IsNull(), }); return user && await packFeed(user); @@ -273,7 +273,7 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => { usernameLower: username.toLowerCase(), host: host ?? IsNull(), isSuspended: false, - isDeleted: false, + isDeleted: IsNull(), }); if (user != null) { @@ -306,7 +306,7 @@ router.get('/users/:user', async ctx => { id: ctx.params.user, host: IsNull(), isSuspended: false, - isDeleted: false, + isDeleted: IsNull(), }); if (user == null) { @@ -423,7 +423,7 @@ router.get('/@:user/pages/:page', async (ctx, next) => { usernameLower: username.toLowerCase(), host: host ?? IsNull(), isSuspended: false, - isDeleted: false, + isDeleted: IsNull(), }); if (user == null) return; diff --git a/packages/backend/src/services/blocking/delete.ts b/packages/backend/src/services/blocking/delete.ts index 82f92f05a..c26f1ac54 100644 --- a/packages/backend/src/services/blocking/delete.ts +++ b/packages/backend/src/services/blocking/delete.ts @@ -2,13 +2,13 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import { renderBlock } from '@/remote/activitypub/renderer/block.js'; import renderUndo from '@/remote/activitypub/renderer/undo.js'; import { deliver } from '@/queue/index.js'; -import { CacheableUser } from '@/models/entities/user.js'; +import { User } from '@/models/entities/user.js'; import { Blockings, Users } from '@/models/index.js'; import Logger from '../logger.js'; const logger = new Logger('blocking/delete'); -export default async function(blocker: CacheableUser, blockee: CacheableUser) { +export default async function(blocker: User, blockee: User) { const blocking = await Blockings.findOneBy({ blockerId: blocker.id, blockeeId: blockee.id, diff --git a/packages/backend/src/services/delete-account.ts b/packages/backend/src/services/delete-account.ts index 2fa6e004b..a52b5b5d4 100644 --- a/packages/backend/src/services/delete-account.ts +++ b/packages/backend/src/services/delete-account.ts @@ -9,7 +9,7 @@ export async function deleteAccount(user: { }): Promise { await Promise.all([ Users.update(user.id, { - isDeleted: true, + isDeleted: -1, }), // revoke all of the users access tokens to block API access AccessTokens.delete({ diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index 9cdade177..25ca7103c 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -2,9 +2,10 @@ import * as fs from 'node:fs'; import { v4 as uuid } from 'uuid'; import S3 from 'aws-sdk/clients/s3.js'; -import { IsNull } from 'typeorm'; +import { In, IsNull } from 'typeorm'; import sharp from 'sharp'; +import { db } from '@/db/postgre.js'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; import { publishMainStream, publishDriveStream } from '@/services/stream.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; @@ -290,25 +291,36 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, _type: string if (result) logger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`); } -async function deleteOldFile(user: IRemoteUser): Promise { - const q = DriveFiles.createQueryBuilder('file') - .where('file.userId = :userId', { userId: user.id }) - .andWhere('NOT file.isLink'); +async function expireOldFiles(user: IRemoteUser, driveCapacity: number): Promise { + // Delete as many files as necessary so the total usage is below driveCapacity, + // oldest files first, and exclude avatar and banner. + // + // Using a window function, i.e. `OVER (ORDER BY "createdAt" DESC)` means that + // the `SUM` will be a running total. + const exceededFileIds = await db.query('SELECT "id" FROM (' + + 'SELECT "id", SUM("size") OVER (ORDER BY "createdAt" DESC) AS "total" FROM "drive_file" WHERE "userId" = $1 AND NOT "isLink"' + + (user.avatarId ? ' AND "id" != $2' : '') + + (user.bannerId ? ' AND "id" != $3' : '') + + ') AS "totals" WHERE "total" > $4', + [ + user.id, + user.avatarId ?? '', + user.bannerId ?? '', + driveCapacity, + ] + ); - if (user.avatarId) { - q.andWhere('file.id != :avatarId', { avatarId: user.avatarId }); + if (exceededFileIds.length === 0) { + // no files to expire, avatar and banner if present are already the only files + throw new Error('remote user drive quota met by avatar and banner'); } - if (user.bannerId) { - q.andWhere('file.id != :bannerId', { bannerId: user.bannerId }); - } + const files = await DriveFiles.findBy({ + id: In(exceededFileIds.map(x => x.id)), + }); - q.orderBy('file.id', 'ASC'); - - const oldFile = await q.getOne(); - - if (oldFile) { - deleteFile(oldFile, true); + for (const file of files) { + deleteFile(file, true); } } @@ -373,19 +385,20 @@ export async function addFile({ //#region Check drive usage if (user && !isLink) { const usage = await DriveFiles.calcDriveUsageOf(user.id); + const isLocalUser = Users.isLocalUser(user); const instance = await fetchMeta(); - const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); + const driveCapacity = 1024 * 1024 * (isLocalUser ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); // If usage limit exceeded if (usage + info.size > driveCapacity) { - if (Users.isLocalUser(user)) { + if (isLocalUser) { throw new Error('no-free-space'); } else { - // delete oldest file (excluding banner and avatar) - deleteOldFile(await Users.findOneByOrFail({ id: user.id }) as IRemoteUser); + // delete older files to make space for new file + expireOldFiles(await Users.findOneByOrFail({ id: user.id }) as IRemoteUser, driveCapacity - info.size); } } } diff --git a/packages/backend/src/services/drive/delete-file.ts b/packages/backend/src/services/drive/delete-file.ts index 2fe8993a8..3938c7116 100644 --- a/packages/backend/src/services/drive/delete-file.ts +++ b/packages/backend/src/services/drive/delete-file.ts @@ -60,14 +60,14 @@ export async function deleteFileSync(file: DriveFile, isExpired = false): Promis await Promise.all(promises); } - postProcess(file, isExpired); + await postProcess(file, isExpired); } async function postProcess(file: DriveFile, isExpired = false): Promise { // Turn into a direct link after expiring a remote file. if (isExpired && file.userHost != null && file.uri != null) { const id = uuid(); - DriveFiles.update(file.id, { + await DriveFiles.update(file.id, { isLink: true, url: file.uri, thumbnailUrl: null, @@ -78,14 +78,14 @@ async function postProcess(file: DriveFile, isExpired = false): Promise { webpublicAccessKey: 'webpublic-' + id, }); } else { - DriveFiles.delete(file.id); + await DriveFiles.delete(file.id); } // update statistics - driveChart.update(file, false); - perUserDriveChart.update(file, false); + await driveChart.update(file, false); + await perUserDriveChart.update(file, false); if (file.userHost != null) { - instanceChart.updateDrive(file, false); + await instanceChart.updateDrive(file, false); } } diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts index e8f3793b7..912ac25f6 100644 --- a/packages/backend/src/services/drive/upload-from-url.ts +++ b/packages/backend/src/services/drive/upload-from-url.ts @@ -60,10 +60,7 @@ export async function uploadFromUrl({ logger.succ(`Got: ${driveFile.id}`); return driveFile; } catch (e) { - logger.error(`Failed to create drive file: ${e}`, { - url, - e, - }); + logger.error(`Failed to create drive file: ${e}`); throw e; } finally { cleanup(); diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts index a3bfc8a0a..67fb8407c 100644 --- a/packages/backend/src/services/logger.ts +++ b/packages/backend/src/services/logger.ts @@ -2,9 +2,8 @@ import cluster from 'node:cluster'; import chalk from 'chalk'; import convertColor from 'color-convert'; import { format as dateFormat } from 'date-fns'; -import * as SyslogPro from 'syslog-pro'; import config from '@/config/index.js'; -import { envOption } from '@/env.js'; +import { envOption, LOG_LEVELS } from '@/env.js'; import type { KEYWORD } from 'color-convert/conversions.js'; type Domain = { @@ -12,16 +11,19 @@ type Domain = { color?: KEYWORD; }; -type Level = 'error' | 'success' | 'warning' | 'debug' | 'info'; +export type Level = LOG_LEVELS[keyof LOG_LEVELS]; /** - * Class that facilitates recording log messages to the console and optionally a syslog server. + * Class that facilitates recording log messages to the console. */ export default class Logger { private domain: Domain; private parentLogger: Logger | null = null; private store: boolean; - private syslogClient: SyslogPro.RFC5424 | null = null; + /** + * Messages below this level will be discarded. + */ + private minLevel: Level; /** * Create a logger instance. @@ -29,26 +31,13 @@ export default class Logger { * @param color Log message color * @param store Whether to store messages */ - constructor(domain: string, color?: KEYWORD, store = true) { + constructor(domain: string, color?: KEYWORD, store = true, minLevel: Level = LOG_LEVELS.info) { this.domain = { name: domain, color, }; this.store = store; - - if (config.syslog) { - this.syslogClient = new SyslogPro.RFC5424({ - applicationName: 'FoundKey', - timestamp: true, - includeStructuredData: true, - color: true, - extendedColor: true, - server: { - target: config.syslog.host, - port: config.syslog.port, - }, - }); - } + this.minLevel = minLevel; } /** @@ -58,69 +47,89 @@ export default class Logger { * @param store Whether to store messages * @returns A Logger instance whose parent logger is this instance. */ - public createSubLogger(domain: string, color?: KEYWORD, store = true): Logger { - const logger = new Logger(domain, color, store); + public createSubLogger(domain: string, color?: KEYWORD, store = true, minLevel: Level = LOG_LEVELS.info): Logger { + const logger = new Logger(domain, color, store, minLevel); logger.parentLogger = this; return logger; } - private log(level: Level, message: string, data?: Record | null, important = false, subDomains: Domain[] = [], _store = true): void { - if (envOption.quiet) return; - const store = _store && this.store && (level !== 'debug'); + /** + * Log a message. + * @param level Indicates the level of this particular message. If it is + * less than the minimum level configured, the message will be discarded. + * @param message The message to be logged. + * @param important Whether to highlight this message as especially important. + * @param subDomains Names of sub-loggers to be added. + */ + private log(level: Level, message: string, important = false, subDomains: Domain[] = [], _store = true): void { + const store = _store && this.store; + // Check against the configured log level. + if (level < this.minLevel) return; + + // If this logger has a parent logger, delegate the actual logging to it, + // so the parent domain(s) will be logged properly. if (this.parentLogger) { - this.parentLogger.log(level, message, data, important, [this.domain].concat(subDomains), store); + this.parentLogger.log(level, message, important, [this.domain].concat(subDomains), store); return; } const time = dateFormat(new Date(), 'HH:mm:ss'); const worker = cluster.isPrimary ? '*' : cluster.worker?.id; - const l = - level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') : - level === 'warning' ? chalk.yellow('WARN') : - level === 'success' ? important ? chalk.bgGreen.white('DONE') : chalk.green('DONE') : - level === 'debug' ? chalk.gray('VERB') : - chalk.blue('INFO'); const domains = [this.domain].concat(subDomains).map(d => d.color ? chalk.rgb(...convertColor.keyword.rgb(d.color))(d.name) : chalk.white(d.name)); - const m = - level === 'error' ? chalk.red(message) : - level === 'warning' ? chalk.yellow(message) : - level === 'success' ? chalk.green(message) : - level === 'debug' ? chalk.gray(message) : - message; - let log = `${l} ${worker}\t[${domains.join(' ')}]\t${m}`; + let levelDisplay; + let messageDisplay; + switch (level) { + case LOG_LEVELS.error: + if (important) { + levelDisplay = chalk.bgRed.white('ERR '); + } else { + levelDisplay = chalk.red('ERR '); + } + messageDisplay = chalk.red(message); + break; + case LOG_LEVELS.warning: + levelDisplay = chalk.yellow('WARN'); + messageDisplay = chalk.yellow(message); + break; + case LOG_LEVELS.success: + if (important) { + levelDisplay = chalk.bgGreen.white('DONE'); + } else { + levelDisplay = chalk.green('DONE'); + } + messageDisplay = chalk.green(message); + break; + case LOG_LEVELS.info: + levelDisplay = chalk.blue('INFO'); + messageDisplay = message; + break; + case LOG_LEVELS.debug: default: + levelDisplay = chalk.gray('VERB'); + messageDisplay = chalk.gray(message); + break; + } + + let log = `${levelDisplay} ${worker}\t[${domains.join(' ')}]\t${messageDisplay}`; if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log; console.log(important ? chalk.bold(log) : log); - - if (store) { - if (this.syslogClient) { - const send = - level === 'error' ? this.syslogClient.error : - level === 'warning' ? this.syslogClient.warning : - this.syslogClient.info; - - send.bind(this.syslogClient)(message).catch(() => {}); - } - } } /** * Log an error message. * Use in situations where execution cannot be continued. * @param err Error or string containing an error message - * @param data Data relating to the error * @param important Whether this error is important */ - public error(err: string | Error, data: Record = {}, important = false): void { + public error(err: string | Error, important = false): void { if (err instanceof Error) { - data.e = err; - this.log('error', err.toString(), data, important); + this.log(LOG_LEVELS.error, err.toString(), important); } else if (typeof err === 'object') { - this.log('error', `${(err as any).message || (err as any).name || err}`, data, important); + this.log(LOG_LEVELS.error, `${(err as any).message || (err as any).name || err}`, important); } else { - this.log('error', `${err}`, data, important); + this.log(LOG_LEVELS.error, `${err}`, important); } } @@ -128,45 +137,39 @@ export default class Logger { * Log a warning message. * Use in situations where execution can continue but needs to be improved. * @param message Warning message - * @param data Data relating to the warning * @param important Whether this warning is important */ - public warn(message: string, data?: Record | null, important = false): void { - this.log('warning', message, data, important); + public warn(message: string, important = false): void { + this.log(LOG_LEVELS.warning, message, important); } /** * Log a success message. * Use in situations where something has been successfully done. * @param message Success message - * @param data Data relating to the success * @param important Whether this success message is important */ - public succ(message: string, data?: Record | null, important = false): void { - this.log('success', message, data, important); + public succ(message: string, important = false): void { + this.log(LOG_LEVELS.success, message, important); } /** * Log a debug message. * Use for debugging (information needed by developers but not required by users). * @param message Debug message - * @param data Data relating to the debug message * @param important Whether this debug message is important */ - public debug(message: string, data?: Record | null, important = false): void { - if (process.env.NODE_ENV !== 'production' || envOption.verbose) { - this.log('debug', message, data, important); - } + public debug(message: string, important = false): void { + this.log(LOG_LEVELS.debug, message, important); } /** * Log an informational message. * Use when something needs to be logged but doesn't fit into other levels. * @param message Info message - * @param data Data relating to the info message * @param important Whether this info message is important */ - public info(message: string, data?: Record | null, important = false): void { - this.log('info', message, data, important); + public info(message: string, important = false): void { + this.log(LOG_LEVELS.info, message, important); } } diff --git a/packages/backend/src/services/messages/create.ts b/packages/backend/src/services/messages/create.ts index 4a0ea53a8..dd31d8794 100644 --- a/packages/backend/src/services/messages/create.ts +++ b/packages/backend/src/services/messages/create.ts @@ -1,5 +1,5 @@ import { Not } from 'typeorm'; -import { CacheableUser, User } from '@/models/entities/user.js'; +import { User } from '@/models/entities/user.js'; import { UserGroup } from '@/models/entities/user-group.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { MessagingMessages, UserGroupJoinings, Mutings, Users } from '@/models/index.js'; @@ -13,7 +13,7 @@ import renderCreate from '@/remote/activitypub/renderer/create.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import { deliver } from '@/queue/index.js'; -export async function createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: CacheableUser | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) { +export async function createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: User | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) { const message = { id: genId(), createdAt: new Date(), diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 8f48bc12a..9c0500bda 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -1,7 +1,6 @@ import { ArrayOverlap, Not, In } from 'typeorm'; import * as mfm from 'mfm-js'; import { db } from '@/db/postgre.js'; -import es from '@/db/elasticsearch.js'; import { publishMainStream, publishNotesStream } from '@/services/stream.js'; import { DeliverManager } from '@/remote/activitypub/deliver-manager.js'; import renderNote from '@/remote/activitypub/renderer/note.js'; @@ -9,14 +8,13 @@ import renderCreate from '@/remote/activitypub/renderer/create.js'; import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import { resolveUser } from '@/remote/resolve-user.js'; -import config from '@/config/index.js'; import { concat } from '@/prelude/array.js'; import { insertNoteUnread } from '@/services/note/unread.js'; 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 { Note } from '@/models/entities/note.js'; -import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, MutedNotes, Channels, ChannelFollowings, NoteThreadMutings } from '@/models/index.js'; +import { Mutings, Users, NoteWatchings, Notes, Instances, MutedNotes, Channels, ChannelFollowings, NoteThreadMutings } from '@/models/index.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { App } from '@/models/entities/app.js'; import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; @@ -32,27 +30,16 @@ import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { getAntennas } from '@/misc/antenna-cache.js'; import { endedPollNotificationQueue } from '@/queue/queues.js'; import { webhookDeliver } from '@/queue/index.js'; -import { Cache } from '@/misc/cache.js'; import { UserProfile } from '@/models/entities/user-profile.js'; import { getActiveWebhooks } from '@/misc/webhook-cache.js'; import { IActivity } from '@/remote/activitypub/type.js'; import { renderNoteOrRenoteActivity } from '@/remote/activitypub/renderer/note-or-renote.js'; -import { MINUTE } from '@/const.js'; import { updateHashtags } from '../update-hashtag.js'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; import { createNotification } from '../create-notification.js'; import { addNoteToAntenna } from '../add-note-to-antenna.js'; import { deliverToRelays } from '../relay.js'; - -const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>( - 5 * MINUTE, - () => UserProfiles.find({ - where: { - enableWordMute: true, - }, - select: ['userId', 'mutedWords'], - }), -); +import { mutedWordsCache, index } from './index.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -576,20 +563,6 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O } } -function index(note: Note): void { - if (note.text == null || config.elasticsearch == null) return; - - es.index({ - index: config.elasticsearch.index || 'misskey_note', - id: note.id.toString(), - body: { - text: normalizeForSearch(note.text), - userId: note.userId, - userHost: note.userHost, - }, - }); -} - async function notifyToWatchersOfRenotee(renote: Note, user: { id: User['id']; }, nm: NotificationManager, type: NotificationType): Promise { const watchers = await NoteWatchings.findBy({ noteId: renote.id, diff --git a/packages/backend/src/services/note/index.ts b/packages/backend/src/services/note/index.ts new file mode 100644 index 000000000..f2bd21db1 --- /dev/null +++ b/packages/backend/src/services/note/index.ts @@ -0,0 +1,32 @@ +import es from '@/db/elasticsearch.js'; +import config from '@/config/index.js'; +import { Note } from '@/models/entities/note.js'; +import { UserProfiles } from '@/models/index.js'; +import { normalizeForSearch } from '@/misc/normalize-for-search.js'; +import { Cache } from '@/misc/cache.js'; +import { UserProfile } from '@/models/entities/user-profile.js'; +import { MINUTE } from '@/const.js'; + +export const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>( + 5 * MINUTE, + () => UserProfiles.find({ + where: { + enableWordMute: true, + }, + select: ['userId', 'mutedWords'], + }), +); + +export function index(note: Note): void { + if (note.text == null || config.elasticsearch == null) return; + + es.index({ + index: config.elasticsearch.index || 'misskey_note', + id: note.id.toString(), + body: { + text: normalizeForSearch(note.text), + userId: note.userId, + userHost: note.userHost, + }, + }); +} diff --git a/packages/backend/src/services/note/polls/vote.ts b/packages/backend/src/services/note/polls/vote.ts index b86e7107d..cdd27da50 100644 --- a/packages/backend/src/services/note/polls/vote.ts +++ b/packages/backend/src/services/note/polls/vote.ts @@ -1,12 +1,12 @@ import { ArrayOverlap, Not } from 'typeorm'; import { publishNoteStream } from '@/services/stream.js'; -import { CacheableUser } from '@/models/entities/user.js'; +import { User } from '@/models/entities/user.js'; import { Note } from '@/models/entities/note.js'; import { PollVotes, NoteWatchings, Polls, Blockings, NoteThreadMutings } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { createNotification } from '@/services/create-notification.js'; -export async function vote(user: CacheableUser, note: Note, choice: number): Promise { +export async function vote(user: User, note: Note, choice: number): Promise { const poll = await Polls.findOneBy({ noteId: note.id }); if (poll == null) throw new Error('poll not found'); diff --git a/packages/backend/src/services/stream.ts b/packages/backend/src/services/stream.ts index 0119d7fdf..87ddee19d 100644 --- a/packages/backend/src/services/stream.ts +++ b/packages/backend/src/services/stream.ts @@ -95,9 +95,7 @@ class Publisher { }; } -const publisher = new Publisher(); - -export default publisher; +export const publisher = new Publisher(); export const publishInternalEvent = publisher.publishInternalEvent; export const publishUserEvent = publisher.publishUserEvent; diff --git a/packages/backend/src/services/suspend-user.ts b/packages/backend/src/services/suspend-user.ts index 11e6266a0..7ed6bff80 100644 --- a/packages/backend/src/services/suspend-user.ts +++ b/packages/backend/src/services/suspend-user.ts @@ -6,6 +6,9 @@ import { User } from '@/models/entities/user.js'; import { Users } from '@/models/index.js'; import { publishInternalEvent } from '@/services/stream.js'; +/** + * Sends an internal event and for local users queues the delete activites. + */ export async function doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise { publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); @@ -15,6 +18,6 @@ export async function doPostSuspend(user: { id: User['id']; host: User['host'] } // deliver to all of known network const dm = new DeliverManager(user, content); dm.addEveryone(); - await dm.execute(); + await dm.execute(user.id); } } diff --git a/packages/backend/src/services/user-cache.ts b/packages/backend/src/services/user-cache.ts index b4bbb6f1a..9638f8101 100644 --- a/packages/backend/src/services/user-cache.ts +++ b/packages/backend/src/services/user-cache.ts @@ -1,20 +1,20 @@ import { IsNull } from 'typeorm'; -import { CacheableLocalUser, ILocalUser, User } from '@/models/entities/user.js'; +import { ILocalUser, User } from '@/models/entities/user.js'; import { Users } from '@/models/index.js'; import { Cache } from '@/misc/cache.js'; import { subscriber } from '@/db/redis.js'; export const userByIdCache = new Cache( Infinity, - async (id) => await Users.findOneBy({ id, isDeleted: false }) ?? undefined, + async (id) => await Users.findOneBy({ id, isDeleted: IsNull() }) ?? undefined, ); -export const localUserByNativeTokenCache = new Cache( +export const localUserByNativeTokenCache = new Cache( Infinity, - async (token) => await Users.findOneBy({ token, host: IsNull(), isDeleted: false }) as ILocalUser | null ?? undefined, + async (token) => await Users.findOneBy({ token, host: IsNull(), isDeleted: IsNull() }) as ILocalUser | null ?? undefined, ); export const uriPersonCache = new Cache( Infinity, - async (uri) => await Users.findOneBy({ uri, isDeleted: false }) ?? undefined, + async (uri) => await Users.findOneBy({ uri, isDeleted: IsNull() }) ?? undefined, ); subscriber.on('message', async (_, data) => { diff --git a/packages/backend/test/api.ts b/packages/backend/test/api.ts index ae46ae92d..222c8d4d4 100644 --- a/packages/backend/test/api.ts +++ b/packages/backend/test/api.ts @@ -10,7 +10,7 @@ describe('API', () => { let bob: any; let carol: any; - before(async () => { + before(async function() { this.timeout(0); p = await startServer(); alice = await signup({ username: 'alice' }); diff --git a/packages/client/package.json b/packages/client/package.json index d9a994d4a..545c58c9f 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "13.0.0-preview4", + "version": "13.0.0-preview5", "private": true, "scripts": { "watch": "vite build --watch --mode development", @@ -34,7 +34,7 @@ "json5": "2.2.1", "katex": "0.16.0", "matter-js": "0.18.0", - "mfm-js": "0.22.1", + "mfm-js": "0.23.3", "photoswipe": "5.2.8", "prismjs": "1.28.0", "punycode": "2.1.1", diff --git a/packages/client/src/components/global/misskey-flavored-markdown.vue b/packages/client/src/components/global/misskey-flavored-markdown.vue index 72ab8b9ce..eb0c5f34b 100644 --- a/packages/client/src/components/global/misskey-flavored-markdown.vue +++ b/packages/client/src/components/global/misskey-flavored-markdown.vue @@ -56,6 +56,14 @@ withDefaults(defineProps<{ } } +._mfm_small_ { + opacity: 0.7; +} + +._mfm_small_ ._mfm_small_{ + opacity: initial; +} + @keyframes mfm-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts index 1107edcd6..2d04a9731 100644 --- a/packages/client/src/components/mfm.ts +++ b/packages/client/src/components/mfm.ts @@ -10,7 +10,6 @@ import MkSearch from '@/components/mfm-search.vue'; import MkSparkle from '@/components/sparkle.vue'; import MkA from '@/components/global/a.vue'; import { host } from '@/config'; -import { MFM_TAGS } from '@/scripts/mfm-tags'; export default defineComponent({ props: { @@ -42,7 +41,7 @@ export default defineComponent({ render() { if (this.text == null || this.text === '') return; - const ast = (this.plain ? mfm.parsePlain : mfm.parse)(this.text, { fnNameList: MFM_TAGS }); + const ast = (this.plain ? mfm.parseSimple : mfm.parse)(this.text); const validTime = (t: string | true) => { if (typeof t !== 'string') return null; @@ -139,17 +138,17 @@ export default defineComponent({ } case 'x2': { return h('span', { - class: 'mfm-x2', + class: 'mfm-x2' }, genEl(token.children)); } case 'x3': { return h('span', { - class: 'mfm-x3', + class: 'mfm-x3' }, genEl(token.children)); } case 'x4': { return h('span', { - class: 'mfm-x4', + class: 'mfm-x4' }, genEl(token.children)); } case 'font': { @@ -185,6 +184,30 @@ export default defineComponent({ style = `transform: rotate(${degrees}deg); transform-origin: center center;`; break; } + case 'position': { + const x = parseFloat(token.props.args.x ?? '0'); + const y = parseFloat(token.props.args.y ?? '0'); + style = `transform: translate(${x}em, ${y}em);`; + break; + } + case 'scale': { + const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5); + const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5); + style = `transform: scale(${x}, ${y});`; + break; + } + case 'fg': { + let color = token.props.args.color ?? 'f00'; + if (!/^([0-9a-f]{3}){1,2}$/i.test(color)) color = 'f00'; + style = `color: #${color};`; + break; + } + case 'bg': { + let color = token.props.args.color ?? '0f0'; + if (!/^([0-9a-f]{3}){1,2}$/i.test(color)) color = '0f0'; + style = `background-color: #${color};`; + break; + } } if (style == null) { return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']); @@ -197,7 +220,7 @@ export default defineComponent({ case 'small': { return h('small', { - style: 'opacity: 0.7;', + class: '_mfm_small_' }, genEl(token.children)); } diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue index 04b7d3800..bbe7d5451 100644 --- a/packages/client/src/components/post-form.vue +++ b/packages/client/src/components/post-form.vue @@ -21,7 +21,7 @@ diff --git a/packages/client/src/components/visibility-picker.vue b/packages/client/src/components/visibility-picker.vue index 50a6ff667..1b0707578 100644 --- a/packages/client/src/components/visibility-picker.vue +++ b/packages/client/src/components/visibility-picker.vue @@ -16,7 +16,7 @@