diff --git a/.config/docker_example.yml b/.config/docker_example.yml index bd5eab492..f8124bc9d 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -114,11 +114,6 @@ id: 'aid' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Syslog option -#syslog: -# host: localhost -# port: 514 - # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/.config/example.yml b/.config/example.yml index cabf167fb..a19b5d04e 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -114,11 +114,6 @@ id: 'aid' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Syslog option -#syslog: -# host: localhost -# port: 514 - # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 @@ -135,6 +130,7 @@ proxyBypassHosts: #proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 # Media Proxy +# Reference Implementation: https://github.com/misskey-dev/media-proxy #mediaProxy: https://example.com/proxy # Proxy remote files (default: false) diff --git a/.dockerignore b/.dockerignore index 854e643d3..8f984831e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -16,9 +16,15 @@ files/ misskey-assets/ fluent-emojis/ .pnp.* + +# .yarn関連 .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/sdks !.yarn/versions + +.idea/ +packages/*/.vscode/ +packages/backend/test/docker-compose.yml diff --git a/.dockleignore b/.dockleignore new file mode 100644 index 000000000..2f9326645 --- /dev/null +++ b/.dockleignore @@ -0,0 +1,3 @@ +DKL-DI-0005 +DKL-DI-0006 +DKL-LI-0003 diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml index b711211cb..59648e566 100644 --- a/.github/workflows/docker-develop.yml +++ b/.github/workflows/docker-develop.yml @@ -14,6 +14,8 @@ jobs: steps: - name: Check out the repo uses: actions/checkout@v3.3.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2.3.0 - name: Docker meta id: meta uses: docker/metadata-action@v4 diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml new file mode 100644 index 000000000..9b79ee54f --- /dev/null +++ b/.github/workflows/dockle.yml @@ -0,0 +1,30 @@ +--- +name: Dockle + +on: + push: + branches: + - master + - develop + pull_request: + +jobs: + dockle: + runs-on: ubuntu-latest + env: + DOCKER_CONTENT_TRUST: 1 + steps: + - uses: actions/checkout@v3.2.0 + - run: | + curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb" + sudo dpkg -i dockle.deb + - run: | + cp .config/docker_example.env .config/docker.env + cp ./docker-compose.yml.example ./docker-compose.yml + - run: | + docker compose up -d web + docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest + - run: | + cmd="dockle --exit-code 1 misskey-web:latest ${image_name}" + echo "> ${cmd}" + eval "${cmd}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 41dfe7f41..309744e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,104 @@ You should also include the user name that made the change. --> +## 13.x.x (unreleased) + +### Improvements +- 非ログイン時にMiAuthを踏んだ際にMiAuthであることを表示する +- /auth/のUIをアップデート + +### Bugfixes +- + +## 13.5.4 (2023/02/09) + +### Improvements +- Server: UIのHTML(ノートなどの特別なページを除く)のキャッシュ時間を15秒から30秒に +- i/notificationsのレートリミットを緩和 + +### Bugfixes +- fix(client): validate url to improve security +- fix(client): dateの初期値が正常に入らない時がある + +## 13.5.3 (2023/02/09) + +### Improvements +- Client: デッキにチャンネルカラムを追加 + +## 13.5.2 (2023/02/08) + +### Changes +- Revert: perf(client): do not render custom emojis in user names + +### Bugfixes +- Client: register_note_view_interruptor not working +- Client: ログイントークンの再生成が出来ない + +## 13.5.0 (2023/02/08) + +### Changes +- perf(client): do not render custom emojis in user names + +### Improvements +- Client: disableShowingAnimatedImagesのデフォルト値をprefers-reduced-motionにする +- enhance(client): tweak medialist style + +### Bugfixes +- fix docker health check +- Client: MkEmojiPickerでもChromeで検索ダイアログで変換確定するとそのまま検索されてしまうのを修正 +- fix(mfm): default degree not used in rotate +- fix(server): validate urls from ap to improve security + +## 13.4.0 (2023/02/05) + +### Improvements +- ロールにアイコンを設定してユーザー名の横に表示できるように +- feat: timeline page for non-login users +- 実績の単なるラッキーの獲得確立を調整 +- Add Thai language support + +### Bugfixes +- fix(server): 自分のノートをお気に入りに登録しても実績解除される問題を修正 +- fix(server): clean up file in FileServer +- fix(server): Deny UNIX domain socket +- fix(server): validate filename and emoji name to improve security +- fix(client): validate input response in aiscript +- fix(client): add webhook delete button +- fix(client): tweak notification style +- fix(client): インラインコードを折り返して表示する + +## 13.3.3 (2023/02/04) + +### Bugfixes +- Server: improve security + +## 13.3.2 (2023/02/04) + +### Improvements +- 外部メディアプロキシへの対応を強化しました + 外部メディアプロキシのFastify実装を作りました + https://github.com/misskey-dev/media-proxy +- Server: improve performance + +### Bugfixes +- Client: validate urls to improve security + +## 13.3.1 (2023/02/04) + +### Bugfixes +- Client: カスタム絵文字にアニメーション画像を再生しない設定が適用されていない問題を修正 +- Client: オートコンプリートでUnicode絵文字がカスタム絵文字として表示されてしまうのを修正 +- Client: Fix Vue-plyr CORS issue +- Client: validate urls to improve security + +## 13.3.0 (2023/02/03) +### Changes +- twitter/github/discord連携機能が削除されました +- ハッシュタグごとのチャートが削除されました +- syslogのサポートが削除されました + +### Improvements +- ロールで広告の非表示が有効になっている場合は最初から広告を非表示にするように ## 13.2.6 (2023/02/01) ### Changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4689543d5..e53992678 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ Thank you for your PR! Before creating a PR, please check the following: - Check if there are any documents that need to be created or updated due to this change. - If you have added a feature or fixed a bug, please add a test case if possible. - Please make sure that tests and Lint are passed in advance. - - You can run it with `yarn test` and `yarn lint`. [See more info](#testing) + - You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing) - If this PR includes UI changes, please attach a screenshot in the text. Thanks for your cooperation 🤗 @@ -102,7 +102,7 @@ If your language is not listed in Crowdin, please open an issue. During development, it is useful to use the ``` -yarn dev +pnpm dev ``` command. @@ -112,7 +112,7 @@ command. - Service Worker is watched by esbuild. ## Testing -- Test codes are located in [`/test`](/test). +- Test codes are located in [`/packages/backend/test`](/packages/backend/test). ### Run test Create a config file. @@ -121,18 +121,18 @@ cp .github/misskey/test.yml .config/ ``` Prepare DB/Redis for testing. ``` -docker-compose -f packages/backend/test/docker-compose.yml up +docker compose -f packages/backend/test/docker-compose.yml up ``` Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. Run all test. ``` -yarn test +pnpm test ``` #### Run specify test ``` -yarn jest -- foo.ts +pnpm jest -- foo.ts ``` ### e2e tests @@ -177,9 +177,9 @@ vue-routerとの最大の違いは、niraxは複数のルーターが存在す これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。 ## Notes -### How to resolve conflictions occurred at yarn.lock? +### How to resolve conflictions occurred at pnpm-lock.yaml? -Just execute `yarn` to fix it. +Just execute `pnpm` to fix it. ### INSERTするときにはsaveではなくinsertを使用する #6441 @@ -265,7 +265,7 @@ MongoDBは`null`で返してきてたので、その感覚で`if (x === null)` ### Migration作成方法 packages/backendで: ```sh -yarn dlx typeorm migration:generate -d ormconfig.js -o +pnpm dlx typeorm migration:generate -d ormconfig.js -o ``` - 生成後、ファイルをmigration下に移してください diff --git a/Dockerfile b/Dockerfile index 3876b5f6c..0bfd24bd9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,7 @@ ARG NODE_ENV=production RUN git submodule update --init RUN pnpm build +RUN rm -rf .git/ FROM node:${NODE_VERSION}-slim AS runner @@ -41,10 +42,12 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ ; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \ && apt-get update \ && apt-get install -y --no-install-recommends \ - ffmpeg tini \ + ffmpeg tini curl \ && corepack enable \ && groupadd -g "${GID}" misskey \ - && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey + && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \ + && find / -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \ + && find / -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; USER misskey WORKDIR /misskey @@ -58,5 +61,6 @@ COPY --chown=misskey:misskey --from=builder /misskey/fluent-emojis /misskey/flue COPY --chown=misskey:misskey . ./ ENV NODE_ENV=production +HEALTHCHECK --interval=5s --retries=20 CMD ["/bin/bash", "/misskey/healthcheck.sh"] ENTRYPOINT ["/usr/bin/tini", "--"] CMD ["pnpm", "run", "migrateandstart"] diff --git a/ROADMAP.md b/ROADMAP.md index b2c5c8757..c95bb8d92 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,16 +6,13 @@ Also, the later tasks are more indefinite and are subject to change as developme This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development. - Make the number of type errors zero (backend) - - Probably need to switch some libraries to others that make it difficult to reduce type errors - - e.g. koa to fastify https://github.com/misskey-dev/misskey/issues/7537 - Improve CI - Fix tests - - mocha, jest, etc. do not support the combination of `TypeScript + ESM + Path alias`, and the tests currently do not work. - Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986 - Add more tests - - May need to implement a mechanism that allows for DI + - ~~May need to implement a mechanism that allows for DI~~ → Done ✔️ - https://github.com/misskey-dev/misskey/pull/9085 - - Measure coverage + - ~~Measure coverage~~ → Done ✔️ - https://github.com/misskey-dev/misskey/pull/9081 - Improve documentation - Refactoring diff --git a/chart/files/default.yml b/chart/files/default.yml index 862951d4d..4061ca3eb 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -133,11 +133,6 @@ id: "aid" # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Syslog option -#syslog: -# host: localhost -# port: 514 - # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/healthcheck.sh b/healthcheck.sh new file mode 100644 index 000000000..e97a3f063 --- /dev/null +++ b/healthcheck.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +PORT=$(grep '^port:' /misskey/.config/default.yml | awk 'NR==1{print $2; exit}') +curl -s -S -o /dev/null "http://localhost:${PORT}" diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 1ff72668e..5542e09b1 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1345,5 +1345,6 @@ _deck: tl: "الخيط الزمني" antenna: "الهوائيات" list: "القوائم" + channel: "القنوات" mentions: "الإشارات" direct: "مباشرة" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index c6e94896e..6f5d67639 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -1441,5 +1441,6 @@ _deck: tl: "টাইমলাইন" antenna: "অ্যান্টেনা" list: "লিস্ট" + channel: "চ্যানেলগুলি" mentions: "উল্লেখসমূহ" direct: "ডাইরেক্ট নোটগুলি" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index eb9ae6f87..926c173f8 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -804,4 +804,5 @@ _deck: tl: "Časová osa" antenna: "Antény" list: "Seznamy" + channel: "Kanály" mentions: "Zmínění" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index a3e8f221d..85c7a3e63 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -68,7 +68,7 @@ export: "Export" files: "Dateien" download: "Herunterladen" driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Notizen mit dieser Datei werden ebenso verschwinden." -unfollowConfirm: "Möchtest du {name} nicht mehr folgen?" +unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?" exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt." importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen." lists: "Listen" @@ -94,7 +94,7 @@ defaultNoteVisibility: "Standardsichtbarkeit" follow: "Folgen" followRequest: "Follow-Anfrage senden" followRequests: "Follow-Anfragen" -unfollow: "Nicht mehr folgen" +unfollow: "Entfolgen" followRequestPending: "Follow-Anfrage ausstehend" enterEmoji: "Gib ein Emoji ein" renote: "Renote" @@ -129,6 +129,7 @@ unblockConfirm: "Möchtest du diese Blockierung wirklich aufheben?" suspendConfirm: "Möchtest du diesen Benutzer wirklich sperren?" unsuspendConfirm: "Möchtest du diesen Benutzer wirklich entsperren?" selectList: "Liste auswählen" +selectChannel: "Kanal auswählen" selectAntenna: "Antenne auswählen" selectWidget: "Widget auswählen" editWidgets: "Widgets bearbeiten" @@ -1195,6 +1196,9 @@ _role: baseRole: "Rollenvorlage" useBaseValue: "Wert der Rollenvorlage verwenden" chooseRoleToAssign: "Zuzuweisende Rolle auswählen" + iconUrl: "Icon-URL" + asBadge: "Als Abzeichen anzeigen" + descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt." canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen" descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten." priority: "Priorität" @@ -1866,5 +1870,6 @@ _deck: tl: "Chronik" antenna: "Antennen" list: "Listen" + channel: "Kanal" mentions: "Erwähnungen" direct: "Direktnachrichten" diff --git a/locales/en-US.yml b/locales/en-US.yml index 4e2f18629..d9a34b899 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -68,7 +68,7 @@ export: "Export" files: "Files" download: "Download" driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Notes with this file attached will also be deleted." -unfollowConfirm: "Are you sure that you want to unfollow {name}?" +unfollowConfirm: "Are you sure you want to unfollow {name}?" exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed." importRequested: "You've requested an import. This may take a while." lists: "Lists" @@ -129,6 +129,7 @@ unblockConfirm: "Are you sure that you want to unblock this account?" suspendConfirm: "Are you sure that you want to suspend this account?" unsuspendConfirm: "Are you sure that you want to unsuspend this account?" selectList: "Select a list" +selectChannel: "Select a channel" selectAntenna: "Select an antenna" selectWidget: "Select a widget" editWidgets: "Edit widgets" @@ -1195,6 +1196,9 @@ _role: baseRole: "Role template" useBaseValue: "Use role template value" chooseRoleToAssign: "Select the role to assign" + iconUrl: "Icon URL" + asBadge: "Show as badge" + descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on." canEditMembersByModerator: "Allow moderators to edit the list of members for this role" descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users." priority: "Priority" @@ -1866,5 +1870,6 @@ _deck: tl: "Timeline" antenna: "Antennas" list: "List" + channel: "Channel" mentions: "Mentions" direct: "Direct notes" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 47799a091..f3cd85e6a 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -129,6 +129,7 @@ unblockConfirm: "¿Quiere dejar de bloquear esta cuenta?" suspendConfirm: "¿Quiere suspender esta cuenta?" unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?" selectList: "Seleccione una lista" +selectChannel: "Seleccionar canal" selectAntenna: "Seleccionar antena" selectWidget: "Seleccionar widget" editWidgets: "Editar widgets" @@ -509,7 +510,7 @@ objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir " serverLogs: "Registros del servidor" deleteAll: "Eliminar todos" showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo" -newNoteRecived: "Tienes una nota nuevo" +newNoteRecived: "Tienes una nota nueva" sounds: "Sonidos" sound: "Sonidos" listen: "Escuchar" @@ -918,17 +919,326 @@ tools: "Utilidades" cannotLoad: "No se puede cargar." numberOfProfileView: "Número de vistas de perfil" like: "¡Muy bien!" +unlike: "Quitar 'me gusta'" +numberOfLikes: "Cantidad de 'Me gusta'" show: "Apariencia" +neverShow: "No mostrar de nuevo" +remindMeLater: "Recordar después" +didYouLikeMisskey: "¿Te gusta Misskey?" +pleaseDonate: "Misskey es software libre, y es usado por {host} . Por favor, ¡considera donar al proyecto principal para que podamos continuar!" +roles: "Roles" +role: "Roles" +normalUser: "Usuario normal" +undefined: "Indefinido" +assign: "Asignar" +unassign: "Quitar" color: "Color" +manageCustomEmojis: "Administrar emojis personalizados" +youCannotCreateAnymore: "Se alcanzó el límite de creación" +cannotPerformTemporary: "Indisponible temporalmente" +cannotPerformTemporaryDescription: "Esta acción no se puede realizar porque se excedió el límite de ejecución. Espera un poco y prueba de nuevo." +preset: "Predefinido" +selectFromPresets: "Escoger desde predefinidos" +achievements: "Logros" +_achievements: + earnedAt: "Desbloqueado el" + _types: + _notes1: + title: "¡Hola Misskey!" + description: "Publicaste tu primera nota" + flavor: "¡Pasándola bien con Misskey!" + _notes10: + title: "Algunas notas" + description: "10 notas publicadas" + _notes100: + title: "¡Muchas notas!" + description: "100 notas publicadas" + _notes500: + title: "¡Cubierto de notas!" + description: "500 notas publicadas" + _notes1000: + title: "¡Una montaña de notas!" + description: "1000 notas publicadas" + _notes5000: + title: "¡Exceso de notas!" + description: "5000 notas publicadas" + _notes10000: + title: "¡Súpernota!" + description: "10000 notas publicadas" + _notes20000: + title: "Necesito... Más... ¡Notas!" + description: "20000 notas publicadas" + _notes30000: + title: "¡Notas! ¡Notas! ¡Notas!" + description: "30000 notas publicadas" + _notes40000: + title: "Fábrica de notas" + description: "40000 notas publicadas" + _notes50000: + title: "¡Un planeta de notas!" + description: "50000 notas publicadas" + _notes60000: + title: "¡Un cuásar de notas!" + description: "60000 notas publicadas" + _notes70000: + title: "¡Un hoyo negro de notas!" + description: "70000 notas publicadas" + _notes80000: + title: "¡Una galaxia de notas!" + description: "80000 notas publicadas" + _notes90000: + title: "¡Todo un universo de notas!" + description: "90000 notas publicadas" + _notes100000: + title: "ALL YOUR NOTE ARE BELONG TO US" + description: "100000 notas publicadas" + flavor: "¿Tienes tanto para publicar?" + _login3: + title: "Principiante I" + description: "Días desde el inicio de sesión: 3" + flavor: "Desde hoy, soy Misskero" + _login7: + title: "Principiante II" + description: "Días desde el inicio de sesión: 7" + flavor: "¿Ya te acostumbraste?" + _login15: + title: "Principiante III" + description: "Días desde el inicio de sesión: 15" + _login30: + title: "Misskero I" + description: "Días desde el inicio de sesión: 30" + _login60: + title: "Misskero II" + description: "Días desde el inicio de sesión: 60" + _login100: + title: "Misskero III" + description: "Días desde el inicio de sesión: 100" + flavor: "Para este usuario, Misskaína" + _login200: + title: "Regular I" + description: "Días desde el inicio de sesión: 200" + _login300: + title: "Regular II" + description: "Días desde el inicio de sesión: 300" + _login400: + title: "Regular III" + description: "Días desde el inicio de sesión: 400" + _login500: + title: "Veterano I" + description: "Días desde el inicio de sesión: 500" + flavor: "Chicos, me encantan las libretas..." + _login600: + title: "Veterano II" + description: "Días desde el inicio de sesión: 600" + _login700: + title: "Veterano III" + description: "Días desde el inicio de sesión: 700" + _login800: + title: "Maestro I" + description: "Días desde el inicio de sesión: 800" + _login900: + title: "Maestro II" + description: "Días desde el inicio de sesión: 900" + _login1000: + title: "Maestro III" + description: "Días desde el inicio de sesión: 1000" + flavor: "¡Gracias por usar Misskey!" + _noteClipped1: + title: "No puedo evitar clipearte..." + description: "Hacer un clip por primera vez" + _noteFavorited1: + title: "Contemplando las estrellas" + description: "Poner una nota como favorito por primera vez" + _myNoteFavorited1: + title: "¡Quiero una estrella!" + description: "Tu nota ha sido marcada como favorito por primera vez" + _profileFilled: + title: "¡Listo!" + description: "Perfil completado" + _markedAsCat: + title: "Soy un gato" + description: "Configurar la cuenta como cuenta de un gato" + flavor: "Aún no tengo nombre" + _following1: + title: "Primera vez siguiendo a alguien" + description: "Seguir a un usuario" + _following10: + title: "Ahí la llevas, ahí la llevas..." + description: "10 usuarios seguidos" + _following50: + title: "¡Un puñado de amigos!" + description: "50 cuentas seguidas" + _following100: + title: "100 amigos" + description: "100 cuentas seguidas" + _following300: + title: "¡Sobrecarga de amigos!" + description: "300 cuentas seguidas" + _followers1: + title: "¡Tu primer seguidor!" + description: "1 seguidor ganado" + _followers10: + title: "¡Sígueme!" + description: "10 seguidores ganados" + _followers50: + title: "Viniendo en manada" + description: "50 seguidores ganados" + _followers100: + title: "Popular" + description: "100 cuentas seguidas" + _followers300: + title: "Por favor, hagan una fila" + description: "300 seguidores ganados" + _followers500: + title: "¡Toda una torre de radio!" + description: "500 seguidores ganados" + _followers1000: + title: "\"Influyente\"" + description: "1000 seguidores gandos" + _collectAchievements30: + title: "Coleccionista" + description: "30 logros ganados" + _viewAchievements3min: + title: "¡Te gustan los logros!" + description: "Mirando tus logros por 3 minutos" + _iLoveMisskey: + title: "¡AMO Misskey!" + description: "\"I ❤ #Misskey\" Publicado" + flavor: "El equipo de desarrollo de Misskey, en verdad, ¡aprecia tu apoyo!" + _foundTreasure: + title: "Búsqueda del tesoro" + description: "Encontraste un tesoro" + _client30min: + title: "Un descansito" + description: "30 minutos dedicados a Misskey" + _noteDeletedWithin1min: + title: "Ah... Mejor no..." + description: "Borrar una nota antes que de pase 1 minuto" + _postedAtLateNight: + title: "Nocturno" + description: "Una nota publicada por la noche" + flavor: "¡Ya casi es hora de dormir!" + _postedAt0min0sec: + title: "Reloj parlante" + description: "Publicar una nota a las 00:00 de la madrugada" + flavor: "Tic, tic, tic ¡TUUUUUN!" + _selfQuote: + title: "Autoreferencia" + description: "Citar tu propia nota" + _htl20npm: + title: "Línea de tiempo fluyendo" + description: "La velocidad de tu línea de tiempo excede las 20 npm (notas por minuto)" + _viewInstanceChart: + title: "Analista" + description: "Gráficas de la instancia mostradas" + _outputHelloWorldOnScratchpad: + title: "¡Hola mundo!" + description: "Escribir \"hello world\" en el compositor" + _open3windows: + title: "Multiventana" + description: "Tener más de 3 ventanas al mismo tiempo" + _driveFolderCircularReference: + title: "Referencia circular" + description: "Intento de crear carpetas recursivamente" + _reactWithoutRead: + title: "¡Sí lo leíste bien?" + description: "Reaccionar a los 3 segundos de publicación de una nota con más de 100 caracteres" + _clickedClickHere: + title: "Pícale aquí" + description: "Le picó ahí" + _justPlainLucky: + title: "Pura suerte" + description: "Obtenido con una probabilidad del 0.01% cada 10 segundos" + _setNameToSyuilo: + title: "Complejo de superioridad" + description: "Configurar el nombre como 'Syuilo'" + _passedSinceAccountCreated1: + title: "Primer aniversario" + description: "Pasó un año desde la creación de la cuenta" + _passedSinceAccountCreated2: + title: "Segundo aniversario" + description: "Pasaron dos años desde la creación de la cuenta" + _passedSinceAccountCreated3: + title: "Tercer aniversario" + description: "Pasaron tres años desde la creación de la cuenta" + _loggedInOnBirthday: + title: "¡Feliz cumpleaños!" + description: "En linea el día de tu cumpleaños" + _loggedInOnNewYearsDay: + title: "¡Feliz Año Nuevo!" + description: "En linea en año nuevo" + flavor: "¡Gracias por tu apoyo a la instancia durante todo este año!" + _cookieClicked: + title: "Un juego para picarle a una galleta" + description: "Picaste una galleta" + flavor: "¿Está mal este juego?" + _brainDiver: + title: "Brain Diver" + description: "Publicaste un vínculo a \"Brain Diver\"" + flavor: "Misskey-Misskey La-Tu-Ma" _role: + new: "Crear rol" + edit: "Editar rol" + name: "Nombre del rol" + description: "Descripción del rol" + permission: "Permisos del rol" + descriptionOfPermission: "Moderador Te permite ejecutar acciones básicas de moderación.\nAdministradores puede cambiar todas las configuraciones de la instancia." + assignTarget: "Asignar objetivo" + descriptionOfAssignTarget: "Manual Para cambiar manualmente lo que se incluye en este rol.\nCondicional configura una condición, y los usuarios que cumplan la condición serán incluídos automáticamente." + manual: "manual" + conditional: "condicional" + condition: "condición" + isConditionalRole: "Esto es un rol condicional" + isPublic: "Publicar rol" + descriptionOfIsPublic: "Cualquiera puede ver los usuarios asignados a este rol. También, el perfil del usuario mostrará este rol." + options: "Opción" + policies: "Política" + baseRole: "Rol base" + useBaseValue: "Usar los valores del rol base" + chooseRoleToAssign: "Selecciona el rol para asignar" + iconUrl: "URL del ícono" + asBadge: "Mostrar como emblema" + descriptionOfAsBadge: "Este ícono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo." + canEditMembersByModerator: "Permitir a los moderadores editar los miembros" + descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo." priority: "Prioridad" _priority: low: "Baja" middle: "Mediano" high: "Alta" + _options: + gtlAvailable: "Explorar la línea de tiempo global" + ltlAvailable: "Explorar la línea de tiempo local" + canPublicNote: "Permitir la publicación" + canInvite: "Puede crear códigos de invitación" + canManageCustomEmojis: "Administrar emojis personalizados" + driveCapacity: "Capacidad de almacenamiento" + pinMax: "Máximo de notas fijadas" + antennaMax: "Máximo de antenas" + wordMuteMax: "Máximo de caracteres en palabras silenciadas" + webhookMax: "Máximo de Webhooks" + clipMax: "Máximo de clips" + noteEachClipsMax: "Máximo de notas con clip" + userListMax: "Máximo de listas de usuarios" + userEachUserListsMax: "Máximo de usuarios en una lista" + rateLimitFactor: "Limitador" + descriptionOfRateLimitFactor: "Límites más bajos son menos restrictivos, más altos menos restrictivos" + canHideAds: "Puede ocultar anuncios" + _condition: + isLocal: "Usuario local" + isRemote: "Usuario remoto" + createdLessThan: "Menos de X han pasado desde la creación de la cuenta" + createdMoreThan: "Más de X han pasado desde la creación de la cuenta" + followersLessThanOrEq: "Tiene X o menos seguidores" + followersMoreThanOrEq: "Tiene X o más seguidores" + followingLessThanOrEq: "Sigue X o menos cuentas" + followingMoreThanOrEq: "Sigue X o más cuentas" + and: "Condicional AND" + or: "Condicional OR" + not: "Condicional NOT" _sensitiveMediaDetection: - description: "Reduce el esfuerzo de la moderación el el servidor a través del reconocimiento automático de contenido NSFW usando 'Machine Learning'. Esto puede incrementar ligeramente la carga en el servidor." - sensitivity: "Sensibilidad de detección" + description: "Reduce el esfuerzo de la moderación en el servidor a través del reconocimiento automático de contenido NSFW usando 'Machine Learning'. Esto puede incrementar ligeramente la carga en el servidor." + sensitivity: "Sensibilidad de la detección" sensitivityDescription: "Reducir la sensibilidad puede acarrear a varios falsos positivos, mientras que incrementarla puede reducir las detecciones (falsos negativos)." setSensitiveFlagAutomatically: "Marcar como NSFW" setSensitiveFlagAutomaticallyDescription: "Los resultados de la detección interna pueden ser retenidos incluso si la opción está desactivada." @@ -1328,10 +1638,12 @@ _widgets: jobQueue: "Cola de trabajos" serverMetric: "Estadísticas del servidor" aiscript: "Consola de AiScript" + aiscriptApp: "Aplicación AiScript" aichan: "indigo" userList: "Lista de usuarios" _userList: chooseList: "Seleccione una lista" + clicker: "Cliqueador" _cw: hide: "Ocultar" show: "Ver más" @@ -1434,7 +1746,16 @@ _timelines: social: "Social" global: "Global" _play: + new: "Crear guión" + edit: "Editar guión" + created: "Guión creado" + updated: "Guión editado" + deleted: "Guión eliminado" + pageSetting: "Configuración de guión" + editThisPage: "Editar este guión" viewSource: "Ver la fuente" + my: "Mis guiones" + liked: "Guiones que te gustaron" featured: "Popular" title: "Título" script: "Script" @@ -1507,6 +1828,7 @@ _notification: pollEnded: "Estan disponibles los resultados de la encuesta" unreadAntennaNote: "Antena {name}" emptyPushNotificationMessage: "Se han actualizado las notificaciones push" + achievementEarned: "Logro desbloqueado" _types: all: "Todo" follow: "Siguiendo" @@ -1548,5 +1870,6 @@ _deck: tl: "Linea de tiempo" antenna: "Antenas" list: "Listas" + channel: "Canal" mentions: "Menciones" direct: "Mensaje directo" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 462b561e4..8a9476e91 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -1541,5 +1541,6 @@ _deck: tl: "Fil" antenna: "Antennes" list: "Listes" + channel: "Canaux" mentions: "Mentions" direct: "Direct" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 6a2ccfd8d..7332e030c 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -1673,5 +1673,6 @@ _deck: tl: "Linimasa" antenna: "Antena" list: "Daftar" + channel: "Kanal" mentions: "Sebutan" direct: "Langsung" diff --git a/locales/index.js b/locales/index.js index d476f4ce0..80bd8f775 100644 --- a/locales/index.js +++ b/locales/index.js @@ -35,6 +35,7 @@ const languages = [ 'pt-PT', 'ru-RU', 'sk-SK', + 'th-TH', 'ug-CN', 'uk-UA', 'vi-VN', diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 66af4654c..12bbc78d6 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1044,7 +1044,7 @@ _achievements: flavor: "Grazie per aver usato Misskey!" _noteClipped1: title: "Devo clippare!" - description: "Ho raccolto in Clip la prima Nota" + description: "Hai raccolto la tua prima Nota in una Clip" _noteFavorited1: title: "Guarda le stelle" description: "Aggiungi una Nota ai preferiti per la prima volta" @@ -1080,7 +1080,7 @@ _achievements: title: "Follow me!" description: "Hai ottenuto 10 profili Follower" _followers50: - title: "Follower a frotte" + title: "Un gregge di Follower" description: "Hai ottenuto 50 Follower" _followers100: title: "Popolare" @@ -1108,7 +1108,7 @@ _achievements: title: "Caccia al tesoro" description: "Hai trovato un tesoro nascosto" _client30min: - title: "Piccola pausa" + title: "Piccola grande pausa" description: "Hai passato più di 30 minuti su Misskey" _noteDeletedWithin1min: title: "Ooops!" @@ -1134,7 +1134,7 @@ _achievements: title: "Hello, world!" description: "Hai scritto «Hello world» nel blocco appunti" _open3windows: - title: "Finestrato" + title: "Apri le finestre!" description: "Hai aperto almeno 3 finestre contemporaneamente" _driveFolderCircularReference: title: "Riferimento circolare" @@ -1170,7 +1170,7 @@ _achievements: _cookieClicked: title: "Clicca il biscotto" description: "Hai giocato a cliccare il cookie" - flavor: "Hai autorizzato i cookie?" + flavor: "È il sito giusto?" _brainDiver: title: "Brain Diver" description: "Pubblica un link a Brain Diver" @@ -1195,6 +1195,9 @@ _role: baseRole: "Ruolo di base" useBaseValue: "Eredita dal ruolo base" chooseRoleToAssign: "Seleziona il ruolo da assegnare" + iconUrl: "URL dell'icona" + asBadge: "Mostra come badge" + descriptionOfAsBadge: "Se indicato, accanto al nome utente viene visualizzata l'icona del ruolo." canEditMembersByModerator: "Anche i Moderatori assegnano profili a questo ruolo" descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori." priority: "Priorità" @@ -1866,5 +1869,6 @@ _deck: tl: "Timeline" antenna: "Antenne" list: "Liste" + channel: "Canale" mentions: "Menzioni" direct: "Diretta" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 778074c71..87905218f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -130,6 +130,7 @@ unblockConfirm: "ブロック解除しますか?" suspendConfirm: "凍結しますか?" unsuspendConfirm: "解凍しますか?" selectList: "リストを選択" +selectChannel: "チャンネルを選択" selectAntenna: "アンテナを選択" selectWidget: "ウィジェットを選択" editWidgets: "ウィジェットを編集" @@ -258,6 +259,8 @@ noMoreHistory: "これより過去の履歴はありません" startMessaging: "チャットを開始" nUsersRead: "{n}人が読みました" agreeTo: "{0}に同意" +agreeBelow: "下記に同意する" +basicNotesBeforeCreateAccount: "基本的な注意事項" tos: "利用規約" start: "始める" home: "ホーム" @@ -863,6 +866,8 @@ failedToFetchAccountInformation: "アカウント情報の取得に失敗しま rateLimitExceeded: "レート制限を超えました" cropImage: "画像のクロップ" cropImageAsk: "画像をクロップしますか?" +cropYes: "クロップする" +cropNo: "そのまま使う" file: "ファイル" recentNHours: "直近{n}時間" recentNDays: "直近{n}日" @@ -941,6 +946,8 @@ cannotPerformTemporaryDescription: "操作回数が制限を超過するため preset: "プリセット" selectFromPresets: "プリセットから選択" achievements: "実績" +gotInvalidResponseError: "サーバーの応答が無効です" +gotInvalidResponseErrorDescription: "サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。" _achievements: earnedAt: "獲得日時" @@ -1150,7 +1157,7 @@ _achievements: description: "ここをクリックした" _justPlainLucky: title: "単なるラッキー" - description: "10秒ごとに0.01%の確率で獲得" + description: "10秒ごとに0.005%の確率で獲得" _setNameToSyuilo: title: "神様コンプレックス" description: "名前を syuilo に設定した" @@ -1186,7 +1193,7 @@ _role: description: "ロールの説明" permission: "ロールの権限" descriptionOfPermission: "モデレーターは基本的なモデレーションに関する操作を行えます。\n管理者はインスタンスの全ての設定を変更できます。" - assignTarget: "アサインターゲット" + assignTarget: "アサイン" descriptionOfAssignTarget: "マニュアルは誰がこのロールに含まれるかを手動で管理します。\nコンディショナルは条件を設定し、それに合致するユーザーが自動で含まれるようになります。" manual: "マニュアル" conditional: "コンディショナル" @@ -1199,6 +1206,9 @@ _role: baseRole: "ベースロール" useBaseValue: "ベースロールの値を使用" chooseRoleToAssign: "アサインするロールを選択" + iconUrl: "アイコン画像のURL" + asBadge: "バッジとして表示" + descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。" canEditMembersByModerator: "モデレーターのメンバー編集を許可" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。" priority: "優先度" @@ -1632,12 +1642,15 @@ _permissions: "write:gallery-likes": "ギャラリーのいいねを操作する" _auth: + shareAccessTitle: "アプリへのアクセス許可" shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?" shareAccessAsk: "アカウントへのアクセスを許可しますか?" + permission: "{name}は次の権限を要求しています" permissionAsk: "このアプリは次の権限を要求しています" pleaseGoBack: "アプリケーションに戻ってやっていってください" callback: "アプリケーションに戻っています" denied: "アクセスを拒否しました" + pleaseLogin: "アプリケーションにアクセス許可を与えるには、ログインが必要です。" _antennaSources: all: "全てのノート" @@ -1929,5 +1942,6 @@ _deck: tl: "タイムライン" antenna: "アンテナ" list: "リスト" + channel: "チャンネル" mentions: "あなた宛て" direct: "ダイレクト" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 40d28b196..05de911be 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1628,5 +1628,6 @@ _deck: tl: "タイムライン" antenna: "アンテナ" list: "リスト" + channel: "チャンネル" mentions: "あんた宛て" direct: "ダイレクト" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 265f14ec5..23814208a 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1866,5 +1866,6 @@ _deck: tl: "타임라인" antenna: "안테나" list: "리스트" + channel: "채널" mentions: "받은 멘션" direct: "다이렉트" diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml new file mode 100644 index 000000000..befb2eb36 --- /dev/null +++ b/locales/lo-LA.yml @@ -0,0 +1,162 @@ +--- +_lang_: "ພາສາລາວ" +headlineMisskey: "ເຊື່ອມຕໍ່ເຄືອຂ່າຍໂດຍຫມາຍເຫດ" +introMisskey: "ຍິນດີຕ້ອນຮັບ! Misskey ເປັນແຫຼ່ງເປີດ, ການບໍລິການ microblogging ກະຈາຍ\nສ້າງ \"ບັນທຶກ\" ເພື່ອແບ່ງປັນຄວາມຄິດຂອງທ່ານກັບທຸກໆຄົນທີ່ຢູ່ອ້ອມຮອບທ່ານ 📡\nດ້ວຍ \"ປະຕິກິລິຍາ\", ທ່ານຍັງສາມາດສະແດງຄວາມຮູ້ສຶກຂອງທ່ານຢ່າງໄວວາກ່ຽວກັບບັນທຶກຂອງທຸກໆຄົນ 👍\nມາສຳຫຼວດໂລກໃໝ່! 🚀" +poweredByMisskeyDescription: "{name} ແມ່ນສ່ວນໜຶ່ງຂອງການບໍລິການທີ່ຂັບເຄື່ອນໂດຍແພລດຟອມ open source. Misskey (ເອີ້ນວ່າ \"Misskey instance\")" +monthAndDay: "{ເດືອນ}/{ມື້}" +search: "ຄົ້ນຫາ" +notifications: "ການແຈ້ງເຕືອນ" +username: "ຊື່ຜູ້ໃຊ້" +password: "ລະຫັດຜ່ານ" +forgotPassword: "ລືມລະຫັດຜ່ານ" +fetchingAsApObject: "ກຳລັງດຶງຂໍ້ມູນຈາກ fediverse..." +ok: "ຕົກ​ລົງ" +gotIt: "ເຂົ້າໃຈແລ້ວ!" +cancel: "ຍົກເລີກ" +noThankYou: "ບໍ່​ແມ່ນ​ຕອນ​ນີ້" +enterUsername: "ປ້ອນຊື່ຜູ້ໃຊ້" +renotedBy: "Renoted ໂດຍ {ຜູ້ໃຊ້}" +noNotes: "ບໍ່ມີຫມາຍເຫດ" +noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ" +instance: "ອີນສະແຕນ" +settings: "ກຳນົດຄ່າ" +basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ" +otherSettings: "ການຕັ້ງຄ່າອື່ນໆ" +openInWindow: "ເປີດຢູ່ໃນປ່ອງຢ້ຽມ" +profile: "ໂພຼຟາຍ" +timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​" +noAccountDescription: "ຜູ້ໃຊ້ນີ້ຍັງບໍ່ໄດ້ຂຽນໃນຊີວະປະຫວັດຂອງເຂົາເຈົ້າເທື່ອ" +login: "ເຂົ້າ​ສູ່​ລະ​ບົບ" +loggingIn: "ກຳລັງເຂົ້າສູ່ລະບົບ..." +logout: "ອອກ​ຈາກ​ລະ​ບົບ" +signup: "ລົງ​ທະ​ບຽນ" +uploading: "ການອັບໂຫຼດ..." +save: "ບັນທຶກ" +users: "ຜູ້ໃຊ້ຕ່າງໆ" +addUser: "ເພີ່ມຜູ້ໃຊ້" +favorite: "ເພີ່ມໃສ່ລາຍການທີ່ມັກ" +favorites: "ລາຍການທີ່ມັກ" +unfavorite: "ລຶບອອກຈາກລາຍການທີ່ມັກ" +favorited: "ເພີ່ມໃສ່ລາຍການທີ່ມັກແລ້ວ" +alreadyFavorited: "ເພີ່ມເຂົ້າໃນລາຍການທີ່ມັກແລ້ວ." +cantFavorite: "ບໍ່ສາມາດເພີ່ມໃສ່ລາຍການທີ່ມັກໄດ້." +pin: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌" +unpin: "ຖອດປັກໝຸດອອກຈາກໂປຣໄຟລ໌" +copyContent: "ຄັດລອກເນື້ອຫາ" +copyLink: "ສຳເນົາລິ້ງ" +delete: "ລຶບ" +deleteAndEdit: "ລົບ​ແລະ​ແກ້​ໄຂ​" +deleteAndEditConfirm: "ເຈົ້າ​ແນ່​ໃຈ​ບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບບັນທຶກນີ້ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍການໂຕ້ຕອບ, ບັນທຶກ, ແລະການຕອບກັບທັງໝົດ" +addToList: "ເພີ່ມໃສ່ລາຍຊື່" +sendMessage: "ສົ່ງຂໍ້ຄວາມ" +copyRSS: "ສຳເນົາ RSS" +copyUsername: "ສຳເນົາຊື່ຜູ້ໃຊ້" +searchUser: "ຄົ້ນຫາຜູ້ໃຊ້" +reply: "ຕອບ​ໄປ​ທີ" +loadMore: "ໂຫຼດເພີ່ມເຕີມ" +showMore: "ໂຫຼດເພີ່ມເຕີມ" +showLess: "ປິດ" +youGotNewFollower: "ໄດ້ຕິດຕາມທ່ານ" +receiveFollowRequest: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍທີ່ໄດ້ຮັບ" +followRequestAccepted: "ຜູ້ຕິດຕາມໄດ້ຍອມຮັບຄໍາຮ້ອງຂໍຂອງທ່ານ" +mention: "ໄດ້ກ່າວມາ" +mentions: "ກ່າວເຖິງ" +directNotes: "ໂດຍກົງຫມາຍເຫດ" +importAndExport: "ນໍາເຂົ້າ / ສົ່ງອອກ" +import: "ນຳເຂົ້າ" +export: "ນຳອອກ" +files: "ໄຟລ໌" +download: "ດາວໂຫລດ" +driveFileDeleteConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບໄຟລ໌ \"{name}\"? ບັນທຶກທີ່ມີໄຟລ໌ແນບນີ້ຈະຖືກລຶບຖິ້ມ" +unfollowConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການເຊົາຕິດຕາມ {name}?" +exportRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການສົ່ງອອກ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ ແລະມັນຈະຖືກເພີ່ມໃສ່ drive ຂອງທ່ານເມື່ອມັນສຳເລັດແລ້ວ" +importRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການນໍາເຂົ້າ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ" +lists: "ລາຍການ" +noLists: "ທ່ານ​ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​" +note: "ບັນທຶກ" +notes: "ບັນທຶກ" +following: "ກຳລັງຕິດຕາມ" +followers: "ຜູ້ຕິດຕາມ" +followsYou: "ຕິດ​ຕາມ​ເຈົ້າ" +createList: "ສ້າງລາຍຊື່" +manageLists: "ການບໍລິຫານບັນຊີລາຍການ" +error: "ຂໍ້ຜິດພາດ" +somethingHappened: "​ອຸຍ, ມີ​ບາງ​ຢ່າງ​ຜິ​ດ​ພາດ" +retry: "ລອງໃຫມ່" +pageLoadError: "ເກີດຄວາມຜິດພາດໃນການໂຫລດໜ້ານີ້" +pageLoadErrorDescription: "ປົກກະຕິແລ້ວມັນເກີດຈາກຄວາມຜິດພາດເຄືອຂ່າຍ ຫຼື cache ຂອງຕົວທ່ອງເວັບ ລອງລຶບລ້າງແຄດແລ້ວລອງໃໝ່ພາຍຫຼັງສອງສາມນາທີ" +serverIsDead: "ເຊີບເວີນີ້ບໍ່ຕອບສະໜອງ ກະລຸນາລໍຖ້າຈັກໜ່ອຍແລ້ວລອງໃໝ່ອີກຄັ້ງ" +youShouldUpgradeClient: "ເພື່ອເບິ່ງໜ້ານີ້, ກະລຸນາໂຫຼດຂໍ້ມູນຄືນໃໝ່ເພື່ອອັບເດດລູກຄ້າຂອງທ່ານ" +enterListName: "ໃສ່ຊື່ສຳລັບລາຍຊື່" +privacy: "ຄວາມເປັນສ່ວນຕົວ" +makeFollowManuallyApprove: "ປະຕິບັດຕາມການຮ້ອງຂໍຮຽກຮ້ອງໃຫ້ມີການອະນຸມັດ" +defaultNoteVisibility: "ເປັນຄ່າເລີ່ມຕົ້ນ" +follow: "ກຳລັງຕິດຕາມ" +followRequest: "ສົ່ງ​ການ​ຮ້ອງ​ຂໍ​ປະ​ຕິ​ບ​ຕາມ​" +followRequests: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍ" +unfollow: "ເຊົາຕິດຕາມ" +followRequestPending: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍທີ່ລໍຖ້າຢູ່" +enterEmoji: "ປ້ອນອີໂມຈິ" +renote: "Renote" +unrenote: "ເລີກ Renote" +pinned: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌" +addAccount: "ເພີ່ມບັນຊີ" +loginFailed: "ການເຂົ້າສູ່ລະບົບບໍ່ສຳເລັດ" +general: "ທົ່ວໄປ" +wallpaper: "ພາບພື້ນຫລັງ" +setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ" +instances: "ອີນສະແຕນ" +statistics: "ສະຖິຕິ" +clearQueue: "ລ້າງຄິວ" +clearCachedFiles: "ລຶບລ້າງແຄສ" +editProfile: "ແກ້ໄຂໂປຣໄຟລ໌" +remove: "ລຶບ" +userList: "ລາຍການ" +smtpUser: "ຊື່ຜູ້ໃຊ້" +smtpPass: "ລະຫັດຜ່ານ" +clearCache: "ລຶບລ້າງແຄສ" +user: "ຜູ້ໃຊ້ຕ່າງໆ" +searchByGoogle: "ຄົ້ນຫາ" +file: "ໄຟລ໌" +_email: + _follow: + title: "ໄດ້ຕິດຕາມທ່ານ" +_mfm: + mention: "ໄດ້ກ່າວມາ" + search: "ຄົ້ນຫາ" +_theme: + keys: + mention: "ໄດ້ກ່າວມາ" + renote: "Renote" +_sfx: + note: "ບັນທຶກ" + notification: "ການແຈ້ງເຕືອນ" +_widgets: + profile: "ໂພຼຟາຍ" + notifications: "ການແຈ້ງເຕືອນ" + timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​" +_cw: + show: "ໂຫຼດເພີ່ມເຕີມ" +_visibility: + followers: "ຜູ້ຕິດຕາມ" +_profile: + username: "ຊື່ຜູ້ໃຊ້" +_exportOrImport: + followingList: "ກຳລັງຕິດຕາມ" + userLists: "ລາຍການ" +_notification: + youWereFollowed: "ໄດ້ຕິດຕາມທ່ານ" + _types: + follow: "ກຳລັງຕິດຕາມ" + mention: "ໄດ້ກ່າວມາ" + renote: "Renote" + _actions: + reply: "ຕອບ​ໄປ​ທີ" + renote: "Renote" +_deck: + _columns: + notifications: "ການແຈ້ງເຕືອນ" + tl: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​" + list: "ລາຍການ" + channel: "ຊ່ອງ" + mentions: "ກ່າວເຖິງ" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index d78185be8..d8c7739f6 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -1438,5 +1438,6 @@ _deck: tl: "Oś czasu" antenna: "Anteny" list: "Listy" + channel: "Kanały" mentions: "Wspomnienia" direct: "Bezpośredni" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index b1ec5426a..f354801d5 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -721,4 +721,5 @@ _deck: tl: "Cronologie" antenna: "Antene" list: "Liste" + channel: "Canale" mentions: "Mențiuni" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 2085cb2be..26e9ad0e9 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1845,5 +1845,6 @@ _deck: tl: "Лента" antenna: "Антенны" list: "Списки" + channel: "Каналы" mentions: "Упоминания" direct: "Личное" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index ee2ca11fa..369f1af36 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -1545,5 +1545,6 @@ _deck: tl: "Časová os" antenna: "Antény" list: "Zoznam" + channel: "Kanály" mentions: "Zmienky" direct: "Priame poznámky" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index ca23e44fa..0f5ff6248 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -129,6 +129,7 @@ unblockConfirm: "คุณแน่ใจแล้วเหรอ? ว่าต suspendConfirm: "นายแน่ใจแล้วเหรอว่าต้องการระงับบัญชีนี้อ่ะ?" unsuspendConfirm: "นายแน่ใจแล้วหรอ? ว่าต้องการยกเลิกการระงับบัญชีนี้" selectList: "เลือกรายการ" +selectChannel: "เลือกแชนแนล" selectAntenna: "เลือกเสาอากาศ" selectWidget: "เลือกวิดเจ็ต" editWidgets: "แก้ไขวิดเจ็ต" @@ -1147,7 +1148,7 @@ _achievements: description: "คุณได้คลิกที่นี่" _justPlainLucky: title: "แค่ลัคกี้ธรรมดา" - description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.01% ทุก ๆ 10 วินาที" + description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.005% ทุก ๆ 10 วินาที" _setNameToSyuilo: title: "พระเจ้าคอมเพล็กซ์" description: "ตั้งชื่อของคุณเป็น \"syuilo\"" @@ -1182,7 +1183,7 @@ _role: description: "คำอธิบายบทบาท" permission: "สิทธิ์ตามบทบาท" descriptionOfPermission: "ผู้ดูแลกลั่นกรองเนื้อหา สามารถดำเนินการดูแลขั้นพื้นฐานได้นะ\nผู้ดูแลระบบ สามารถเปลี่ยนการตั้งค่าทั้งหมดของอินสแตนซ์ได้นะ" - assignTarget: "กำหนดเป้าหมาย" + assignTarget: "มอบหมาย" descriptionOfAssignTarget: "แมนนวล เพื่อเปลี่ยนผู้ที่เป็นส่วนหนึ่งของบทบาทนี้และใครที่ไม่ใช่ด้วยตนเอง\nเงื่อนไข เพื่อให้ผู้ใช้ได้รับการกำหนดและนำออกจากบทบาทนี้โดยอัตโนมัติตามเงื่อนไขชุดหนึ่ง" manual: "ปรับเอง" conditional: "มีเงื่อนไข" @@ -1195,6 +1196,9 @@ _role: baseRole: "บทบาทพื้นฐาน" useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น" chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด" + iconUrl: "ไอคอน URL" + asBadge: "แสดงเป็นตรา" + descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน" canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก" descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ" priority: "ลำดับความสำคัญ" @@ -1866,5 +1870,6 @@ _deck: tl: "ไทม์ไลน์" antenna: "เสาอากาศ" list: "รายการ" + channel: "แชนแนล" mentions: "พูดถึง" direct: "ไดเร็ค" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index e660635d9..68e949f92 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -529,7 +529,7 @@ state: "Стан" sort: "Сортування" ascendingOrder: "За зростанням" descendingOrder: "За спаданням" -scratchpad: "Чернетка" +scratchpad: "Scratchpad" scratchpadDescription: "Scratchpad надає середовище для експериментів з AiScript. Ви можете писати, виконувати його і тестувати взаємодію з Misskey." output: "Вихід" script: "Скрипт" @@ -1084,22 +1084,32 @@ _achievements: description: "Перевищити швидкість домашньої стрічки 20npm (нотаток на хвилину)" _viewInstanceChart: title: "Аналітик" + _outputHelloWorldOnScratchpad: + title: "Hello, world!" + description: "Вивести \"hello world\" у Скретчпаді" _clickedClickHere: title: "Натисніть тут" description: "Натиснуто тут" + _justPlainLucky: + title: "Просто вдача" + description: "Можна отримати з ймовірністю 0,01% кожні 10 секунд" _setNameToSyuilo: title: "Комплекс бога" description: "Встановлено ім'я \"syuilo\"" _passedSinceAccountCreated1: title: "Перша річниця" + description: "Минув рік з моменту створення акаунта" _passedSinceAccountCreated2: title: "Друга річниця" + description: "Минуло 2 роки з моменту створення акаунта" _passedSinceAccountCreated3: title: "Третя річниця" description: "Минуло 3 роки з моменту створення акаунта" _loggedInOnBirthday: title: "З Днем народження!" + description: "Увійти у свій день народження" _loggedInOnNewYearsDay: + title: "З Новим роком!" description: "Увійшли в перший день року" _brainDiver: title: "Brain Diver" @@ -1372,8 +1382,8 @@ _tutorial: step1_1: "Ласкаво просимо!" step1_2: "Ця сторінка має назву \"стрічка подій\". На ній з'являються записи користувачів на яких ви підписані." step1_3: "Наразі ваша стрічка порожня, оскільки ви ще не написали жодної нотатки і не підписані на інших." - step2_1: "Перш ніж зробити запис або підписатись на когось, спочатку заповніть свій обліковий запис." - step2_2: "Надання деякої інформації про себе дозволить іншим користувачам підписатись на вас." + step2_1: "Перш ніж зробити запис або підписатись на когось, заповніть свій профіль." + step2_2: "Надання деякої інформації про себе допоможе іншим користувачам вирішити підписатись на вас." step3_1: "Ви успішно налаштували свій обліковий запис?" step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані." step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми." @@ -1679,5 +1689,6 @@ _deck: tl: "Стрічка" antenna: "Антени" list: "Списки" + channel: "Канали" mentions: "Згадки" direct: "Особисте" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index b460b5e83..26527c74c 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1520,5 +1520,6 @@ _deck: tl: "Bảng tin" antenna: "Trạm phát sóng" list: "Danh sách" + channel: "Kênh" mentions: "Lượt nhắc" direct: "Nhắn riêng" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 651221fe6..9a63bdec4 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1023,17 +1023,23 @@ _achievements: title: "定期联系Ⅲ" description: "总登录天数400天" _login500: + title: "老熟人Ⅰ" description: "总登录天数500天" flavor: "诸君,我喜欢贴文" _login600: + title: "老熟人Ⅱ" description: "总登录天数600天" _login700: + title: "老熟人Ⅲ" description: "总登录天数700天" _login800: + title: "帖子大师Ⅰ" description: "总登录天数800天" _login900: + title: "帖子大师Ⅱ" description: "总登录天数900天" _login1000: + title: "帖子大师Ⅲ" description: "总登录天数1000天" flavor: "感谢您使用Misskey!" _noteClipped1: @@ -1072,19 +1078,22 @@ _achievements: description: "第一次被关注" _followers10: title: "关注我吧!" - description: "关注者超过10人" + description: "拥有超过10名关注者" _followers50: title: "三五成群" - description: "关注者超过50人" + description: "拥有超过50名关注者" _followers100: title: "胜友如云" - description: "关注者超过100人" + description: "拥有超过100名关注者" _followers300: title: "排列成行" - description: "关注者超过300人" + description: "拥有超过300名关注者" _followers500: - title: "风向标" - description: "关注者超过500人" + title: "信号塔" + description: "拥有超过500名关注者" + _followers1000: + title: "大影响家" + description: "拥有超过1000名关注者" _collectAchievements30: title: "成就收藏家" description: "获得超过30个成就" @@ -1096,6 +1105,7 @@ _achievements: description: "发布\"I ❤ #Misskey\"帖子" flavor: "感谢您使用 Misskey ! by 开发团队" _foundTreasure: + title: "寻宝" description: "发现了隐藏的宝藏" _client30min: title: "休息一下!" @@ -1104,7 +1114,7 @@ _achievements: title: "无话可说" description: "发帖后一分钟内就将其删除" _postedAtLateNight: - title: "夜行者" + title: "夜猫子" description: "深夜发布帖子" flavor: "差不多该去睡了喔。" _postedAt0min0sec: @@ -1114,13 +1124,21 @@ _achievements: _selfQuote: title: "自我提及" description: "引用了自己的帖子" + _htl20npm: + title: "流动的时间线" + description: "在首页时间线的流速超过20npm" + _viewInstanceChart: + title: "分析师" + description: "查看了实例信息中的图表" _outputHelloWorldOnScratchpad: title: "Hello, world!" + description: "在AiScript控制台中输出 hello world" _open3windows: title: "多窗口" description: "打开了三个或更多的窗口" _driveFolderCircularReference: title: "循环引用" + description: "试图对网盘中的文件夹进行循环嵌套" _reactWithoutRead: title: "有好好读过吗?" description: "在含有100字以上的帖子被发出三秒内做出回应" @@ -1129,7 +1147,7 @@ _achievements: description: "点了这里" _justPlainLucky: title: "超高校级的幸运" - description: "每10秒有0.01的概率获得" + description: "每10秒有0.01的概率自动获得" _setNameToSyuilo: title: "像神一样呐" description: "将名称设定为syuilo" @@ -1177,6 +1195,9 @@ _role: baseRole: "基本角色" useBaseValue: "使用基本角色的值" chooseRoleToAssign: "选择要分配的角色" + iconUrl: "图标URL" + asBadge: "作为徽章显示" + descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。" canEditMembersByModerator: "允许监察者编辑成员" descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" priority: "优先级" @@ -1848,5 +1869,6 @@ _deck: tl: "时间线" antenna: "天线" list: "列表" + channel: "频道" mentions: "提及" direct: "指定用户" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 058ee416e..570afce8f 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -326,7 +326,7 @@ connectService: "己連結" disconnectService: "己斷開 " enableLocalTimeline: "開啟本地時間軸" enableGlobalTimeline: "啟用全域時間軸" -disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審核員仍可以繼續使用。" +disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審查員仍可以繼續使用。" registration: "註冊" enableRegistration: "開啟新使用者註冊" invite: "邀請" @@ -389,8 +389,8 @@ aboutMisskey: "關於 Misskey" administrator: "管理員" token: "權杖" twoStepAuthentication: "兩階段驗證" -moderator: "審核員" -moderation: "監察" +moderator: "審查員" +moderation: "審查" nUsersMentioned: "提到了{n}" securityKey: "安全金鑰" securityKeyName: "金鑰名稱" @@ -607,7 +607,7 @@ testEmail: "測試郵件發送" wordMute: "被靜音的文字" regexpError: "正規表達式錯誤" regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:" -instanceMute: "實例的靜音" +instanceMute: "被靜音的實例" userSaysSomething: "{name}說了什麼" makeActive: "啟用" display: "檢視" @@ -939,6 +939,8 @@ cannotPerformTemporaryDescription: "由於超過操作次數限制,暫時無 preset: "預設值" selectFromPresets: "從預設值中選擇" achievements: "成就" +gotInvalidResponseError: "伺服器的回應無效" +gotInvalidResponseErrorDescription: "伺服器可能已關閉或者在維護中,請稍後再試。" _achievements: earnedAt: "獲得日期" _types: @@ -1181,7 +1183,7 @@ _role: name: "角色名稱" description: "角色描述 " permission: "角色的權限" - descriptionOfPermission: "審核員執行與審核相關的基本操作。\n管理員能變更實例的全部設定。" + descriptionOfPermission: "審查員執行與審查相關的基本操作。\n管理員能變更實例的全部設定" assignTarget: "指派目標" descriptionOfAssignTarget: "手動是以手動管理這個角色包含的人員。\n符合條件是設定條件以自動包含符合條件的使用者。" manual: "手動" @@ -1195,8 +1197,11 @@ _role: baseRole: "基本角色" useBaseValue: "使用基本角色的值" chooseRoleToAssign: "選擇要指派的角色" - canEditMembersByModerator: "允許編輯監察員的成員" - descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。" + iconUrl: "圖示的URL" + asBadge: "顯示為徽章" + descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。" + canEditMembersByModerator: "允許編輯審查員的成員" + descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。" priority: "優先級" _priority: low: "低" @@ -1233,7 +1238,7 @@ _role: or: "~或~" not: "~否" _sensitiveMediaDetection: - description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。" + description: "您可以使用機器學習自動檢測敏感媒體並將其用於審查。 伺服器的負荷會稍微增加。" sensitivity: "檢測敏感度" sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。" setSensitiveFlagAutomatically: "設定 NSFW 旗標" @@ -1866,5 +1871,6 @@ _deck: tl: "時間軸" antenna: "天線" list: "清單" + channel: "頻道" mentions: "提及" direct: "指定使用者" diff --git a/package.json b/package.json index 2da150f4c..b51d074b9 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "misskey", - "version": "13.2.6-simkey", + "version": "13.5.5-simkey", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/sim1222/misskey.git" }, - "packageManager": "pnpm@7.24.3", + "packageManager": "pnpm@7.27.0", "workspaces": [ "packages/frontend", "packages/backend", @@ -19,7 +19,7 @@ "start": "cd packages/backend && node ./built/boot/index.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js", "init": "pnpm migrate", - "migrate": "cd packages/backend && pnpm typeorm migration:run -d ormconfig.js", + "migrate": "cd packages/backend && pnpm migrate", "migrateandstart": "pnpm migrate && pnpm start", "gulp": "pnpm exec gulp build", "watch": "pnpm dev", @@ -28,8 +28,8 @@ "cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts", "cy:run": "pnpm cypress run", "e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run", - "jest": "cd packages/backend && pnpm cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --runInBand", - "jest-and-coverage": "cd packages/backend && pnpm cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --runInBand", + "jest": "cd packages/backend && pnpm jest", + "jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage", "test": "pnpm jest", "test-and-coverage": "pnpm jest-and-coverage", "format": "pnpm exec gulp format", @@ -38,8 +38,8 @@ "cleanall": "pnpm clean-all" }, "resolutions": { - "chokidar": "^3.5.3", - "lodash": "^4.17.21" + "chokidar": "3.5.3", + "lodash": "4.17.21" }, "dependencies": { "execa": "5.1.1", @@ -49,19 +49,19 @@ "gulp-replace": "1.1.4", "gulp-terser": "2.1.0", "js-yaml": "4.1.0", - "typescript": "4.9.4" + "typescript": "4.9.5" }, "devDependencies": { "@types/gulp": "4.0.10", "@types/gulp-rename": "2.0.1", - "@typescript-eslint/eslint-plugin": "5.49.0", - "@typescript-eslint/parser": "5.49.0", + "@typescript-eslint/eslint-plugin": "5.51.0", + "@typescript-eslint/parser": "5.51.0", "cross-env": "7.0.3", - "cypress": "12.4.0", - "eslint": "^8.32.0", + "cypress": "12.5.1", + "eslint": "8.33.0", "start-server-and-test": "1.15.3" }, "optionalDependencies": { - "@tensorflow/tfjs-core": "^4.2.0" + "@tensorflow/tfjs-core": "4.2.0" } } diff --git a/packages/backend/migration/1675404035646-cleanup.js b/packages/backend/migration/1675404035646-cleanup.js new file mode 100644 index 000000000..09b22ee39 --- /dev/null +++ b/packages/backend/migration/1675404035646-cleanup.js @@ -0,0 +1,29 @@ +export class cleanup1675404035646 { + name = 'cleanup1675404035646' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTwitterIntegration"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableGithubIntegration"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableDiscordIntegration"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "twitterConsumerKey"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "twitterConsumerSecret"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "githubClientId"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "githubClientSecret"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "discordClientId"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "discordClientSecret"`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "integrations"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "integrations" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`ALTER TABLE "meta" ADD "discordClientSecret" character varying(128)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "discordClientId" character varying(128)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "githubClientSecret" character varying(128)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "githubClientId" character varying(128)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "twitterConsumerSecret" character varying(128)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "twitterConsumerKey" character varying(128)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "enableDiscordIntegration" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "meta" ADD "enableGithubIntegration" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "meta" ADD "enableTwitterIntegration" boolean NOT NULL DEFAULT false`); + } +} diff --git a/packages/backend/migration/1675557528704-role-icon-badge.js b/packages/backend/migration/1675557528704-role-icon-badge.js new file mode 100644 index 000000000..0ebca088e --- /dev/null +++ b/packages/backend/migration/1675557528704-role-icon-badge.js @@ -0,0 +1,13 @@ +export class roleIconBadge1675557528704 { + name = 'roleIconBadge1675557528704' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" ADD "iconUrl" character varying(512)`); + await queryRunner.query(`ALTER TABLE "role" ADD "asBadge" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "asBadge"`); + await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "iconUrl"`); + } +} diff --git a/packages/backend/ormconfig.js b/packages/backend/ormconfig.js index 32c26f7b6..229e5bf1f 100644 --- a/packages/backend/ormconfig.js +++ b/packages/backend/ormconfig.js @@ -1,6 +1,6 @@ import { DataSource } from 'typeorm'; import { loadConfig } from './built/config.js'; -import { entities } from './built/postgre.js'; +import { entities } from './built/postgres.js'; const config = loadConfig(); diff --git a/packages/backend/package.json b/packages/backend/package.json index e677c1cf9..6ec2ef4b7 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -19,34 +19,34 @@ "test-and-coverage": "pnpm jest-and-coverage" }, "optionalDependencies": { - "@tensorflow/tfjs": "^4.2.0", + "@tensorflow/tfjs": "4.2.0", "@tensorflow/tfjs-node": "4.2.0" }, "dependencies": { - "@bull-board/api": "^4.11.0", - "@bull-board/fastify": "^4.11.0", - "@bull-board/ui": "^4.11.0", + "@bull-board/api": "4.11.1", + "@bull-board/fastify": "4.11.1", + "@bull-board/ui": "4.11.1", "@discordapp/twemoji": "14.0.2", "@fastify/accepts": "4.1.0", - "@fastify/cookie": "^8.3.0", + "@fastify/cookie": "8.3.0", "@fastify/cors": "8.2.0", - "@fastify/http-proxy": "^8.4.0", + "@fastify/http-proxy": "8.4.0", "@fastify/multipart": "7.4.0", - "@fastify/static": "6.7.0", + "@fastify/static": "6.8.0", "@fastify/view": "7.4.1", - "@nestjs/common": "9.2.1", - "@nestjs/core": "9.2.1", - "@nestjs/testing": "9.2.1", + "@nestjs/common": "9.3.7", + "@nestjs/core": "9.3.7", + "@nestjs/testing": "9.3.7", "@peertube/http-signature": "1.7.0", "@sinonjs/fake-timers": "10.0.2", - "accepts": "^1.3.8", + "accepts": "1.3.8", "ajv": "8.12.0", "archiver": "5.3.1", "autwh": "0.1.0", "aws-sdk": "2.1295.0", "bcryptjs": "2.4.3", "blurhash": "2.0.4", - "bull": "4.10.2", + "bull": "4.10.3", "cacheable-lookup": "6.1.0", "cbor": "8.1.0", "chalk": "5.2.0", @@ -62,11 +62,11 @@ "feed": "4.2.2", "file-type": "18.2.0", "fluent-ffmpeg": "2.1.2", - "form-data": "^4.0.0", - "got": "^12.5.3", + "form-data": "4.0.0", + "got": "12.5.3", "hpagent": "1.2.0", "ioredis": "4.28.5", - "ip-cidr": "3.0.11", + "ip-cidr": "3.1.0", "is-svg": "4.3.2", "js-yaml": "4.1.0", "jsdom": "21.1.0", @@ -75,22 +75,22 @@ "jsrsasign": "10.6.1", "mfm-js": "0.23.3", "mime-types": "2.1.35", - "misskey-js": "0.0.14", + "misskey-js": "0.0.15", "ms": "3.0.0-canary.1", "nested-property": "4.0.0", "node-fetch": "3.3.0", - "nodemailer": "6.9.0", + "nodemailer": "6.9.1", "nsfwjs": "2.4.2", - "oauth": "^0.10.0", + "oauth": "0.10.0", "os-utils": "0.0.14", "parse5": "7.1.2", - "pg": "8.8.0", + "pg": "8.9.0", "private-ip": "3.0.0", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", "pug": "3.0.2", "punycode": "2.3.0", - "pureimage": "0.3.15", + "pureimage": "0.3.17", "qrcode": "1.5.1", "random-seed": "0.3.0", "ratelimiter": "3.4.1", @@ -102,23 +102,22 @@ "rss-parser": "3.12.0", "rxjs": "7.8.0", "s-age": "1.1.2", - "sanitize-html": "2.8.1", - "seedrandom": "^3.0.5", + "sanitize-html": "2.9.0", + "seedrandom": "3.0.5", "semver": "7.3.8", "sharp": "0.31.3", "speakeasy": "2.0.0", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "summaly": "2.7.0", - "syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2", - "systeminformation": "5.17.4", - "tinycolor2": "1.5.2", + "systeminformation": "5.17.8", + "tinycolor2": "1.6.0", "tmp": "0.2.1", "tsc-alias": "1.8.2", "tsconfig-paths": "4.1.2", "twemoji-parser": "14.0.0", - "typeorm": "0.3.11", - "typescript": "4.9.4", + "typeorm": "0.3.12", + "typescript": "4.9.5", "ulid": "2.3.0", "unzipper": "0.10.11", "uuid": "9.0.0", @@ -129,28 +128,28 @@ "xev": "3.0.2" }, "devDependencies": { - "@jest/globals": "^29.4.1", - "@redocly/openapi-core": "1.0.0-beta.120", - "@swc/cli": "^0.1.59", - "@swc/core": "1.3.29", + "@jest/globals": "29.4.2", + "@redocly/openapi-core": "1.0.0-beta.123", + "@swc/cli": "0.1.61", + "@swc/core": "1.3.34", "@swc/jest": "0.2.24", "@types/accepts": "1.3.5", "@types/archiver": "5.3.1", "@types/bcryptjs": "2.4.2", "@types/bull": "4.10.0", "@types/cbor": "6.0.0", - "@types/color-convert": "^2.0.0", - "@types/content-disposition": "^0.5.5", + "@types/color-convert": "2.0.0", + "@types/content-disposition": "0.5.5", "@types/escape-regexp": "0.0.1", "@types/fluent-ffmpeg": "2.1.20", "@types/ioredis": "4.28.10", "@types/jest": "29.4.0", "@types/js-yaml": "4.0.5", - "@types/jsdom": "20.0.1", + "@types/jsdom": "21.1.0", "@types/jsonld": "1.5.8", "@types/jsrsasign": "10.5.5", "@types/mime-types": "2.1.1", - "@types/node": "18.11.18", + "@types/node": "18.13.0", "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.7", "@types/oauth": "0.9.1", @@ -167,7 +166,6 @@ "@types/sharp": "0.31.1", "@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/unzipper": "0.10.5", @@ -176,13 +174,13 @@ "@types/web-push": "3.3.2", "@types/websocket": "1.0.5", "@types/ws": "8.5.4", - "@typescript-eslint/eslint-plugin": "5.49.0", - "@typescript-eslint/parser": "5.49.0", + "@typescript-eslint/eslint-plugin": "5.51.0", + "@typescript-eslint/parser": "5.51.0", "cross-env": "7.0.3", - "eslint": "8.32.0", + "eslint": "8.33.0", "eslint-plugin-import": "2.27.5", "execa": "6.1.0", - "jest": "29.4.1", - "jest-mock": "^29.4.1" + "jest": "29.4.2", + "jest-mock": "29.4.2" } } diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 8a70129eb..35416209a 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -4,7 +4,7 @@ import { DataSource } from 'typeorm'; import { createRedisConnection } from '@/redis.js'; import { DI } from './di-symbols.js'; import { loadConfig } from './config.js'; -import { createPostgreDataSource } from './postgre.js'; +import { createPostgresDataSource } from './postgres.js'; import { RepositoryModule } from './models/RepositoryModule.js'; import type { Provider, OnApplicationShutdown } from '@nestjs/common'; @@ -18,7 +18,7 @@ const $config: Provider = { const $db: Provider = { provide: DI.db, useFactory: async (config) => { - const db = createPostgreDataSource(config); + const db = createPostgresDataSource(config); return await db.initialize(); }, inject: [DI.config], diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 025d7acde..aa98ef1d2 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -65,11 +65,6 @@ export type Source = { deliverJobMaxAttempts?: number; inboxJobMaxAttempts?: number; - syslog: { - host: string; - port: number; - }; - mediaProxy?: string; proxyRemoteFiles?: boolean; @@ -92,6 +87,8 @@ export type Mixin = { userAgent: string; clientEntry: string; clientManifestExists: boolean; + mediaProxy: string; + externalMediaProxyEnabled: boolean; }; export type Config = Source & Mixin; @@ -113,7 +110,7 @@ const path = process.env.NODE_ENV === 'test' export function loadConfig() { const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8')); - const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json') + const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json'); const clientManifest = clientManifestExists ? JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8')) : { 'src/init.ts': { file: 'src/init.ts' } }; @@ -140,6 +137,13 @@ export function loadConfig() { mixin.clientEntry = clientManifest['src/init.ts']; mixin.clientManifestExists = clientManifestExists; + const externalMediaProxy = config.mediaProxy ? + config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy + : null; + const internalMediaProxy = `${mixin.scheme}://${mixin.host}/proxy`; + mixin.mediaProxy = externalMediaProxy ?? internalMediaProxy; + mixin.externalMediaProxyEnabled = externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy; + if (!config.redis.prefix) config.redis.prefix = mixin.host; return Object.assign(config, mixin); diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts index 5fd9c451c..2ebee0f7e 100644 --- a/packages/backend/src/core/AchievementService.ts +++ b/packages/backend/src/core/AchievementService.ts @@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { CreateNotificationService } from '@/core/CreateNotificationService.js'; -const ACHIEVEMENT_TYPES = [ +export const ACHIEVEMENT_TYPES = [ 'notes1', 'notes10', 'notes100', diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 7db8c43ea..a71327e94 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -10,10 +10,9 @@ import { isUserRelated } from '@/misc/is-user-related.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js'; import * as Acct from '@/misc/acct.js'; -import { Cache } from '@/misc/cache.js'; import type { Packed } from '@/misc/schema.js'; import { DI } from '@/di-symbols.js'; -import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; +import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { StreamMessages } from '@/server/api/stream/types.js'; @@ -23,7 +22,6 @@ import type { OnApplicationShutdown } from '@nestjs/common'; export class AntennaService implements OnApplicationShutdown { private antennasFetched: boolean; private antennas: Antenna[]; - private blockingCache: Cache; constructor( @Inject(DI.redisSubscriber) @@ -32,9 +30,6 @@ export class AntennaService implements OnApplicationShutdown { @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -52,14 +47,13 @@ export class AntennaService implements OnApplicationShutdown { private utilityService: UtilityService, private idService: IdService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private pushNotificationService: PushNotificationService, private noteEntityService: NoteEntityService, private antennaEntityService: AntennaEntityService, ) { this.antennasFetched = false; this.antennas = []; - this.blockingCache = new Cache(1000 * 60 * 5); this.redisSubscriber.on('message', this.onRedisMessage); } @@ -109,7 +103,7 @@ export class AntennaService implements OnApplicationShutdown { read: read, }); - this.globalEventServie.publishAntennaStream(antenna.id, 'note', note); + this.globalEventService.publishAntennaStream(antenna.id, 'note', note); if (!read) { const mutings = await this.mutingsRepository.find({ @@ -139,7 +133,7 @@ export class AntennaService implements OnApplicationShutdown { setTimeout(async () => { const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false }); if (unread) { - this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna); + this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna); this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', { antenna: { id: antenna.id, name: antenna.name }, note: await this.noteEntityService.pack(note), @@ -155,10 +149,6 @@ export class AntennaService implements OnApplicationShutdown { public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise { if (note.visibility === 'specified') return false; if (note.visibility === 'followers') return false; - - // アンテナ作成者がノート作成者にブロックされていたらスキップ - const blockings = await this.blockingCache.fetch(noteUser.id, () => this.blockingsRepository.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId))); - if (blockings.some(blocking => blocking === antenna.userId)) return false; if (!antenna.withReplies && note.replyId != null) return false; diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index eddf40794..6a6d1b864 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -62,7 +62,6 @@ import PerUserNotesChart from './chart/charts/per-user-notes.js'; import PerUserPvChart from './chart/charts/per-user-pv.js'; import DriveChart from './chart/charts/drive.js'; import PerUserReactionsChart from './chart/charts/per-user-reactions.js'; -import HashtagChart from './chart/charts/hashtag.js'; import PerUserFollowingChart from './chart/charts/per-user-following.js'; import PerUserDriveChart from './chart/charts/per-user-drive.js'; import ApRequestChart from './chart/charts/ap-request.js'; @@ -187,7 +186,6 @@ const $PerUserNotesChart: Provider = { provide: 'PerUserNotesChart', useExisting const $PerUserPvChart: Provider = { provide: 'PerUserPvChart', useExisting: PerUserPvChart }; const $DriveChart: Provider = { provide: 'DriveChart', useExisting: DriveChart }; const $PerUserReactionsChart: Provider = { provide: 'PerUserReactionsChart', useExisting: PerUserReactionsChart }; -const $HashtagChart: Provider = { provide: 'HashtagChart', useExisting: HashtagChart }; const $PerUserFollowingChart: Provider = { provide: 'PerUserFollowingChart', useExisting: PerUserFollowingChart }; const $PerUserDriveChart: Provider = { provide: 'PerUserDriveChart', useExisting: PerUserDriveChart }; const $ApRequestChart: Provider = { provide: 'ApRequestChart', useExisting: ApRequestChart }; @@ -315,7 +313,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PerUserPvChart, DriveChart, PerUserReactionsChart, - HashtagChart, PerUserFollowingChart, PerUserDriveChart, ApRequestChart, @@ -437,7 +434,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PerUserPvChart, $DriveChart, $PerUserReactionsChart, - $HashtagChart, $PerUserFollowingChart, $PerUserDriveChart, $ApRequestChart, @@ -559,7 +555,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PerUserPvChart, DriveChart, PerUserReactionsChart, - HashtagChart, PerUserFollowingChart, PerUserDriveChart, ApRequestChart, @@ -680,7 +675,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PerUserPvChart, $DriveChart, $PerUserReactionsChart, - $HashtagChart, $PerUserFollowingChart, $PerUserDriveChart, $ApRequestChart, diff --git a/packages/backend/src/core/CreateNotificationService.ts b/packages/backend/src/core/CreateNotificationService.ts index f376b7b9c..cd47844a7 100644 --- a/packages/backend/src/core/CreateNotificationService.ts +++ b/packages/backend/src/core/CreateNotificationService.ts @@ -26,7 +26,7 @@ export class CreateNotificationService { private notificationEntityService: NotificationEntityService, private idService: IdService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private pushNotificationService: PushNotificationService, ) { } @@ -60,7 +60,7 @@ export class CreateNotificationService { const packed = await this.notificationEntityService.pack(notification, {}); // Publish notification event - this.globalEventServie.publishMainStream(notifieeId, 'notification', packed); + this.globalEventService.publishMainStream(notifieeId, 'notification', packed); // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する setTimeout(async () => { @@ -77,7 +77,7 @@ export class CreateNotificationService { } //#endregion - this.globalEventServie.publishMainStream(notifieeId, 'unreadNotification', packed); + this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed); this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 39814e1be..63f031944 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -120,7 +120,7 @@ export class CustomEmojiService { const url = isLocal ? emojiUrl : this.config.proxyRemoteFiles - ? `${this.config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}` + ? `${this.config.mediaProxy}/emoji.webp?${query({ url: emojiUrl })}` : emojiUrl; return url; diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index 0ac12857c..2acb5f230 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -14,7 +14,7 @@ export class DeleteAccountService { private userSuspendService: UserSuspendService, private queueService: QueueService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, ) { } @@ -38,6 +38,6 @@ export class DeleteAccountService { }); // Terminate streaming - this.globalEventServie.publishUserEvent(user.id, 'terminate', {}); + this.globalEventService.publishUserEvent(user.id, 'terminate', {}); } } diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index a971e06fd..852c1f32e 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -60,6 +60,7 @@ export class DownloadService { retry: { limit: 0, }, + enableUnixSockets: false, }).on('response', (res: Got.Response) => { if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) { if (this.isPrivateIp(res.ip)) { diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index 309cfe8c3..851e42e7b 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -4,7 +4,6 @@ import type { User } from '@/models/entities/User.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { IdService } from '@/core/IdService.js'; import type { Hashtag } from '@/models/entities/Hashtag.js'; -import HashtagChart from '@/core/chart/charts/hashtag.js'; import type { HashtagsRepository, UsersRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -20,7 +19,6 @@ export class HashtagService { private userEntityService: UserEntityService, private idService: IdService, - private hashtagChart: HashtagChart, ) { } @@ -143,9 +141,5 @@ export class HashtagService { } as Hashtag); } } - - if (!isUserAttached) { - this.hashtagChart.update(tag, user); - } } } diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts index 221631f12..441c254f4 100644 --- a/packages/backend/src/core/LoggerService.ts +++ b/packages/backend/src/core/LoggerService.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import * as SyslogPro from 'syslog-pro'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import Logger from '@/logger.js'; @@ -8,29 +7,14 @@ import type { KEYWORD } from 'color-convert/conversions'; @Injectable() export class LoggerService { - private syslogClient; - constructor( @Inject(DI.config) private config: Config, ) { - if (this.config.syslog) { - this.syslogClient = new SyslogPro.RFC5424({ - applicationName: 'Misskey', - timestamp: true, - includeStructuredData: true, - color: true, - extendedColor: true, - server: { - target: config.syslog.host, - port: config.syslog.port, - }, - }); - } } @bindThis public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) { - return new Logger(domain, color, store, this.syslogClient); + return new Logger(domain, color, store); } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 3dc44a25f..4a81f764d 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -175,7 +175,7 @@ export class NoteCreateService { private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private idService: IdService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private queueService: QueueService, private noteReadService: NoteReadService, private createNotificationService: CreateNotificationService, @@ -535,7 +535,7 @@ export class NoteCreateService { // Pack the note const noteObj = await this.noteEntityService.pack(note); - this.globalEventServie.publishNotesStream(noteObj); + this.globalEventService.publishNotesStream(noteObj); this.webhookService.getActiveWebhooks().then(webhooks => { webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note')); @@ -561,7 +561,7 @@ export class NoteCreateService { if (!threadMuted) { nm.push(data.reply.userId, 'reply'); - this.globalEventServie.publishMainStream(data.reply.userId, 'reply', noteObj); + this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply')); for (const webhook of webhooks) { @@ -584,7 +584,7 @@ export class NoteCreateService { // Publish event if ((user.id !== data.renote.userId) && data.renote.userHost === null) { - this.globalEventServie.publishMainStream(data.renote.userId, 'renote', noteObj); + this.globalEventService.publishMainStream(data.renote.userId, 'renote', noteObj); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote')); for (const webhook of webhooks) { @@ -684,7 +684,7 @@ export class NoteCreateService { detail: true, }); - this.globalEventServie.publishMainStream(u.id, 'mention', detailPackedNote); + this.globalEventService.publishMainStream(u.id, 'mention', detailPackedNote); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); for (const webhook of webhooks) { diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index b1f16b6e8..4dad82509 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -34,7 +34,7 @@ export class NoteDeleteService { private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, private apRendererService: ApRendererService, @@ -63,7 +63,7 @@ export class NoteDeleteService { } if (!quiet) { - this.globalEventServie.publishNoteStream(note.id, 'deleted', { + this.globalEventService.publishNoteStream(note.id, 'deleted', { deletedAt: deletedAt, }); diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts index f4395725d..84983d600 100644 --- a/packages/backend/src/core/NoteReadService.ts +++ b/packages/backend/src/core/NoteReadService.ts @@ -40,7 +40,7 @@ export class NoteReadService { private userEntityService: UserEntityService, private idService: IdService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private notificationService: NotificationService, private antennaService: AntennaService, private pushNotificationService: PushNotificationService, @@ -87,13 +87,13 @@ export class NoteReadService { if (exist == null) return; if (params.isMentioned) { - this.globalEventServie.publishMainStream(userId, 'unreadMention', note.id); + this.globalEventService.publishMainStream(userId, 'unreadMention', note.id); } if (params.isSpecified) { - this.globalEventServie.publishMainStream(userId, 'unreadSpecifiedNote', note.id); + this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id); } if (note.channelId) { - this.globalEventServie.publishMainStream(userId, 'unreadChannel', note.id); + this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id); } }, 2000); } @@ -155,7 +155,7 @@ export class NoteReadService { }).then(mentionsCount => { if (mentionsCount === 0) { // 全て既読になったイベントを発行 - this.globalEventServie.publishMainStream(userId, 'readAllUnreadMentions'); + this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions'); } }); @@ -165,7 +165,7 @@ export class NoteReadService { }).then(specifiedCount => { if (specifiedCount === 0) { // 全て既読になったイベントを発行 - this.globalEventServie.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); + this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); } }); @@ -175,7 +175,7 @@ export class NoteReadService { }).then(channelNoteCount => { if (channelNoteCount === 0) { // 全て既読になったイベントを発行 - this.globalEventServie.publishMainStream(userId, 'readAllChannels'); + this.globalEventService.publishMainStream(userId, 'readAllChannels'); } }); @@ -200,14 +200,14 @@ export class NoteReadService { }); if (count === 0) { - this.globalEventServie.publishMainStream(userId, 'readAntenna', antenna); + this.globalEventService.publishMainStream(userId, 'readAntenna', antenna); this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id }); } } this.userEntityService.getHasUnreadAntenna(userId).then(unread => { if (!unread) { - this.globalEventServie.publishMainStream(userId, 'readAllAntennas'); + this.globalEventService.publishMainStream(userId, 'readAllAntennas'); this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined); } }); diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts index abc598ab7..042dcb3e6 100644 --- a/packages/backend/src/core/PollService.ts +++ b/packages/backend/src/core/PollService.ts @@ -1,17 +1,17 @@ import { Inject, Injectable } from '@nestjs/common'; import { Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UsersRepository, BlockingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; +import type { NotesRepository, UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; import type { Note } from '@/models/entities/Note.js'; import { RelayService } from '@/core/RelayService.js'; import type { CacheableUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { bindThis } from '@/decorators.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; @Injectable() export class PollService { @@ -28,14 +28,11 @@ export class PollService { @Inject(DI.pollVotesRepository) private pollVotesRepository: PollVotesRepository, - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - private userEntityService: UserEntityService, private idService: IdService, private relayService: RelayService, - private globalEventServie: GlobalEventService, - private createNotificationService: CreateNotificationService, + private globalEventService: GlobalEventService, + private userBlockingService: UserBlockingService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, ) { @@ -52,11 +49,8 @@ export class PollService { // Check blocking if (note.userId !== user.id) { - const block = await this.blockingsRepository.findOneBy({ - blockerId: note.userId, - blockeeId: user.id, - }); - if (block) { + const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); + if (blocked) { throw new Error('blocked'); } } @@ -88,7 +82,7 @@ export class PollService { const index = choice + 1; // In SQL, array index is 1 based await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); - this.globalEventServie.publishNoteStream(note.id, 'pollVoted', { + this.globalEventService.publishNoteStream(note.id, 'pollVoted', { choice: choice, userId: user.id, }); diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index 4cc844cce..c334d749e 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -1,10 +1,10 @@ import { Inject, Injectable } from '@nestjs/common'; -import { Brackets } from 'typeorm'; +import { Brackets, ObjectLiteral } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { User } from '@/models/entities/User.js'; import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository } from '@/models/index.js'; -import type { SelectQueryBuilder } from 'typeorm'; import { bindThis } from '@/decorators.js'; +import type { SelectQueryBuilder } from 'typeorm'; @Injectable() export class QueryService { @@ -32,7 +32,7 @@ export class QueryService { ) { } - public makePaginationQuery(q: SelectQueryBuilder, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder { + public makePaginationQuery(q: SelectQueryBuilder, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder { if (sinceId && untilId) { q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 0c1c3d0a3..380659005 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -18,7 +18,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; -import { UtilityService } from './UtilityService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; const legacies: Record = { 'like': '👍', @@ -73,8 +74,9 @@ export class ReactionService { private metaService: MetaService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, + private userBlockingService: UserBlockingService, private idService: IdService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, private createNotificationService: CreateNotificationService, @@ -86,11 +88,8 @@ export class ReactionService { public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string) { // Check blocking if (note.userId !== user.id) { - const block = await this.blockingsRepository.findOneBy({ - blockerId: note.userId, - blockeeId: user.id, - }); - if (block) { + const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); + if (blocked) { throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7'); } } @@ -157,7 +156,7 @@ export class ReactionService { select: ['name', 'host', 'originalUrl', 'publicUrl'], }); - this.globalEventServie.publishNoteStream(note.id, 'reacted', { + this.globalEventService.publishNoteStream(note.id, 'reacted', { reaction: decodedReaction.reaction, emoji: emoji != null ? { name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`, @@ -229,7 +228,7 @@ export class ReactionService { if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1); - this.globalEventServie.publishNoteStream(note.id, 'unreacted', { + this.globalEventService.publishNoteStream(note.id, 'unreacted', { reaction: this.decodeReaction(exist.reaction).reaction, userId: user.id, }); diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index f8f9231cd..d15d8c0ae 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -202,6 +202,19 @@ export class RoleService implements OnApplicationShutdown { return [...assignedRoles, ...matchedCondRoles]; } + /** + * 指定ユーザーのバッジロール一覧取得 + */ + @bindThis + public async getUserBadgeRoles(userId: User['id']) { + const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); + const assignedRoleIds = assigns.map(x => x.roleId); + const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); + const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id)); + // コンディショナルロールも含めるのは負荷高そうだから一旦無し + return assignedBadgeRoles; + } + @bindThis public async getUserPolicies(userId: User['id'] | null): Promise { const meta = await this.metaService.fetch(); diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index c92370042..d73432866 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -1,5 +1,6 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import Redis from 'ioredis'; import { IdService } from '@/core/IdService.js'; import type { CacheableUser, User } from '@/models/entities/User.js'; import type { Blocking } from '@/models/entities/Blocking.js'; @@ -7,7 +8,6 @@ import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import { DI } from '@/di-symbols.js'; -import logger from '@/logger.js'; import type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; import Logger from '@/logger.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -15,12 +15,20 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { WebhookService } from '@/core/WebhookService.js'; import { bindThis } from '@/decorators.js'; +import { Cache } from '@/misc/cache.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; @Injectable() -export class UserBlockingService { +export class UserBlockingService implements OnApplicationShutdown { private logger: Logger; + // キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ + private blockingsByUserIdCache: Cache; + constructor( + @Inject(DI.redisSubscriber) + private redisSubscriber: Redis.Redis, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -42,13 +50,44 @@ export class UserBlockingService { private userEntityService: UserEntityService, private idService: IdService, private queueService: QueueService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private webhookService: WebhookService, private apRendererService: ApRendererService, private perUserFollowingChart: PerUserFollowingChart, private loggerService: LoggerService, ) { this.logger = this.loggerService.getLogger('user-block'); + + this.blockingsByUserIdCache = new Cache(Infinity); + + this.redisSubscriber.on('message', this.onMessage); + } + + @bindThis + private async onMessage(_: string, data: string): Promise { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message as StreamMessages['internal']['payload']; + switch (type) { + case 'blockingCreated': { + const cached = this.blockingsByUserIdCache.get(body.blockerId); + if (cached) { + this.blockingsByUserIdCache.set(body.blockerId, [...cached, ...[body.blockeeId]]); + } + break; + } + case 'blockingDeleted': { + const cached = this.blockingsByUserIdCache.get(body.blockerId); + if (cached) { + this.blockingsByUserIdCache.set(body.blockerId, cached.filter(x => x !== body.blockeeId)); + } + break; + } + default: + break; + } + } } @bindThis @@ -72,6 +111,11 @@ export class UserBlockingService { await this.blockingsRepository.insert(blocking); + this.globalEventService.publishInternalEvent('blockingCreated', { + blockerId: blocker.id, + blockeeId: blockee.id, + }); + if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking)); this.queueService.deliver(blocker, content, blockee.inbox); @@ -97,15 +141,15 @@ export class UserBlockingService { if (this.userEntityService.isLocalUser(followee)) { this.userEntityService.pack(followee, followee, { detail: true, - }).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); + }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); } if (this.userEntityService.isLocalUser(follower)) { this.userEntityService.pack(followee, follower, { detail: true, }).then(async packed => { - this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); - this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed); + this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed); + this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); for (const webhook of webhooks) { @@ -152,8 +196,8 @@ export class UserBlockingService { this.userEntityService.pack(followee, follower, { detail: true, }).then(async packed => { - this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); - this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed); + this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed); + this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); for (const webhook of webhooks) { @@ -210,10 +254,31 @@ export class UserBlockingService { await this.blockingsRepository.delete(blocking.id); + this.globalEventService.publishInternalEvent('blockingDeleted', { + blockerId: blocker.id, + blockeeId: blockee.id, + }); + // deliver if remote bloking if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker)); this.queueService.deliver(blocker, content, blockee.inbox); } } + + @bindThis + public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise { + const blockedUserIds = await this.blockingsByUserIdCache.fetch(blockerId, () => this.blockingsRepository.find({ + where: { + blockerId, + }, + select: ['blockeeId'], + }).then(records => records.map(record => record.blockeeId))); + return blockedUserIds.includes(blockeeId); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined) { + this.redisSubscriber.off('message', this.onMessage); + } } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index f1ce311ce..2214a4862 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -12,10 +12,11 @@ import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { WebhookService } from '@/core/WebhookService.js'; import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { DI } from '@/di-symbols.js'; -import type { BlockingsRepository, FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { bindThis } from '@/decorators.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; import Logger from '../logger.js'; const logger = new Logger('following/create'); @@ -48,21 +49,18 @@ export class UserFollowingService { @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, private userEntityService: UserEntityService, + private userBlockingService: UserBlockingService, private idService: IdService, private queueService: QueueService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private createNotificationService: CreateNotificationService, private federatedInstanceService: FederatedInstanceService, private webhookService: WebhookService, private apRendererService: ApRendererService, - private globalEventService: GlobalEventService, private perUserFollowingChart: PerUserFollowingChart, private instanceChart: InstanceChart, ) { @@ -77,28 +75,22 @@ export class UserFollowingService { // check blocking const [blocking, blocked] = await Promise.all([ - this.blockingsRepository.findOneBy({ - blockerId: follower.id, - blockeeId: followee.id, - }), - this.blockingsRepository.findOneBy({ - blockerId: followee.id, - blockeeId: follower.id, - }), + this.userBlockingService.checkBlocked(follower.id, followee.id), + this.userBlockingService.checkBlocked(followee.id, follower.id), ]); if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) { - // リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。 + // リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。 const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee)); this.queueService.deliver(followee, content, follower.inbox); return; } else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) { - // リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。 - await this.blockingsRepository.delete(blocking.id); + // リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。 + await this.userBlockingService.unblock(follower, followee); } else { - // それ以外は単純に例外 - if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking'); - if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked'); + // それ以外は単純に例外 + if (blocking) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking'); + if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked'); } const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id }); @@ -227,8 +219,8 @@ export class UserFollowingService { this.userEntityService.pack(followee.id, follower, { detail: true, }).then(async packed => { - this.globalEventServie.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); - this.globalEventServie.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); + this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); + this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow')); for (const webhook of webhooks) { @@ -242,7 +234,7 @@ export class UserFollowingService { // Publish followed event if (this.userEntityService.isLocalUser(followee)) { this.userEntityService.pack(follower.id, followee).then(async packed => { - this.globalEventServie.publishMainStream(followee.id, 'followed', packed); + this.globalEventService.publishMainStream(followee.id, 'followed', packed); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed')); for (const webhook of webhooks) { @@ -288,8 +280,8 @@ export class UserFollowingService { this.userEntityService.pack(followee.id, follower, { detail: true, }).then(async packed => { - this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); - this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed); + this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed); + this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); for (const webhook of webhooks) { @@ -357,18 +349,12 @@ export class UserFollowingService { // check blocking const [blocking, blocked] = await Promise.all([ - this.blockingsRepository.findOneBy({ - blockerId: follower.id, - blockeeId: followee.id, - }), - this.blockingsRepository.findOneBy({ - blockerId: followee.id, - blockeeId: follower.id, - }), + this.userBlockingService.checkBlocked(follower.id, followee.id), + this.userBlockingService.checkBlocked(followee.id, follower.id), ]); - if (blocking != null) throw new Error('blocking'); - if (blocked != null) throw new Error('blocked'); + if (blocking) throw new Error('blocking'); + if (blocked) throw new Error('blocked'); const followRequest = await this.followRequestsRepository.insert({ id: this.idService.genId(), @@ -388,11 +374,11 @@ export class UserFollowingService { // Publish receiveRequest event if (this.userEntityService.isLocalUser(followee)) { - this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventServie.publishMainStream(followee.id, 'receiveFollowRequest', packed)); + this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed)); this.userEntityService.pack(followee.id, followee, { detail: true, - }).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); + }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); // 通知を作成 this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', { @@ -440,7 +426,7 @@ export class UserFollowingService { this.userEntityService.pack(followee.id, followee, { detail: true, - }).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); + }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); } @bindThis @@ -468,7 +454,7 @@ export class UserFollowingService { this.userEntityService.pack(followee.id, followee, { detail: true, - }).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); + }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); } @bindThis @@ -583,8 +569,8 @@ export class UserFollowingService { detail: true, }); - this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packedFollowee); - this.globalEventServie.publishMainStream(follower.id, 'unfollow', packedFollowee); + this.globalEventService.publishUserEvent(follower.id, 'unfollow', packedFollowee); + this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); for (const webhook of webhooks) { diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index fc4873830..c17439499 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -25,7 +25,7 @@ export class UserListService { private idService: IdService, private userFollowingService: UserFollowingService, private roleService: RoleService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private proxyAccountService: ProxyAccountService, ) { } @@ -46,7 +46,7 @@ export class UserListService { userListId: list.id, } as UserListJoining); - this.globalEventServie.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target)); + this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target)); // このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする if (this.userEntityService.isRemoteUser(target)) { diff --git a/packages/backend/src/core/UserMutingService.ts b/packages/backend/src/core/UserMutingService.ts index 3029d02c0..e98f11709 100644 --- a/packages/backend/src/core/UserMutingService.ts +++ b/packages/backend/src/core/UserMutingService.ts @@ -18,7 +18,7 @@ export class UserMutingService { private idService: IdService, private queueService: QueueService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, ) { } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 91a2767e6..648f30229 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -274,7 +274,7 @@ export class ApRendererService { } as any; if (reaction.startsWith(':')) { - const name = reaction.replace(/:/g, ''); + const name = reaction.replaceAll(':', ''); const emoji = await this.emojisRepository.findOneBy({ name, host: IsNull(), diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index d01817b0d..928ef1ae7 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -48,6 +48,10 @@ export class ApImageService { throw new Error('invalid image: url not privided'); } + if (!image.url.startsWith('https://')) { + throw new Error('invalid image: unexpected shcema of url: ' + image.url); + } + this.logger.info(`Creating the Image: ${image.url}`); const instance = await this.metaService.fetch(); diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index c9192f53b..813415e6f 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -1,8 +1,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; import { DI } from '@/di-symbols.js'; -import type { MessagingMessagesRepository, PollsRepository, EmojisRepository } from '@/models/index.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { MessagingMessagesRepository, PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type { CacheableRemoteUser } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; @@ -18,6 +17,7 @@ import { PollService } from '@/core/PollService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { MessagingService } from '@/core/MessagingService.js'; +import { bindThis } from '@/decorators.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { ApLoggerService } from '../ApLoggerService.js'; @@ -32,7 +32,6 @@ import { ApQuestionService } from './ApQuestionService.js'; import { ApImageService } from './ApImageService.js'; import type { Resolver } from '../ApResolverService.js'; import type { IObject, IPost } from '../type.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ApNoteService { @@ -133,6 +132,16 @@ export class ApNoteService { const note: IPost = object; this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); + + if (note.id && !note.id.startsWith('https://')) { + throw new Error('unexpected shcema of note.id: ' + note.id); + } + + const url = getOneApHrefNullable(note.url); + + if (url && !url.startsWith('https://')) { + throw new Error('unexpected shcema of note url: ' + url); + } this.logger.info(`Creating the Note: ${note.id}`); @@ -307,7 +316,7 @@ export class ApNoteService { apEmojis, poll, uri: note.id, - url: getOneApHrefNullable(note.url), + url: url, }, silent); } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index f86b5e6f9..76f820cda 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -29,6 +29,7 @@ import { UserNotePining } from '@/models/entities/UserNotePining.js'; import { StatusError } from '@/misc/status-error.js'; import type { UtilityService } from '@/core/UtilityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { bindThis } from '@/decorators.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { extractApHashtags } from './tag.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -43,37 +44,6 @@ import type { IActor, IObject, IApPropertyValue } from '../type.js'; const nameLength = 128; const summaryLength = 2048; -const services: { - [x: string]: (id: string, username: string) => any -} = { - 'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }), - 'misskey:authentication:github': (id, login) => ({ id, login }), - 'misskey:authentication:discord': (id, name) => $discord(id, name), -}; - -const $discord = (id: string, name: string) => { - if (typeof name !== 'string') { - name = 'unknown#0000'; - } - const [username, discriminator] = name.split('#'); - return { id, username, discriminator }; -}; - -function addService(target: { [x: string]: any }, source: IApPropertyValue) { - const service = services[source.name]; - - if (typeof source.value !== 'string') { - source.value = 'unknown'; - } - - const [id, username] = source.value.split('@'); - - if (service) { - target[source.name.split(':')[2]] = service(id, username); - } -} -import { bindThis } from '@/decorators.js'; - @Injectable() export class ApPersonService implements OnModuleInit { private utilityService: UtilityService; @@ -282,6 +252,12 @@ export class ApPersonService implements OnModuleInit { const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); + const url = getOneApHrefNullable(person.url); + + if (url && !url.startsWith('https://')) { + throw new Error('unexpected shcema of person url: ' + url); + } + // Create user let user: IRemoteUser; try { @@ -313,7 +289,7 @@ export class ApPersonService implements OnModuleInit { await transactionalEntityManager.save(new UserProfile({ userId: user.id, description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, - url: getOneApHrefNullable(person.url), + url: url, fields, birthday: bday ? bday[0] : null, location: person['vcard:Address'] ?? null, @@ -455,6 +431,12 @@ export class ApPersonService implements OnModuleInit { const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); + const url = getOneApHrefNullable(person.url); + + if (url && !url.startsWith('https://')) { + throw new Error('unexpected shcema of person url: ' + url); + } + const updates = { lastFetchedAt: new Date(), inbox: person.inbox, @@ -489,7 +471,7 @@ export class ApPersonService implements OnModuleInit { } await this.userProfilesRepository.update({ userId: exist.id }, { - url: getOneApHrefNullable(person.url), + url: url, fields, description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, birthday: bday ? bday[0] : null, @@ -540,22 +522,16 @@ export class ApPersonService implements OnModuleInit { name: string, value: string }[] = []; - const services: { [x: string]: any } = {}; - if (Array.isArray(attachments)) { for (const attachment of attachments.filter(isPropertyValue)) { - if (isPropertyValue(attachment.identifier)) { - addService(services, attachment.identifier); - } else { - fields.push({ - name: attachment.name, - value: this.mfmService.fromHtml(attachment.value), - }); - } + fields.push({ + name: attachment.name, + value: this.mfmService.fromHtml(attachment.value), + }); } } - return { fields, services }; + return { fields }; } @bindThis diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts index 4fba1b57d..779a32ac5 100644 --- a/packages/backend/src/core/chart/ChartManagementService.ts +++ b/packages/backend/src/core/chart/ChartManagementService.ts @@ -10,7 +10,6 @@ import PerUserNotesChart from './charts/per-user-notes.js'; import PerUserPvChart from './charts/per-user-pv.js'; import DriveChart from './charts/drive.js'; import PerUserReactionsChart from './charts/per-user-reactions.js'; -import HashtagChart from './charts/hashtag.js'; import PerUserFollowingChart from './charts/per-user-following.js'; import PerUserDriveChart from './charts/per-user-drive.js'; import ApRequestChart from './charts/ap-request.js'; @@ -31,7 +30,6 @@ export class ChartManagementService implements OnApplicationShutdown { private perUserPvChart: PerUserPvChart, private driveChart: DriveChart, private perUserReactionsChart: PerUserReactionsChart, - private hashtagChart: HashtagChart, private perUserFollowingChart: PerUserFollowingChart, private perUserDriveChart: PerUserDriveChart, private apRequestChart: ApRequestChart, @@ -46,7 +44,6 @@ export class ChartManagementService implements OnApplicationShutdown { this.perUserPvChart, this.driveChart, this.perUserReactionsChart, - this.hashtagChart, this.perUserFollowingChart, this.perUserDriveChart, this.apRequestChart, diff --git a/packages/backend/src/core/chart/charts/entities/hashtag.ts b/packages/backend/src/core/chart/charts/entities/hashtag.ts deleted file mode 100644 index 4d0403904..000000000 --- a/packages/backend/src/core/chart/charts/entities/hashtag.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Chart from '../../core.js'; - -export const name = 'hashtag'; - -export const schema = { - 'local.users': { uniqueIncrement: true }, - 'remote.users': { uniqueIncrement: true }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/core/chart/charts/hashtag.ts b/packages/backend/src/core/chart/charts/hashtag.ts deleted file mode 100644 index 3899b4136..000000000 --- a/packages/backend/src/core/chart/charts/hashtag.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Injectable, Inject } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import type { User } from '@/models/entities/User.js'; -import { AppLockService } from '@/core/AppLockService.js'; -import { DI } from '@/di-symbols.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { bindThis } from '@/decorators.js'; -import Chart from '../core.js'; -import { ChartLoggerService } from '../ChartLoggerService.js'; -import { name, schema } from './entities/hashtag.js'; -import type { KVs } from '../core.js'; - -/** - * ハッシュタグに関するチャート - */ -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class HashtagChart extends Chart { - constructor( - @Inject(DI.db) - private db: DataSource, - - private appLockService: AppLockService, - private userEntityService: UserEntityService, - private chartLoggerService: ChartLoggerService, - ) { - super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true); - } - - protected async tickMajor(): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - @bindThis - public async update(hashtag: string, user: { id: User['id'], host: User['host'] }): Promise { - await this.commit({ - 'local.users': this.userEntityService.isLocalUser(user) ? [user.id] : [], - 'remote.users': this.userEntityService.isLocalUser(user) ? [] : [user.id], - }, hashtag); - } -} diff --git a/packages/backend/src/core/chart/entities.ts b/packages/backend/src/core/chart/entities.ts index c2759e8b3..b44e2e38b 100644 --- a/packages/backend/src/core/chart/entities.ts +++ b/packages/backend/src/core/chart/entities.ts @@ -7,7 +7,6 @@ import { entity as PerUserNotesChart } from './charts/entities/per-user-notes.js import { entity as PerUserPvChart } from './charts/entities/per-user-pv.js'; import { entity as DriveChart } from './charts/entities/drive.js'; import { entity as PerUserReactionsChart } from './charts/entities/per-user-reactions.js'; -import { entity as HashtagChart } from './charts/entities/hashtag.js'; import { entity as PerUserFollowingChart } from './charts/entities/per-user-following.js'; import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js'; import { entity as ApRequestChart } from './charts/entities/ap-request.js'; @@ -27,7 +26,6 @@ export const entities = [ PerUserPvChart.hour, PerUserPvChart.day, DriveChart.hour, DriveChart.day, PerUserReactionsChart.hour, PerUserReactionsChart.day, - HashtagChart.hour, HashtagChart.day, PerUserFollowingChart.hour, PerUserFollowingChart.day, PerUserDriveChart.hour, PerUserDriveChart.day, ApRequestChart.hour, ApRequestChart.day, diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts index 5e2f019a1..6ce590aa9 100644 --- a/packages/backend/src/core/entities/ChannelEntityService.ts +++ b/packages/backend/src/core/entities/ChannelEntityService.ts @@ -54,7 +54,7 @@ export class ChannelEntityService { name: channel.name, description: channel.description, userId: channel.userId, - bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner, false) : null, + bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null, usersCount: channel.usersCount, notesCount: channel.notesCount, diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index 7f54cfdea..9dd115d45 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -20,6 +20,7 @@ type PackOptions = { withUser?: boolean, }; import { bindThis } from '@/decorators.js'; +import { isMimeImage } from '@/misc/is-mime-image.js'; @Injectable() export class DriveFileEntityService { @@ -71,27 +72,42 @@ export class DriveFileEntityService { } @bindThis - public getPublicUrl(file: DriveFile, thumbnail = false): string | null { + public getPublicUrl(file: DriveFile, mode? : 'static' | 'avatar'): string | null { // static = thumbnail + const proxiedUrl = (url: string) => appendQuery( + `${this.config.mediaProxy}/${mode ?? 'image'}.webp`, + query({ + url, + ...(mode ? { [mode]: '1' } : {}), + }) + ); + // リモートかつメディアプロキシ - if (file.uri != null && file.userHost != null && this.config.mediaProxy != null) { - return appendQuery(this.config.mediaProxy, query({ - url: file.uri, - thumbnail: thumbnail ? '1' : undefined, - })); + if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { + if (!(mode === 'static' && file.type.startsWith('video'))) { + return proxiedUrl(file.uri); + } } // リモートかつ期限切れはローカルプロキシを試みる if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { - const key = thumbnail ? file.thumbnailAccessKey : file.webpublicAccessKey; + const key = mode === 'static' ? file.thumbnailAccessKey : file.webpublicAccessKey; if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外 - return `${this.config.url}/files/${key}`; + const url = `${this.config.url}/files/${key}`; + if (mode === 'avatar') return proxiedUrl(file.uri); + return url; } } - const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/avif', 'image/svg+xml'].includes(file.type); + const url = file.webpublicUrl ?? file.url; - return thumbnail ? (file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null)) : (file.webpublicUrl ?? file.url); + if (mode === 'static') { + return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? proxiedUrl(url) : null); + } + if (mode === 'avatar') { + return proxiedUrl(url); + } + return url; } @bindThis @@ -166,8 +182,8 @@ export class DriveFileEntityService { isSensitive: file.isSensitive, blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), - url: opts.self ? file.url : this.getPublicUrl(file, false), - thumbnailUrl: this.getPublicUrl(file, true), + url: opts.self ? file.url : this.getPublicUrl(file), + thumbnailUrl: this.getPublicUrl(file, 'static'), comment: file.comment, folderId: file.folderId, folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { @@ -201,8 +217,8 @@ export class DriveFileEntityService { isSensitive: file.isSensitive, blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), - url: opts.self ? file.url : this.getPublicUrl(file, false), - thumbnailUrl: this.getPublicUrl(file, true), + url: opts.self ? file.url : this.getPublicUrl(file), + thumbnailUrl: this.getPublicUrl(file, 'static'), comment: file.comment, folderId: file.folderId, folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts index 52f337446..dbb89ff19 100644 --- a/packages/backend/src/core/entities/RoleEntityService.ts +++ b/packages/backend/src/core/entities/RoleEntityService.ts @@ -56,11 +56,13 @@ export class RoleEntityService { name: role.name, description: role.description, color: role.color, + iconUrl: role.iconUrl, target: role.target, condFormula: role.condFormula, isPublic: role.isPublic, isAdministrator: role.isAdministrator, isModerator: role.isModerator, + asBadge: role.asBadge, canEditMembersByModerator: role.canEditMembersByModerator, policies: policies, usersCount: assigns.length, diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 546e61a26..eea9d5567 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -314,10 +314,10 @@ export class UserEntityService implements OnModuleInit { @bindThis public async getAvatarUrl(user: User): Promise { if (user.avatar) { - return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id); + return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user.id); } else if (user.avatarId) { const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId }); - return this.driveFileEntityService.getPublicUrl(avatar, true) ?? this.getIdenticonUrl(user.id); + return this.driveFileEntityService.getPublicUrl(avatar, 'avatar') ?? this.getIdenticonUrl(user.id); } else { return this.getIdenticonUrl(user.id); } @@ -326,7 +326,7 @@ export class UserEntityService implements OnModuleInit { @bindThis public getAvatarUrlSync(user: User): string { if (user.avatar) { - return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id); + return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user.id); } else { return this.getIdenticonUrl(user.id); } @@ -415,6 +415,11 @@ export class UserEntityService implements OnModuleInit { } : undefined) : undefined, emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), onlineStatus: this.getOnlineStatus(user), + // パフォーマンス上の理由でローカルユーザーのみ + badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.map(r => ({ + name: r.name, + iconUrl: r.iconUrl, + }))) : undefined, ...(opts.detail ? { url: profile!.url, @@ -422,7 +427,7 @@ export class UserEntityService implements OnModuleInit { createdAt: user.createdAt.toISOString(), updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null, - bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner, false) : null, + bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner) : null, bannerBlurhash: user.banner?.blurhash ?? null, isLocked: user.isLocked, isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote), @@ -454,6 +459,7 @@ export class UserEntityService implements OnModuleInit { id: role.id, name: role.name, color: role.color, + iconUrl: role.iconUrl, description: role.description, isModerator: role.isModerator, isAdministrator: role.isAdministrator, @@ -489,7 +495,6 @@ export class UserEntityService implements OnModuleInit { hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id), hasUnreadNotification: this.getHasUnreadNotification(user.id), hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), - integrations: profile!.integrations, mutedWords: profile!.mutedWords, mutedInstances: profile!.mutedInstances, mutingNotificationTypes: profile!.mutingNotificationTypes, diff --git a/packages/backend/src/decorators.ts b/packages/backend/src/decorators.ts index 94b1c4be8..db23317ee 100644 --- a/packages/backend/src/decorators.ts +++ b/packages/backend/src/decorators.ts @@ -5,7 +5,7 @@ * The getter will return a .bind version of the function * and memoize the result against a symbol on the instance */ -export function bindThis(target, key, descriptor) { +export function bindThis(target: any, key: string, descriptor: any) { let fn = descriptor.value; if (typeof fn !== 'function') { @@ -34,7 +34,7 @@ export function bindThis(target, key, descriptor) { }); return boundFn; }, - set(value) { + set(value: any) { fn = value; }, }; diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts index 5d275bc7b..91039098f 100644 --- a/packages/backend/src/logger.ts +++ b/packages/backend/src/logger.ts @@ -17,15 +17,13 @@ export default class Logger { private context: Context; private parentLogger: Logger | null = null; private store: boolean; - private syslogClient: any | null = null; - constructor(context: string, color?: KEYWORD, store = true, syslogClient = null) { + constructor(context: string, color?: KEYWORD, store = true) { this.context = { name: context, color: color, }; this.store = store; - this.syslogClient = syslogClient; } @bindThis @@ -47,7 +45,7 @@ export default class Logger { } const time = dateFormat(new Date(), 'HH:mm:ss'); - const worker = cluster.isPrimary ? '*' : cluster.worker.id; + const worker = cluster.isPrimary ? '*' : cluster.worker!.id; const l = level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') : level === 'warning' ? chalk.yellow('WARN') : @@ -69,20 +67,6 @@ export default class Logger { console.log(important ? chalk.bold(log) : log); if (level === 'error' && data) console.log(data); - - if (store) { - if (this.syslogClient) { - const send = - level === 'error' ? this.syslogClient.error : - level === 'warning' ? this.syslogClient.warning : - level === 'success' ? this.syslogClient.info : - level === 'debug' ? this.syslogClient.info : - level === 'info' ? this.syslogClient.info : - null as never; - - send.bind(this.syslogClient)(message).catch(() => {}); - } - } } @bindThis diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 69512498f..43a71a2b5 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,5 +1,7 @@ import { bindThis } from '@/decorators.js'; +// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? + export class Cache { public cache: Map; private lifetime: number; diff --git a/packages/backend/src/misc/gen-identicon.ts b/packages/backend/src/misc/gen-identicon.ts index 4a70d7a4b..b40745973 100644 --- a/packages/backend/src/misc/gen-identicon.ts +++ b/packages/backend/src/misc/gen-identicon.ts @@ -51,7 +51,7 @@ export function genIdenticon(seed: string, stream: WriteStream): Promise { bg.addColorStop(0, bgColors[0]); bg.addColorStop(1, bgColors[1]); - ctx.fillStyle = bg; + ctx.fillStyle = bg as any; ctx.beginPath(); ctx.fillRect(0, 0, size, size); diff --git a/packages/backend/src/misc/i18n.ts b/packages/backend/src/misc/i18n.ts index e304a8ada..b1c727827 100644 --- a/packages/backend/src/misc/i18n.ts +++ b/packages/backend/src/misc/i18n.ts @@ -11,10 +11,9 @@ export class I18n> { // string にしているのは、ドット区切りでのパス指定を許可するため // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも - @bindThis public t(key: string, args?: Record): string { try { - let str = key.split('.').reduce((o, i) => o[i], this.locale) as string; + let str = key.split('.').reduce((o, i) => o[i], this.locale as any) as string; if (args) { for (const [k, v] of Object.entries(args)) { diff --git a/packages/backend/src/misc/nyaize.ts b/packages/backend/src/misc/nyaize.ts index 500d1db2c..350f8d217 100644 --- a/packages/backend/src/misc/nyaize.ts +++ b/packages/backend/src/misc/nyaize.ts @@ -1,14 +1,14 @@ export function nyaize(text: string): string { return text // ja-JP - .replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ') + .replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ') // en-US .replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya') .replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan') .replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan') // ko-KR .replace(/[나-낳]/g, match => String.fromCharCode( - match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0) + match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0), )) .replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥') .replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥'); diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts index 5d222a6da..9d777c623 100644 --- a/packages/backend/src/models/entities/Meta.ts +++ b/packages/backend/src/models/entities/Meta.ts @@ -279,57 +279,6 @@ export class Meta { }) public swPrivateKey: string | null; - @Column('boolean', { - default: false, - }) - public enableTwitterIntegration: boolean; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public twitterConsumerKey: string | null; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public twitterConsumerSecret: string | null; - - @Column('boolean', { - default: false, - }) - public enableGithubIntegration: boolean; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public githubClientId: string | null; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public githubClientSecret: string | null; - - @Column('boolean', { - default: false, - }) - public enableDiscordIntegration: boolean; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public discordClientId: string | null; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public discordClientSecret: string | null; - @Column('varchar', { length: 128, nullable: true, diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/entities/Role.ts index abd5f864a..8cf681186 100644 --- a/packages/backend/src/models/entities/Role.ts +++ b/packages/backend/src/models/entities/Role.ts @@ -102,6 +102,11 @@ export class Role { }) public color: string | null; + @Column('varchar', { + length: 512, nullable: true, + }) + public iconUrl: string | null; + @Column('enum', { enum: ['manual', 'conditional'], default: 'manual', @@ -118,6 +123,12 @@ export class Role { }) public isPublic: boolean; + // trueの場合ユーザー名の横にバッジとして表示 + @Column('boolean', { + default: false, + }) + public asBadge: boolean; + @Column('boolean', { default: false, }) diff --git a/packages/backend/src/models/entities/UserProfile.ts b/packages/backend/src/models/entities/UserProfile.ts index 86df8d5d9..1ff261cda 100644 --- a/packages/backend/src/models/entities/UserProfile.ts +++ b/packages/backend/src/models/entities/UserProfile.ts @@ -184,11 +184,6 @@ export class UserProfile { @JoinColumn() public pinnedPage: Page | null; - @Column('jsonb', { - default: {}, - }) - public integrations: Record; - @Index() @Column('boolean', { default: false, select: false, diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts index aac5e9332..1fc935253 100644 --- a/packages/backend/src/models/schema/user.ts +++ b/packages/backend/src/models/schema/user.ts @@ -323,10 +323,6 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: false, optional: false, }, - integrations: { - type: 'object', - nullable: true, optional: false, - }, mutedWords: { type: 'array', nullable: false, optional: false, diff --git a/packages/backend/src/postgre.ts b/packages/backend/src/postgres.ts similarity index 99% rename from packages/backend/src/postgre.ts rename to packages/backend/src/postgres.ts index c55cb78a6..33b924e77 100644 --- a/packages/backend/src/postgre.ts +++ b/packages/backend/src/postgres.ts @@ -197,7 +197,7 @@ export const entities = [ const log = process.env.NODE_ENV !== 'production'; -export function createPostgreDataSource(config: Config) { +export function createPostgresDataSource(config: Config) { return new DataSource({ type: 'postgres', host: config.db.host, diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts index 2adf7cbe6..5254d3c7d 100644 --- a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts @@ -12,7 +12,6 @@ import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; import PerUserPvChart from '@/core/chart/charts/per-user-pv.js'; import DriveChart from '@/core/chart/charts/drive.js'; import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; -import HashtagChart from '@/core/chart/charts/hashtag.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; @@ -37,7 +36,6 @@ export class CleanChartsProcessorService { private perUserPvChart: PerUserPvChart, private driveChart: DriveChart, private perUserReactionsChart: PerUserReactionsChart, - private hashtagChart: HashtagChart, private perUserFollowingChart: PerUserFollowingChart, private perUserDriveChart: PerUserDriveChart, private apRequestChart: ApRequestChart, @@ -61,7 +59,6 @@ export class CleanChartsProcessorService { this.perUserPvChart.clean(), this.driveChart.clean(), this.perUserReactionsChart.clean(), - this.hashtagChart.clean(), this.perUserFollowingChart.clean(), this.perUserDriveChart.clean(), this.apRequestChart.clean(), diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index 87b23f189..df024a8f3 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -12,9 +12,9 @@ import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp, createTempDir } from '@/misc/create-temp.js'; import { DownloadService } from '@/core/DownloadService.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ExportCustomEmojisProcessorService { @@ -82,6 +82,10 @@ export class ExportCustomEmojisProcessorService { }); for (const emoji of customEmojis) { + if (!/^[a-zA-Z0-9_]+$/.test(emoji.name)) { + this.logger.error(`invalid emoji name: ${emoji.name}`); + continue; + } const ext = mime.extension(emoji.type ?? 'image/png'); const fileName = emoji.name + (ext ? '.' + ext : ''); const emojiPath = path + '/' + fileName; diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 0061c2a8f..2d43615e2 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -81,6 +81,10 @@ export class ImportCustomEmojisProcessorService { for (const record of meta.emojis) { if (!record.downloaded) continue; + if (!/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/.test(record.fileName)) { + this.logger.error(`invalid filename: ${record.fileName}`); + continue; + } const emojiInfo = record.emoji; const emojiPath = outputPath + '/' + record.fileName; await this.emojisRepository.delete({ diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts index 1a8fe65a4..74e7c632d 100644 --- a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts @@ -11,13 +11,12 @@ import InstanceChart from '@/core/chart/charts/instance.js'; import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; import DriveChart from '@/core/chart/charts/drive.js'; import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; -import HashtagChart from '@/core/chart/charts/hashtag.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ResyncChartsProcessorService { @@ -35,7 +34,6 @@ export class ResyncChartsProcessorService { private perUserNotesChart: PerUserNotesChart, private driveChart: DriveChart, private perUserReactionsChart: PerUserReactionsChart, - private hashtagChart: HashtagChart, private perUserFollowingChart: PerUserFollowingChart, private perUserDriveChart: PerUserDriveChart, private apRequestChart: ApRequestChart, diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts index 51eff2a15..751e02dc2 100644 --- a/packages/backend/src/queue/processors/TickChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts @@ -12,7 +12,6 @@ import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; import PerUserPvChart from '@/core/chart/charts/per-user-pv.js'; import DriveChart from '@/core/chart/charts/drive.js'; import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; -import HashtagChart from '@/core/chart/charts/hashtag.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; @@ -37,7 +36,6 @@ export class TickChartsProcessorService { private perUserPvChart: PerUserPvChart, private driveChart: DriveChart, private perUserReactionsChart: PerUserReactionsChart, - private hashtagChart: HashtagChart, private perUserFollowingChart: PerUserFollowingChart, private perUserDriveChart: PerUserDriveChart, private apRequestChart: ApRequestChart, @@ -61,7 +59,6 @@ export class TickChartsProcessorService { this.perUserPvChart.tick(false), this.driveChart.tick(false), this.perUserReactionsChart.tick(false), - this.hashtagChart.tick(false), this.perUserFollowingChart.tick(false), this.perUserDriveChart.tick(false), this.apRequestChart.tick(false), diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index bdd2e9750..186d3822d 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -1,3 +1,4 @@ +import { IncomingMessage } from 'node:http'; import { Inject, Injectable } from '@nestjs/common'; import fastifyAccepts from '@fastify/accepts'; import httpSignature from '@peertube/http-signature'; @@ -19,6 +20,7 @@ import { QueryService } from '@/core/QueryService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { IActivity } from '@/core/activitypub/type.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; import type { FindOptionsWhere } from 'typeorm'; @@ -97,7 +99,8 @@ export class ActivityPubServerService { return; } - this.queueService.inbox(request.body, signature); + // TODO: request.bodyのバリデーション? + this.queueService.inbox(request.body as IActivity, signature); reply.code(202); } @@ -413,20 +416,21 @@ export class ActivityPubServerService { @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { - fastify.addConstraintStrategy({ + // addConstraintStrategy の型定義がおかしいため + (fastify.addConstraintStrategy as any)({ name: 'apOrHtml', storage() { - const store = {}; + const store = {} as any; return { - get(key) { + get(key: string) { return store[key] ?? null; }, - set(key, value) { + set(key: string, value: any) { store[key] = value; }, }; }, - deriveConstraint(request, ctx) { + deriveConstraint(request: IncomingMessage) { const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]); const isAp = typeof accepted === 'string' && !accepted.match(/html/); return isAp ? 'ap' : 'html'; @@ -536,6 +540,7 @@ export class ActivityPubServerService { return (this.apRendererService.renderActivity(this.apRendererService.renderKey(user, keypair))); } else { reply.code(400); + return; } }); diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 40024270a..49ded6c28 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -137,38 +137,42 @@ export class FileServerService { try { if (file.state === 'remote') { - const convertFile = async () => { - if (file.fileRole === 'thumbnail') { - if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(file.mime)) { - return this.imageProcessingService.convertToWebpStream( - file.path, - 498, - 280 - ); - } else if (file.mime.startsWith('video/')) { - return await this.videoProcessingService.generateVideoThumbnail(file.path); - } - } + let image: IImageStreamable | null = null; - if (file.fileRole === 'webpublic') { - if (['image/svg+xml'].includes(file.mime)) { - return this.imageProcessingService.convertToWebpStream( - file.path, - 2048, - 2048, - { ...webpDefault, lossless: true } - ) - } - } + if (file.fileRole === 'thumbnail') { + if (isMimeImage(file.mime, 'sharp-convertible-image')) { + reply.header('Cache-Control', 'max-age=31536000, immutable'); - return { + const url = new URL(`${this.config.mediaProxy}/static.webp`); + url.searchParams.set('url', file.url); + url.searchParams.set('static', '1'); + + file.cleanup(); + return await reply.redirect(301, url.toString()); + } else if (file.mime.startsWith('video/')) { + image = await this.videoProcessingService.generateVideoThumbnail(file.path); + } + } + + if (file.fileRole === 'webpublic') { + if (['image/svg+xml'].includes(file.mime)) { + reply.header('Cache-Control', 'max-age=31536000, immutable'); + + const url = new URL(`${this.config.mediaProxy}/svg.webp`); + url.searchParams.set('url', file.url); + + file.cleanup(); + return await reply.redirect(301, url.toString()); + } + } + + if (!image) { + image = { data: fs.createReadStream(file.path), ext: file.ext, type: file.mime, }; - }; - - const image = await convertFile(); + } if ('pipe' in image.data && typeof image.data.pipe === 'function') { // image.dataがstreamなら、stream終了後にcleanup @@ -180,7 +184,6 @@ export class FileServerService { } reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream'); - reply.header('Cache-Control', 'max-age=31536000, immutable'); return image.data; } @@ -217,6 +220,23 @@ export class FileServerService { return; } + if (this.config.externalMediaProxyEnabled) { + // 外部のメディアプロキシが有効なら、そちらにリダイレクト + + reply.header('Cache-Control', 'public, max-age=259200'); // 3 days + + const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`); + + for (const [key, value] of Object.entries(request.query)) { + url.searchParams.append(key, value); + } + + return await reply.redirect( + 301, + url.toString(), + ); + } + // Create temp file const file = await this.getStreamAndTypeFromUrl(url); if (file === '404') { @@ -235,8 +255,21 @@ export class FileServerService { const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image'); const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image'); + if ( + 'emoji' in request.query || + 'avatar' in request.query || + 'static' in request.query || + 'preview' in request.query || + 'badge' in request.query + ) { + if (!isConvertibleImage) { + // 画像でないなら404でお茶を濁す + throw new StatusError('Unexpected mime', 404); + } + } + let image: IImageStreamable | null = null; - if ('emoji' in request.query && isConvertibleImage) { + if ('emoji' in request.query || 'avatar' in request.query) { if (!isAnimationConvertibleImage && !('static' in request.query)) { image = { data: fs.createReadStream(file.path), @@ -246,7 +279,7 @@ export class FileServerService { } else { const data = sharp(file.path, { animated: !('static' in request.query) }) .resize({ - height: 128, + height: 'emoji' in request.query ? 128 : 320, withoutEnlargement: true, }) .webp(webpDefault); @@ -257,16 +290,11 @@ export class FileServerService { type: 'image/webp', }; } - } else if ('static' in request.query && isConvertibleImage) { + } else if ('static' in request.query) { image = this.imageProcessingService.convertToWebpStream(file.path, 498, 280); - } else if ('preview' in request.query && isConvertibleImage) { + } else if ('preview' in request.query) { image = this.imageProcessingService.convertToWebpStream(file.path, 200, 200); } else if ('badge' in request.query) { - if (!isConvertibleImage) { - // 画像でないなら404でお茶を濁す - throw new StatusError('Unexpected mime', 404); - } - const mask = sharp(file.path) .resize(96, 96, { fit: 'inside', @@ -370,7 +398,7 @@ export class FileServerService { @bindThis private async getFileFromKey(key: string): Promise< - { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; } + { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; } | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; } | '404' | '204' @@ -392,6 +420,7 @@ export class FileServerService { const result = await this.downloadAndDetectTypeFromUrl(file.uri); return { ...result, + url: file.uri, fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original', file, } diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 024ddfe63..a43630c04 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -111,9 +111,6 @@ export class NodeinfoServerService { enableHcaptcha: meta.enableHcaptcha, enableRecaptcha: meta.enableRecaptcha, maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, - enableTwitterIntegration: meta.enableTwitterIntegration, - enableGithubIntegration: meta.enableGithubIntegration, - enableDiscordIntegration: meta.enableDiscordIntegration, enableEmail: meta.enableEmail, enableServiceWorker: meta.enableServiceWorker, proxyAccountName: proxyAccount ? proxyAccount.username : null, diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index 9dc152769..b605f3c8a 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -7,9 +7,6 @@ import { NodeinfoServerService } from './NodeinfoServerService.js'; import { ServerService } from './ServerService.js'; import { WellKnownServerService } from './WellKnownServerService.js'; import { GetterService } from './api/GetterService.js'; -import { DiscordServerService } from './api/integration/DiscordServerService.js'; -import { GithubServerService } from './api/integration/GithubServerService.js'; -import { TwitterServerService } from './api/integration/TwitterServerService.js'; import { ChannelsService } from './api/stream/ChannelsService.js'; import { ActivityPubServerService } from './ActivityPubServerService.js'; import { ApiLoggerService } from './api/ApiLoggerService.js'; @@ -54,9 +51,6 @@ import { UserListChannelService } from './api/stream/channels/user-list.js'; ServerService, WellKnownServerService, GetterService, - DiscordServerService, - GithubServerService, - TwitterServerService, ChannelsService, ApiCallService, ApiLoggerService, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index beb3a34ec..f76871c60 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -106,7 +106,7 @@ export class ServerService { } } - const url = new URL('/proxy/emoji.webp', this.config.url); + const url = new URL(`${this.config.mediaProxy}/emoji.webp`); // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); url.searchParams.set('emoji', '1'); @@ -166,6 +166,7 @@ export class ServerService { return 'Verify succeeded!'; } else { reply.code(404); + return; } }); diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index b29c9616c..e406949cd 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -12,9 +12,6 @@ import endpoints, { IEndpoint } from './endpoints.js'; import { ApiCallService } from './ApiCallService.js'; import { SignupApiService } from './SignupApiService.js'; import { SigninApiService } from './SigninApiService.js'; -import { GithubServerService } from './integration/GithubServerService.js'; -import { DiscordServerService } from './integration/DiscordServerService.js'; -import { TwitterServerService } from './integration/TwitterServerService.js'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; @Injectable() @@ -38,9 +35,6 @@ export class ApiServerService { private apiCallService: ApiCallService, private signupApiService: SignupApiService, private signinApiService: SigninApiService, - private githubServerService: GithubServerService, - private discordServerService: DiscordServerService, - private twitterServerService: TwitterServerService, ) { //this.createServer = this.createServer.bind(this); } @@ -133,10 +127,6 @@ export class ApiServerService { fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply)); - fastify.register(this.discordServerService.create); - fastify.register(this.githubServerService.create); - fastify.register(this.twitterServerService.create); - fastify.get('/v1/instance/peers', async (request, reply) => { const instances = await this.instancesRepository.find({ select: ['host'], diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 466651f37..4a55c6cbe 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -97,7 +97,6 @@ import * as ep___charts_activeUsers from './endpoints/charts/active-users.js'; import * as ep___charts_apRequest from './endpoints/charts/ap-request.js'; import * as ep___charts_drive from './endpoints/charts/drive.js'; import * as ep___charts_federation from './endpoints/charts/federation.js'; -import * as ep___charts_hashtag from './endpoints/charts/hashtag.js'; import * as ep___charts_instance from './endpoints/charts/instance.js'; import * as ep___charts_notes from './endpoints/charts/notes.js'; import * as ep___charts_user_drive from './endpoints/charts/user/drive.js'; @@ -433,7 +432,6 @@ const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useCl const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default }; const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default }; const $charts_federation: Provider = { provide: 'ep:charts/federation', useClass: ep___charts_federation.default }; -const $charts_hashtag: Provider = { provide: 'ep:charts/hashtag', useClass: ep___charts_hashtag.default }; const $charts_instance: Provider = { provide: 'ep:charts/instance', useClass: ep___charts_instance.default }; const $charts_notes: Provider = { provide: 'ep:charts/notes', useClass: ep___charts_notes.default }; const $charts_user_drive: Provider = { provide: 'ep:charts/user/drive', useClass: ep___charts_user_drive.default }; @@ -773,7 +771,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $charts_apRequest, $charts_drive, $charts_federation, - $charts_hashtag, $charts_instance, $charts_notes, $charts_user_drive, @@ -1107,7 +1104,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $charts_apRequest, $charts_drive, $charts_federation, - $charts_hashtag, $charts_instance, $charts_notes, $charts_user_drive, diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts index a9c34e363..1f8915ecc 100644 --- a/packages/backend/src/server/api/RateLimiterService.ts +++ b/packages/backend/src/server/api/RateLimiterService.ts @@ -34,7 +34,7 @@ export class RateLimiterService { const min = (): void => { const minIntervalLimiter = new Limiter({ id: `${actor}:${limitation.key}:min`, - duration: limitation.minInterval * factor, + duration: limitation.minInterval! * factor, max: 1, db: this.redisClient, }); @@ -62,8 +62,8 @@ export class RateLimiterService { const max = (): void => { const limiter = new Limiter({ id: `${actor}:${limitation.key}`, - duration: limitation.duration * factor, - max: limitation.max / factor, + duration: limitation.duration! * factor, + max: limitation.max! / factor, db: this.redisClient, }); diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 10f8423d4..d490097de 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -10,9 +10,9 @@ import { getIpHash } from '@/misc/get-ip-hash.js'; import type { ILocalUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; +import { bindThis } from '@/decorators.js'; import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; -import { bindThis } from '@/decorators.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() @@ -131,7 +131,7 @@ export class SigninApiService { createdAt: new Date(), userId: user.id, ip: request.ip, - headers: request.headers, + headers: request.headers as any, success: false, }); diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts index 89a8a9ff1..c78d9f85c 100644 --- a/packages/backend/src/server/api/SigninService.ts +++ b/packages/backend/src/server/api/SigninService.ts @@ -25,7 +25,7 @@ export class SigninService { } @bindThis - public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser, redirect = false) { + public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser) { setImmediate(async () => { // Append signin history const record = await this.signinsRepository.insert({ @@ -33,7 +33,7 @@ export class SigninService { createdAt: new Date(), userId: user.id, ip: request.ip, - headers: request.headers, + headers: request.headers as any, success: true, }).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0])); @@ -41,25 +41,11 @@ export class SigninService { this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record)); }); - if (redirect) { - //#region Cookie - reply.setCookie('igi', user.token!, { - path: '/', - // SEE: https://github.com/koajs/koa/issues/974 - // When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header - secure: this.config.url.startsWith('https'), - httpOnly: false, - }); - //#endregion - - reply.redirect(this.config.url); - } else { - reply.code(200); - return { - id: user.id, - i: user.token, - }; - } + reply.code(200); + return { + id: user.id, + i: user.token, + }; } } diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index 4b676bb8b..ffd7e203e 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -146,6 +146,7 @@ export class SignupApiService { `To complete signup, please click this link: ${link}`); reply.code(204); + return; } else { try { const { account, secret } = await this.signupService.signup({ @@ -162,7 +163,7 @@ export class SignupApiService { token: secret, }; } catch (err) { - throw new FastifyReplyError(400, err); + throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString()); } } } @@ -195,7 +196,7 @@ export class SignupApiService { return this.signinService.signin(request, reply, account as ILocalUser); } catch (err) { - throw new FastifyReplyError(400, err); + throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString()); } } } diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 3678fe14e..55e1900d5 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -96,7 +96,6 @@ import * as ep___charts_activeUsers from './endpoints/charts/active-users.js'; import * as ep___charts_apRequest from './endpoints/charts/ap-request.js'; import * as ep___charts_drive from './endpoints/charts/drive.js'; import * as ep___charts_federation from './endpoints/charts/federation.js'; -import * as ep___charts_hashtag from './endpoints/charts/hashtag.js'; import * as ep___charts_instance from './endpoints/charts/instance.js'; import * as ep___charts_notes from './endpoints/charts/notes.js'; import * as ep___charts_user_drive from './endpoints/charts/user/drive.js'; @@ -430,7 +429,6 @@ const eps = [ ['charts/ap-request', ep___charts_apRequest], ['charts/drive', ep___charts_drive], ['charts/federation', ep___charts_federation], - ['charts/hashtag', ep___charts_hashtag], ['charts/instance', ep___charts_instance], ['charts/notes', ep___charts_notes], ['charts/user/drive', ep___charts_user_drive], diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index b39382705..2b19104ea 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -138,18 +138,6 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - enableTwitterIntegration: { - type: 'boolean', - optional: false, nullable: false, - }, - enableGithubIntegration: { - type: 'boolean', - optional: false, nullable: false, - }, - enableDiscordIntegration: { - type: 'boolean', - optional: false, nullable: false, - }, enableServiceWorker: { type: 'boolean', optional: false, nullable: false, @@ -223,30 +211,6 @@ export const meta = { optional: true, nullable: true, format: 'id', }, - twitterConsumerKey: { - type: 'string', - optional: true, nullable: true, - }, - twitterConsumerSecret: { - type: 'string', - optional: true, nullable: true, - }, - githubClientId: { - type: 'string', - optional: true, nullable: true, - }, - githubClientSecret: { - type: 'string', - optional: true, nullable: true, - }, - discordClientId: { - type: 'string', - optional: true, nullable: true, - }, - discordClientSecret: { - type: 'string', - optional: true, nullable: true, - }, summaryProxy: { type: 'string', optional: true, nullable: true, @@ -389,9 +353,6 @@ export default class extends Endpoint { defaultLightTheme: instance.defaultLightTheme, defaultDarkTheme: instance.defaultDarkTheme, enableEmail: instance.enableEmail, - enableTwitterIntegration: instance.enableTwitterIntegration, - enableGithubIntegration: instance.enableGithubIntegration, - enableDiscordIntegration: instance.enableDiscordIntegration, enableServiceWorker: instance.enableServiceWorker, translatorAvailable: instance.deeplAuthKey != null, pinnedPages: instance.pinnedPages, @@ -409,12 +370,6 @@ export default class extends Endpoint { setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, proxyAccountId: instance.proxyAccountId, - twitterConsumerKey: instance.twitterConsumerKey, - twitterConsumerSecret: instance.twitterConsumerSecret, - githubClientId: instance.githubClientId, - githubClientSecret: instance.githubClientSecret, - discordClientId: instance.discordClientId, - discordClientSecret: instance.discordClientSecret, summalyProxy: instance.summalyProxy, email: instance.email, smtpSecure: instance.smtpSecure, diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts index f136c6d62..df60c6be9 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts @@ -19,11 +19,13 @@ export const paramDef = { name: { type: 'string' }, description: { type: 'string' }, color: { type: 'string', nullable: true }, - target: { type: 'string' }, + iconUrl: { type: 'string', nullable: true }, + target: { type: 'string', enum: ['manual', 'conditional'] }, condFormula: { type: 'object' }, isPublic: { type: 'boolean' }, isModerator: { type: 'boolean' }, isAdministrator: { type: 'boolean' }, + asBadge: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' }, policies: { type: 'object', @@ -33,11 +35,13 @@ export const paramDef = { 'name', 'description', 'color', + 'iconUrl', 'target', 'condFormula', 'isPublic', 'isModerator', 'isAdministrator', + 'asBadge', 'canEditMembersByModerator', 'policies', ], @@ -64,11 +68,13 @@ export default class extends Endpoint { name: ps.name, description: ps.description, color: ps.color, + iconUrl: ps.iconUrl, target: ps.target, condFormula: ps.condFormula, isPublic: ps.isPublic, isAdministrator: ps.isAdministrator, isModerator: ps.isModerator, + asBadge: ps.asBadge, canEditMembersByModerator: ps.canEditMembersByModerator, policies: ps.policies, }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts index fc4c3d8f1..b939ccdbf 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -27,11 +27,13 @@ export const paramDef = { name: { type: 'string' }, description: { type: 'string' }, color: { type: 'string', nullable: true }, - target: { type: 'string' }, + iconUrl: { type: 'string', nullable: true }, + target: { type: 'string', enum: ['manual', 'conditional'] }, condFormula: { type: 'object' }, isPublic: { type: 'boolean' }, isModerator: { type: 'boolean' }, isAdministrator: { type: 'boolean' }, + asBadge: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' }, policies: { type: 'object', @@ -42,11 +44,13 @@ export const paramDef = { 'name', 'description', 'color', + 'iconUrl', 'target', 'condFormula', 'isPublic', 'isModerator', 'isAdministrator', + 'asBadge', 'canEditMembersByModerator', 'policies', ], @@ -73,11 +77,13 @@ export default class extends Endpoint { name: ps.name, description: ps.description, color: ps.color, + iconUrl: ps.iconUrl, target: ps.target, condFormula: ps.condFormula, isPublic: ps.isPublic, isModerator: ps.isModerator, isAdministrator: ps.isAdministrator, + asBadge: ps.asBadge, canEditMembersByModerator: ps.canEditMembersByModerator, policies: ps.policies, }); diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 94603cc91..823af6d8b 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -65,11 +65,6 @@ export default class extends Endpoint { }; } - const maskedKeys = ['accessToken', 'accessTokenSecret', 'refreshToken']; - Object.keys(profile.integrations).forEach(integration => { - maskedKeys.forEach(key => profile.integrations[integration][key] = ''); - }); - const signins = await this.signinsRepository.findBy({ userId: user.id }); const roles = await this.roleService.getUserRoles(user.id); @@ -84,7 +79,6 @@ export default class extends Endpoint { carefulBot: profile.carefulBot, injectFeaturedNote: profile.injectFeaturedNote, receiveAnnouncementEmail: profile.receiveAnnouncementEmail, - integrations: profile.integrations, mutedWords: profile.mutedWords, mutedInstances: profile.mutedInstances, mutingNotificationTypes: profile.mutingNotificationTypes, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index aacd634ed..354ef22aa 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -68,15 +68,6 @@ export const paramDef = { summalyProxy: { type: 'string', nullable: true }, deeplAuthKey: { type: 'string', nullable: true }, deeplIsPro: { type: 'boolean' }, - enableTwitterIntegration: { type: 'boolean' }, - twitterConsumerKey: { type: 'string', nullable: true }, - twitterConsumerSecret: { type: 'string', nullable: true }, - enableGithubIntegration: { type: 'boolean' }, - githubClientId: { type: 'string', nullable: true }, - githubClientSecret: { type: 'string', nullable: true }, - enableDiscordIntegration: { type: 'boolean' }, - discordClientId: { type: 'string', nullable: true }, - discordClientSecret: { type: 'string', nullable: true }, enableEmail: { type: 'boolean' }, email: { type: 'string', nullable: true }, smtpSecure: { type: 'boolean' }, @@ -270,42 +261,6 @@ export default class extends Endpoint { set.summalyProxy = ps.summalyProxy; } - if (ps.enableTwitterIntegration !== undefined) { - set.enableTwitterIntegration = ps.enableTwitterIntegration; - } - - if (ps.twitterConsumerKey !== undefined) { - set.twitterConsumerKey = ps.twitterConsumerKey; - } - - if (ps.twitterConsumerSecret !== undefined) { - set.twitterConsumerSecret = ps.twitterConsumerSecret; - } - - if (ps.enableGithubIntegration !== undefined) { - set.enableGithubIntegration = ps.enableGithubIntegration; - } - - if (ps.githubClientId !== undefined) { - set.githubClientId = ps.githubClientId; - } - - if (ps.githubClientSecret !== undefined) { - set.githubClientSecret = ps.githubClientSecret; - } - - if (ps.enableDiscordIntegration !== undefined) { - set.enableDiscordIntegration = ps.enableDiscordIntegration; - } - - if (ps.discordClientId !== undefined) { - set.discordClientId = ps.discordClientId; - } - - if (ps.discordClientSecret !== undefined) { - set.discordClientSecret = ps.discordClientSecret; - } - if (ps.enableEmail !== undefined) { set.enableEmail = ps.enableEmail; } diff --git a/packages/backend/src/server/api/endpoints/charts/hashtag.ts b/packages/backend/src/server/api/endpoints/charts/hashtag.ts deleted file mode 100644 index 71e5bab76..000000000 --- a/packages/backend/src/server/api/endpoints/charts/hashtag.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { getJsonSchema } from '@/core/chart/core.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import HashtagChart from '@/core/chart/charts/hashtag.js'; -import { schema } from '@/core/chart/charts/entities/hashtag.js'; - -export const meta = { - tags: ['charts', 'hashtags'], - - res: getJsonSchema(schema), - - allowGet: true, - cacheSec: 60 * 60, -} as const; - -export const paramDef = { - type: 'object', - properties: { - span: { type: 'string', enum: ['day', 'hour'] }, - limit: { type: 'integer', minimum: 1, maximum: 500, default: 30 }, - offset: { type: 'integer', nullable: true, default: null }, - tag: { type: 'string' }, - }, - required: ['span', 'tag'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - private hashtagChart: HashtagChart, - ) { - super(meta, paramDef, async (ps, me) => { - return await this.hashtagChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.tag); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/charts/user/pv.ts b/packages/backend/src/server/api/endpoints/charts/user/pv.ts index c920e0f57..33652c3ad 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/pv.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/pv.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import PerUserPvChart from '@/core/chart/charts/per-user-pv.js'; -import { schema } from '@/core/chart/charts/entities/per-user-notes.js'; +import { schema } from '@/core/chart/charts/entities/per-user-pv.js'; export const meta = { tags: ['charts', 'users'], diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts index a337a05f8..13b91685a 100644 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ b/packages/backend/src/server/api/endpoints/endpoint.ts @@ -27,7 +27,7 @@ export default class extends Endpoint { return { params: Object.entries(ep.params.properties ?? {}).map(([k, v]) => ({ name: k, - type: v.type.charAt(0).toUpperCase() + v.type.slice(1), + type: v.type ? v.type.charAt(0).toUpperCase() + v.type.slice(1) : 'string', })), }; }); diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts index 52ae5475b..d7109c695 100644 --- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts +++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { AchievementService } from '@/core/AchievementService.js'; +import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js'; export const meta = { requireCredential: true, @@ -10,7 +10,7 @@ export const meta = { export const paramDef = { type: 'object', properties: { - name: { type: 'string' }, + name: { type: 'string', enum: ACHIEVEMENT_TYPES }, }, required: ['name'], } as const; diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index f46cb3328..a82f55ce7 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -15,8 +15,8 @@ export const meta = { requireCredential: true, limit: { - duration: 60000, - max: 100, + duration: 30000, + max: 50, }, kind: 'read:notifications', diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 89fa50317..2fa7a09d4 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -169,18 +169,6 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - enableTwitterIntegration: { - type: 'boolean', - optional: false, nullable: false, - }, - enableGithubIntegration: { - type: 'boolean', - optional: false, nullable: false, - }, - enableDiscordIntegration: { - type: 'boolean', - optional: false, nullable: false, - }, enableServiceWorker: { type: 'boolean', optional: false, nullable: false, @@ -193,6 +181,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + mediaProxy: { + type: 'string', + optional: false, nullable: false, + }, features: { type: 'object', optional: true, nullable: false, @@ -225,18 +217,6 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - twitter: { - type: 'boolean', - optional: false, nullable: false, - }, - github: { - type: 'boolean', - optional: false, nullable: false, - }, - discord: { - type: 'boolean', - optional: false, nullable: false, - }, serviceWorker: { type: 'boolean', optional: false, nullable: false, @@ -325,17 +305,14 @@ export default class extends Endpoint { imageUrl: ad.imageUrl, })), enableEmail: instance.enableEmail, - - enableTwitterIntegration: instance.enableTwitterIntegration, - enableGithubIntegration: instance.enableGithubIntegration, - enableDiscordIntegration: instance.enableDiscordIntegration, - enableServiceWorker: instance.enableServiceWorker, translatorAvailable: instance.deeplAuthKey != null, policies: { ...DEFAULT_POLICIES, ...instance.policies }, + mediaProxy: this.config.mediaProxy, + ...(ps.detail ? { pinnedPages: instance.pinnedPages, pinnedClipId: instance.pinnedClipId, @@ -358,9 +335,6 @@ export default class extends Endpoint { recaptcha: instance.enableRecaptcha, turnstile: instance.enableTurnstile, objectStorage: instance.useObjectStorage, - twitter: instance.enableTwitterIntegration, - github: instance.enableGithubIntegration, - discord: instance.enableDiscordIntegration, serviceWorker: instance.enableServiceWorker, miauth: true, }; diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index e423f0f10..0ce80a1a6 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -5,8 +5,8 @@ import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; import { AchievementService } from '@/core/AchievementService.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['notes', 'favorites'], @@ -79,7 +79,7 @@ export default class extends Endpoint { userId: me.id, }); - if (note.userHost == null) { + if (note.userHost == null && note.userId !== me.id) { this.achievementService.create(note.userId, 'myNoteFavorited1'); } }); diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index d583dfb93..befaea466 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -1,6 +1,6 @@ import { Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, BlockingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; +import type { UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; import type { IRemoteUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -11,6 +11,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { DI } from '@/di-symbols.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -77,9 +78,6 @@ export default class extends Endpoint { @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, @@ -93,6 +91,7 @@ export default class extends Endpoint { private apRendererService: ApRendererService, private globalEventService: GlobalEventService, private createNotificationService: CreateNotificationService, + private userBlockingService: UserBlockingService, ) { super(meta, paramDef, async (ps, me) => { const createdAt = new Date(); @@ -109,11 +108,8 @@ export default class extends Endpoint { // Check blocking if (note.userId !== me.id) { - const block = await this.blockingsRepository.findOneBy({ - blockerId: note.userId, - blockeeId: me.id, - }); - if (block) { + const blocked = await this.userBlockingService.checkBlocked(note.userId, me.id); + if (blocked) { throw new ApiError(meta.errors.youHaveBeenBlocked); } } diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index 061e371d6..bcd793ac4 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -95,14 +95,14 @@ export default class extends Endpoint { try { if (ps.tag) { - if (!safeForSql(ps.tag)) throw 'Injection'; + if (!safeForSql(normalizeForSearch(ps.tag))) throw 'Injection'; query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`); } else { query.andWhere(new Brackets(qb => { for (const tags of ps.query!) { qb.orWhere(new Brackets(qb => { for (const tag of tags) { - if (!safeForSql(tag)) throw 'Injection'; + if (!safeForSql(normalizeForSearch(tag))) throw 'Injection'; qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`); } })); diff --git a/packages/backend/src/server/api/integration/DiscordServerService.ts b/packages/backend/src/server/api/integration/DiscordServerService.ts deleted file mode 100644 index cbced901e..000000000 --- a/packages/backend/src/server/api/integration/DiscordServerService.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import Redis from 'ioredis'; -import { OAuth2 } from 'oauth'; -import { v4 as uuid } from 'uuid'; -import { IsNull } from 'typeorm'; -import type { Config } from '@/config.js'; -import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; -import { DI } from '@/di-symbols.js'; -import { HttpRequestService } from '@/core/HttpRequestService.js'; -import type { ILocalUser } from '@/models/entities/User.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { MetaService } from '@/core/MetaService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; -import { bindThis } from '@/decorators.js'; -import { SigninService } from '../SigninService.js'; -import type { FastifyInstance, FastifyRequest, FastifyPluginOptions } from 'fastify'; - -@Injectable() -export class DiscordServerService { - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.redis) - private redisClient: Redis.Redis, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - - private userEntityService: UserEntityService, - private httpRequestService: HttpRequestService, - private globalEventService: GlobalEventService, - private metaService: MetaService, - private signinService: SigninService, - ) { - //this.create = this.create.bind(this); - } - - @bindThis - public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { - fastify.get('/disconnect/discord', async (request, reply) => { - if (!this.compareOrigin(request)) { - throw new FastifyReplyError(400, 'invalid origin'); - } - - const userToken = this.getUserToken(request); - if (!userToken) { - throw new FastifyReplyError(400, 'signin required'); - } - - const user = await this.usersRepository.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - - delete profile.integrations.discord; - - await this.userProfilesRepository.update(user.id, { - integrations: profile.integrations, - }); - - // Publish i updated event - this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, { - detail: true, - includeSecrets: true, - })); - - return 'Discordの連携を解除しました :v:'; - }); - - const getOAuth2 = async () => { - const meta = await this.metaService.fetch(true); - - if (meta.enableDiscordIntegration) { - return new OAuth2( - meta.discordClientId!, - meta.discordClientSecret!, - 'https://discord.com/', - 'api/oauth2/authorize', - 'api/oauth2/token'); - } else { - return null; - } - }; - - fastify.get('/connect/discord', async (request, reply) => { - if (!this.compareOrigin(request)) { - throw new FastifyReplyError(400, 'invalid origin'); - } - - const userToken = this.getUserToken(request); - if (!userToken) { - throw new FastifyReplyError(400, 'signin required'); - } - - const params = { - redirect_uri: `${this.config.url}/api/dc/cb`, - scope: ['identify'], - state: uuid(), - response_type: 'code', - }; - - this.redisClient.set(userToken, JSON.stringify(params)); - - const oauth2 = await getOAuth2(); - reply.redirect(oauth2!.getAuthorizeUrl(params)); - }); - - fastify.get('/signin/discord', async (request, reply) => { - const sessid = uuid(); - - const params = { - redirect_uri: `${this.config.url}/api/dc/cb`, - scope: ['identify'], - state: uuid(), - response_type: 'code', - }; - - reply.setCookie('signin_with_discord_sid', sessid, { - path: '/', - secure: this.config.url.startsWith('https'), - httpOnly: true, - }); - - this.redisClient.set(sessid, JSON.stringify(params)); - - const oauth2 = await getOAuth2(); - reply.redirect(oauth2!.getAuthorizeUrl(params)); - }); - - fastify.get<{ Querystring: { code: string; state: string; } }>('/dc/cb', async (request, reply) => { - const userToken = this.getUserToken(request); - - const oauth2 = await getOAuth2(); - - if (!userToken) { - const sessid = request.cookies['signin_with_discord_sid']; - - if (!sessid) { - throw new FastifyReplyError(400, 'invalid session'); - } - - const code = request.query.code; - - if (!code || typeof code !== 'string') { - throw new FastifyReplyError(400, 'invalid session'); - } - - const { redirect_uri, state } = await new Promise((res, rej) => { - this.redisClient.get(sessid, async (_, state) => { - if (state == null) throw new Error('empty state'); - res(JSON.parse(state)); - }); - }); - - if (request.query.state !== state) { - throw new FastifyReplyError(400, 'invalid session'); - } - - const { accessToken, refreshToken, expiresDate } = await new Promise((res, rej) => - oauth2!.getOAuthAccessToken(code, { - grant_type: 'authorization_code', - redirect_uri, - }, (err, accessToken, refreshToken, result) => { - if (err) { - rej(err); - } else if (result.error) { - rej(result.error); - } else { - res({ - accessToken, - refreshToken, - expiresDate: Date.now() + Number(result.expires_in) * 1000, - }); - } - })); - - const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', { - 'Authorization': `Bearer ${accessToken}`, - })) as Record; - - if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') { - throw new FastifyReplyError(400, 'invalid session'); - } - - const profile = await this.userProfilesRepository.createQueryBuilder() - .where('"integrations"->\'discord\'->>\'id\' = :id', { id: id }) - .andWhere('"userHost" IS NULL') - .getOne(); - - if (profile == null) { - throw new FastifyReplyError(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`); - } - - await this.userProfilesRepository.update(profile.userId, { - integrations: { - ...profile.integrations, - discord: { - id: id, - accessToken: accessToken, - refreshToken: refreshToken, - expiresDate: expiresDate, - username: username, - discriminator: discriminator, - }, - }, - }); - - return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: profile.userId }) as ILocalUser, true); - } else { - const code = request.query.code; - - if (!code || typeof code !== 'string') { - throw new FastifyReplyError(400, 'invalid session'); - } - - const { redirect_uri, state } = await new Promise((res, rej) => { - this.redisClient.get(userToken, async (_, state) => { - if (state == null) throw new Error('empty state'); - res(JSON.parse(state)); - }); - }); - - if (request.query.state !== state) { - throw new FastifyReplyError(400, 'invalid session'); - } - - const { accessToken, refreshToken, expiresDate } = await new Promise((res, rej) => - oauth2!.getOAuthAccessToken(code, { - grant_type: 'authorization_code', - redirect_uri, - }, (err, accessToken, refreshToken, result) => { - if (err) { - rej(err); - } else if (result.error) { - rej(result.error); - } else { - res({ - accessToken, - refreshToken, - expiresDate: Date.now() + Number(result.expires_in) * 1000, - }); - } - })); - - const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', { - 'Authorization': `Bearer ${accessToken}`, - })) as Record; - if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') { - throw new FastifyReplyError(400, 'invalid session'); - } - - const user = await this.usersRepository.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - - await this.userProfilesRepository.update(user.id, { - integrations: { - ...profile.integrations, - discord: { - accessToken: accessToken, - refreshToken: refreshToken, - expiresDate: expiresDate, - id: id, - username: username, - discriminator: discriminator, - }, - }, - }); - - // Publish i updated event - this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, { - detail: true, - includeSecrets: true, - })); - - return `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`; - } - }); - - done(); - } - - @bindThis - private getUserToken(request: FastifyRequest): string | null { - return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1]; - } - - @bindThis - private compareOrigin(request: FastifyRequest): boolean { - function normalizeUrl(url?: string): string { - return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; - } - - const referer = request.headers['referer']; - - return (normalizeUrl(referer) === normalizeUrl(this.config.url)); - } -} diff --git a/packages/backend/src/server/api/integration/GithubServerService.ts b/packages/backend/src/server/api/integration/GithubServerService.ts deleted file mode 100644 index 76089c935..000000000 --- a/packages/backend/src/server/api/integration/GithubServerService.ts +++ /dev/null @@ -1,280 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import Redis from 'ioredis'; -import { OAuth2 } from 'oauth'; -import { v4 as uuid } from 'uuid'; -import { IsNull } from 'typeorm'; -import type { Config } from '@/config.js'; -import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; -import { DI } from '@/di-symbols.js'; -import { HttpRequestService } from '@/core/HttpRequestService.js'; -import type { ILocalUser } from '@/models/entities/User.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { MetaService } from '@/core/MetaService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; -import { bindThis } from '@/decorators.js'; -import { SigninService } from '../SigninService.js'; -import type { FastifyInstance, FastifyRequest, FastifyPluginOptions } from 'fastify'; - -@Injectable() -export class GithubServerService { - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.redis) - private redisClient: Redis.Redis, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - - private userEntityService: UserEntityService, - private httpRequestService: HttpRequestService, - private globalEventService: GlobalEventService, - private metaService: MetaService, - private signinService: SigninService, - ) { - //this.create = this.create.bind(this); - } - - @bindThis - public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { - fastify.get('/disconnect/github', async (request, reply) => { - if (!this.compareOrigin(request)) { - throw new FastifyReplyError(400, 'invalid origin'); - } - - const userToken = this.getUserToken(request); - if (!userToken) { - throw new FastifyReplyError(400, 'signin required'); - } - - const user = await this.usersRepository.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - - delete profile.integrations.github; - - await this.userProfilesRepository.update(user.id, { - integrations: profile.integrations, - }); - - // Publish i updated event - this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, { - detail: true, - includeSecrets: true, - })); - - return 'GitHubの連携を解除しました :v:'; - }); - - const getOath2 = async () => { - const meta = await this.metaService.fetch(true); - - if (meta.enableGithubIntegration && meta.githubClientId && meta.githubClientSecret) { - return new OAuth2( - meta.githubClientId, - meta.githubClientSecret, - 'https://github.com/', - 'login/oauth/authorize', - 'login/oauth/access_token'); - } else { - return null; - } - }; - - fastify.get('/connect/github', async (request, reply) => { - if (!this.compareOrigin(request)) { - throw new FastifyReplyError(400, 'invalid origin'); - } - - const userToken = this.getUserToken(request); - if (!userToken) { - throw new FastifyReplyError(400, 'signin required'); - } - - const params = { - redirect_uri: `${this.config.url}/api/gh/cb`, - scope: ['read:user'], - state: uuid(), - }; - - this.redisClient.set(userToken, JSON.stringify(params)); - - const oauth2 = await getOath2(); - reply.redirect(oauth2!.getAuthorizeUrl(params)); - }); - - fastify.get('/signin/github', async (request, reply) => { - const sessid = uuid(); - - const params = { - redirect_uri: `${this.config.url}/api/gh/cb`, - scope: ['read:user'], - state: uuid(), - }; - - reply.setCookie('signin_with_github_sid', sessid, { - path: '/', - secure: this.config.url.startsWith('https'), - httpOnly: true, - }); - - this.redisClient.set(sessid, JSON.stringify(params)); - - const oauth2 = await getOath2(); - reply.redirect(oauth2!.getAuthorizeUrl(params)); - }); - - fastify.get<{ Querystring: { code: string; state: string; } }>('/gh/cb', async (request, reply) => { - const userToken = this.getUserToken(request); - - const oauth2 = await getOath2(); - - if (!userToken) { - const sessid = request.cookies['signin_with_github_sid']; - - if (!sessid) { - throw new FastifyReplyError(400, 'invalid session'); - } - - const code = request.query.code; - - if (!code || typeof code !== 'string') { - throw new FastifyReplyError(400, 'invalid session'); - } - - const { redirect_uri, state } = await new Promise((res, rej) => { - this.redisClient.get(sessid, async (_, state) => { - if (state == null) throw new Error('empty state'); - res(JSON.parse(state)); - }); - }); - - if (request.query.state !== state) { - throw new FastifyReplyError(400, 'invalid session'); - } - - const { accessToken } = await new Promise<{ accessToken: string }>((res, rej) => - oauth2!.getOAuthAccessToken(code, { - redirect_uri, - }, (err, accessToken, refresh, result) => { - if (err) { - rej(err); - } else if (result.error) { - rej(result.error); - } else { - res({ accessToken }); - } - })); - - const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', { - 'Authorization': `bearer ${accessToken}`, - })) as Record; - if (typeof login !== 'string' || typeof id !== 'string') { - throw new FastifyReplyError(400, 'invalid session'); - } - - const link = await this.userProfilesRepository.createQueryBuilder() - .where('"integrations"->\'github\'->>\'id\' = :id', { id: id }) - .andWhere('"userHost" IS NULL') - .getOne(); - - if (link == null) { - throw new FastifyReplyError(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`); - } - - return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true); - } else { - const code = request.query.code; - - if (!code || typeof code !== 'string') { - throw new FastifyReplyError(400, 'invalid session'); - } - - const { redirect_uri, state } = await new Promise((res, rej) => { - this.redisClient.get(userToken, async (_, state) => { - if (state == null) throw new Error('empty state'); - res(JSON.parse(state)); - }); - }); - - if (request.query.state !== state) { - throw new FastifyReplyError(400, 'invalid session'); - } - - const { accessToken } = await new Promise<{ accessToken: string }>((res, rej) => - oauth2!.getOAuthAccessToken( - code, - { redirect_uri }, - (err, accessToken, refresh, result) => { - if (err) { - rej(err); - } else if (result.error) { - rej(result.error); - } else { - res({ accessToken }); - } - })); - - const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', { - 'Authorization': `bearer ${accessToken}`, - })) as Record; - - if (typeof login !== 'string' || typeof id !== 'number') { - throw new FastifyReplyError(400, 'invalid session'); - } - - const user = await this.usersRepository.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - - await this.userProfilesRepository.update(user.id, { - integrations: { - ...profile.integrations, - github: { - accessToken: accessToken, - id: id, - login: login, - }, - }, - }); - - // Publish i updated event - this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, { - detail: true, - includeSecrets: true, - })); - - return `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`; - } - }); - - done(); - } - - @bindThis - private getUserToken(request: FastifyRequest): string | null { - return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1]; - } - - @bindThis - private compareOrigin(request: FastifyRequest): boolean { - function normalizeUrl(url?: string): string { - return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; - } - - const referer = request.headers['referer']; - - return (normalizeUrl(referer) === normalizeUrl(this.config.url)); - } -} diff --git a/packages/backend/src/server/api/integration/TwitterServerService.ts b/packages/backend/src/server/api/integration/TwitterServerService.ts deleted file mode 100644 index f31a788d3..000000000 --- a/packages/backend/src/server/api/integration/TwitterServerService.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import Redis from 'ioredis'; -import { v4 as uuid } from 'uuid'; -import { IsNull } from 'typeorm'; -import * as autwh from 'autwh'; -import type { Config } from '@/config.js'; -import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; -import { DI } from '@/di-symbols.js'; -import { HttpRequestService } from '@/core/HttpRequestService.js'; -import type { ILocalUser } from '@/models/entities/User.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { MetaService } from '@/core/MetaService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; -import { bindThis } from '@/decorators.js'; -import { SigninService } from '../SigninService.js'; -import type { FastifyInstance, FastifyRequest, FastifyPluginOptions } from 'fastify'; - -@Injectable() -export class TwitterServerService { - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.redis) - private redisClient: Redis.Redis, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - - private userEntityService: UserEntityService, - private httpRequestService: HttpRequestService, - private globalEventService: GlobalEventService, - private metaService: MetaService, - private signinService: SigninService, - ) { - //this.create = this.create.bind(this); - } - - @bindThis - public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { - fastify.get('/disconnect/twitter', async (request, reply) => { - if (!this.compareOrigin(request)) { - throw new FastifyReplyError(400, 'invalid origin'); - } - - const userToken = this.getUserToken(request); - if (userToken == null) { - throw new FastifyReplyError(400, 'signin required'); - } - - const user = await this.usersRepository.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - - delete profile.integrations.twitter; - - await this.userProfilesRepository.update(user.id, { - integrations: profile.integrations, - }); - - // Publish i updated event - this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, { - detail: true, - includeSecrets: true, - })); - - return 'Twitterの連携を解除しました :v:'; - }); - - const getTwAuth = async () => { - const meta = await this.metaService.fetch(true); - - if (meta.enableTwitterIntegration && meta.twitterConsumerKey && meta.twitterConsumerSecret) { - return autwh({ - consumerKey: meta.twitterConsumerKey, - consumerSecret: meta.twitterConsumerSecret, - callbackUrl: `${this.config.url}/api/tw/cb`, - }); - } else { - return null; - } - }; - - fastify.get('/connect/twitter', async (request, reply) => { - if (!this.compareOrigin(request)) { - throw new FastifyReplyError(400, 'invalid origin'); - } - - const userToken = this.getUserToken(request); - if (userToken == null) { - throw new FastifyReplyError(400, 'signin required'); - } - - const twAuth = await getTwAuth(); - const twCtx = await twAuth!.begin(); - this.redisClient.set(userToken, JSON.stringify(twCtx)); - reply.redirect(twCtx.url); - }); - - fastify.get('/signin/twitter', async (request, reply) => { - const twAuth = await getTwAuth(); - const twCtx = await twAuth!.begin(); - - const sessid = uuid(); - - this.redisClient.set(sessid, JSON.stringify(twCtx)); - - reply.setCookie('signin_with_twitter_sid', sessid, { - path: '/', - secure: this.config.url.startsWith('https'), - httpOnly: true, - }); - - reply.redirect(twCtx.url); - }); - - fastify.get('/tw/cb', async (request, reply) => { - const userToken = this.getUserToken(request); - - const twAuth = await getTwAuth(); - - if (userToken == null) { - const sessid = request.cookies['signin_with_twitter_sid']; - - if (sessid == null) { - throw new FastifyReplyError(400, 'invalid session'); - } - - const get = new Promise((res, rej) => { - this.redisClient.get(sessid, async (_, twCtx) => { - res(twCtx); - }); - }); - - const twCtx = await get; - - const verifier = request.query.oauth_verifier; - if (!verifier || typeof verifier !== 'string') { - throw new FastifyReplyError(400, 'invalid session'); - } - - const result = await twAuth!.done(JSON.parse(twCtx), verifier); - - const link = await this.userProfilesRepository.createQueryBuilder() - .where('"integrations"->\'twitter\'->>\'userId\' = :id', { id: result.userId }) - .andWhere('"userHost" IS NULL') - .getOne(); - - if (link == null) { - throw new FastifyReplyError(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); - } - - return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true); - } else { - const verifier = request.query.oauth_verifier; - - if (!verifier || typeof verifier !== 'string') { - throw new FastifyReplyError(400, 'invalid session'); - } - - const get = new Promise((res, rej) => { - this.redisClient.get(userToken, async (_, twCtx) => { - res(twCtx); - }); - }); - - const twCtx = await get; - - const result = await twAuth!.done(JSON.parse(twCtx), verifier); - - const user = await this.usersRepository.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - - await this.userProfilesRepository.update(user.id, { - integrations: { - ...profile.integrations, - twitter: { - accessToken: result.accessToken, - accessTokenSecret: result.accessTokenSecret, - userId: result.userId, - screenName: result.screenName, - }, - }, - }); - - // Publish i updated event - this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, { - detail: true, - includeSecrets: true, - })); - - return `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`; - } - }); - - done(); - } - - @bindThis - private getUserToken(request: FastifyRequest): string | null { - return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1]; - } - - @bindThis - private compareOrigin(request: FastifyRequest): boolean { - function normalizeUrl(url?: string): string { - return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; - } - - const referer = request.headers['referer']; - - return (normalizeUrl(referer) === normalizeUrl(this.config.url)); - } -} diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index 36bfa7836..8bb4147b4 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -25,6 +25,8 @@ export interface InternalStreamTypes { remoteUserUpdated: { id: User['id']; }; follow: { followerId: User['id']; followeeId: User['id']; }; unfollow: { followerId: User['id']; followeeId: User['id']; }; + blockingCreated: { blockerId: User['id']; blockeeId: User['id']; }; + blockingDeleted: { blockerId: User['id']; blockeeId: User['id']; }; policiesUpdated: Role['policies']; roleCreated: Role; roleDeleted: Role; diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 2a764a25b..c69ee33ea 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -155,7 +155,7 @@ export class ClientServerService { }); serverAdapter.setBasePath(bullBoardPath); - fastify.register(serverAdapter.registerPlugin(), { prefix: bullBoardPath }); + (fastify.register as any)(serverAdapter.registerPlugin(), { prefix: bullBoardPath }); //#endregion fastify.register(fastifyView, { @@ -337,7 +337,7 @@ export class ClientServerService { const renderBase = async (reply: FastifyReply) => { const meta = await this.metaService.fetch(); - reply.header('Cache-Control', 'public, max-age=15'); + reply.header('Cache-Control', 'public, max-age=30'); return await reply.view('base', { img: meta.bannerUrl, title: meta.name ?? 'Misskey', @@ -372,6 +372,7 @@ export class ClientServerService { return feed.atom1(); } else { reply.code(404); + return; } }); @@ -384,6 +385,7 @@ export class ClientServerService { return feed.rss2(); } else { reply.code(404); + return; } }); @@ -396,6 +398,7 @@ export class ClientServerService { return feed.json1(); } else { reply.code(404); + return; } }); diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 802b404ce..57461b7a3 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -33,7 +33,7 @@ export class UrlPreviewService { private wrap(url?: string): string | null { return url != null ? url.match(/^https?:\/\//) - ? `${this.config.url}/proxy/preview.webp?${query({ + ? `${this.config.mediaProxy}/preview.webp?${query({ url, preview: '1', })}` @@ -73,6 +73,14 @@ export class UrlPreviewService { }); this.logger.succ(`Got preview of ${url}: ${summary.title}`); + + if (summary.url && !(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) { + throw new Error('unsupported schema included'); + } + + if (summary.player?.url && !(summary.player.url.startsWith('http://') || summary.player.url.startsWith('https://'))) { + throw new Error('unsupported schema included'); + } summary.icon = this.wrap(summary.icon); summary.thumbnail = this.wrap(summary.thumbnail); diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index d05901bae..8d6897c46 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -35,7 +35,8 @@ html link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') - link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css') + //- https://github.com/misskey-dev/misskey/issues/9842 + link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.2.0') link(rel='modulepreload' href=`/vite/${clientEntry.file}`) if !config.clientManifestExists diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index c8fd41e1d..50988939a 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -11,7 +11,7 @@ import FormData from 'form-data'; import { DataSource } from 'typeorm'; import got, { RequestError } from 'got'; import loadConfig from '../src/config/load.js'; -import { entities } from '../src/postgre.js'; +import { entities } from '@/postgres.js'; import type * as misskey from 'misskey-js'; const _filename = fileURLToPath(import.meta.url); diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 4d6384729..49668ea46 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -12,18 +12,18 @@ "@rollup/plugin-json": "6.0.0", "@rollup/pluginutils": "5.0.2", "@syuilo/aiscript": "0.12.4", - "@tabler/icons-webfont": "^2.1.2", + "@tabler/icons-webfont": "2.2.0", "@vitejs/plugin-vue": "4.0.0", - "@vue/compiler-sfc": "3.2.45", + "@vue/compiler-sfc": "3.2.47", "autobind-decorator": "2.4.0", "autosize": "5.0.2", "blurhash": "2.0.4", "broadcast-channel": "4.20.2", "browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3", - "canvas-confetti": "^1.6.0", + "canvas-confetti": "1.6.0", "chart.js": "4.2.0", "chartjs-adapter-date-fns": "3.0.0", - "chartjs-chart-matrix": "^1.3.0", + "chartjs-chart-matrix": "2.0.1", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.0.0", "compare-versions": "5.0.1", @@ -31,7 +31,7 @@ "date-fns": "2.29.3", "escape-regexp": "0.0.1", "eventemitter3": "5.0.0", - "gsap": "^3.11.4", + "gsap": "3.11.4", "idb-keyval": "6.2.0", "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", @@ -39,16 +39,16 @@ "matter-js": "0.18.0", "mfm-js": "git+https://github.com/sim1222/mfm.js.git", "misetehoshii": "https://github.com/melt-adzuki/misetehoshii", - "misskey-js": "0.0.14", - "photoswipe": "5.3.4", + "misskey-js": "0.0.15", + "photoswipe": "5.3.5", "prismjs": "1.29.0", "punycode": "2.3.0", "querystring": "0.2.1", "rndstr": "1.0.0", - "rollup": "3.11.0", + "rollup": "3.14.0", "s-age": "1.1.2", - "sanitize-html": "^2.8.1", - "sass": "1.57.1", + "sanitize-html": "2.9.0", + "sass": "1.58.0", "seedrandom": "3.0.5", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", @@ -56,15 +56,16 @@ "textarea-caret": "3.1.0", "three": "0.149.0", "throttle-debounce": "5.0.0", - "tinycolor2": "1.5.2", + "tinycolor2": "1.6.0", "tsc-alias": "1.8.2", "tsconfig-paths": "4.1.2", "twemoji-parser": "14.0.0", - "typescript": "4.9.4", + "typescript": "4.9.5", "uuid": "9.0.0", "vanilla-tilt": "1.8.0", - "vite": "4.0.4", - "vue": "3.2.45", + "vue-plyr": "7.0.0", + "vite": "4.1.1", + "vue": "3.2.47", "vue-prism-editor": "2.0.0-alpha.2", "vuedraggable": "next" }, @@ -74,25 +75,25 @@ "@types/gulp": "4.0.10", "@types/gulp-rename": "2.0.1", "@types/matter-js": "0.18.2", - "@types/node": "^18.11.18", + "@types/node": "18.13.0", "@types/punycode": "2.1.0", - "@types/sanitize-html": "^2.8.0", + "@types/sanitize-html": "2.8.0", "@types/seedrandom": "3.0.4", "@types/throttle-debounce": "5.0.0", "@types/tinycolor2": "1.4.3", "@types/uuid": "9.0.0", "@types/websocket": "1.0.5", "@types/ws": "8.5.4", - "@typescript-eslint/eslint-plugin": "5.49.0", - "@typescript-eslint/parser": "5.49.0", - "@vue/runtime-core": "3.2.45", + "@typescript-eslint/eslint-plugin": "5.51.0", + "@typescript-eslint/parser": "5.51.0", + "@vue/runtime-core": "3.2.47", "cross-env": "7.0.3", - "cypress": "12.4.0", - "eslint": "8.32.0", + "cypress": "12.5.1", + "eslint": "8.33.0", "eslint-plugin-import": "2.27.5", "eslint-plugin-vue": "9.9.0", "start-server-and-test": "1.15.3", - "vue-eslint-parser": "^9.1.0", - "vue-tsc": "^1.0.24" + "vue-eslint-parser": "9.1.0", + "vue-tsc": "1.0.24" } } diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 31c125d3a..610212b6e 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -61,8 +61,6 @@ export async function signout() { } catch (err) {} //#endregion - document.cookie = 'igi=; path=/'; - if (accounts.length > 0) login(accounts[0].token); else unisonReload('/'); } diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue index 19d04721d..d30037dcf 100644 --- a/packages/frontend/src/components/MkAchievements.vue +++ b/packages/frontend/src/components/MkAchievements.vue @@ -107,6 +107,7 @@ onMounted(() => { } .iconFrame { + position: relative; width: 58px; height: 58px; padding: 6px; diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 2ba899661..60033b0a0 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -17,7 +17,8 @@
  1. - + + diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue index b07402882..b656307d9 100644 --- a/packages/frontend/src/components/MkCode.core.vue +++ b/packages/frontend/src/components/MkCode.core.vue @@ -1,6 +1,6 @@ diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 84adb790f..7e4d2016b 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -18,7 +18,7 @@
    - +
    diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue index 651b20cef..e0885f555 100644 --- a/packages/frontend/src/components/MkCwButton.vue +++ b/packages/frontend/src/components/MkCwButton.vue @@ -1,5 +1,5 @@
    - {{ (showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt }} - {{ i18n.ts.cancel }} + {{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }} + {{ cancelText ?? i18n.ts.cancel }}
    {{ action.text }} @@ -82,6 +82,8 @@ const props = withDefaults(defineProps<{ showOkButton?: boolean; showCancelButton?: boolean; cancelableByBgClick?: boolean; + okText?: string; + cancelText?: string; }>(), { type: 'info', showOkButton: true, diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 2b38056bc..521dbe3b4 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -1,6 +1,6 @@ @@ -44,6 +45,9 @@ import MkButton from '@/components/MkButton.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { useRouter } from '@/router'; + +const router = useRouter(); const props = defineProps<{ webhookId: string; @@ -86,6 +90,19 @@ async function save(): Promise { }); } +async function del(): Promise { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('deleteAreYouSure', { x: webhook.name }), + }); + if (canceled) return; + + await os.apiWithDialog('i/webhooks/delete', { + webhookId: props.webhookId, + }); + + router.push('/settings/webhook'); +} const headerActions = $computed(() => []); const headerTabs = $computed(() => []); diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue index b979db15f..d058bc4d6 100644 --- a/packages/frontend/src/pages/share.vue +++ b/packages/frontend/src/pages/share.vue @@ -29,7 +29,7 @@ import { noteVisibilities } from 'misskey-js'; import * as Acct from 'misskey-js/built/acct'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; -import XPostForm from '@/components/MkPostForm.vue'; +import MkPostForm from '@/components/MkPostForm.vue'; import * as os from '@/os'; import { mainRouter } from '@/router'; import { definePageMetadata } from '@/scripts/page-metadata'; @@ -69,14 +69,14 @@ async function init() { ...(visibleAccts ? visibleAccts.split(',').map(Acct.parse) : []), ] // TypeScriptの指示通りに変換する - .map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q) - .map(q => os.api('users/show', q) - .then(user => { - visibleUsers.push(user); - }, () => { - console.error(`Invalid user query: ${JSON.stringify(q)}`); - }), - ), + .map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q) + .map(q => os.api('users/show', q) + .then(user => { + visibleUsers.push(user); + }, () => { + console.error(`Invalid user query: ${JSON.stringify(q)}`); + }), + ), ); } @@ -120,13 +120,13 @@ async function init() { if (fileIds) { await Promise.all( fileIds.split(',') - .map(fileId => os.api('drive/files/show', { fileId }) - .then(file => { - files.push(file); - }, () => { - console.error(`Failed to fetch a file ${fileId}`); - }), - ), + .map(fileId => os.api('drive/files/show', { fileId }) + .then(file => { + files.push(file); + }, () => { + console.error(`Failed to fetch a file ${fileId}`); + }), + ), ); } //#endregion diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 56ff6755e..111181a3f 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -1,10 +1,10 @@