Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
sim1222 2023-02-10 18:06:45 +09:00
commit d075f1ba6e
No known key found for this signature in database
GPG Key ID: 04EF48D01BEB0298
201 changed files with 3129 additions and 3964 deletions

View File

@ -114,11 +114,6 @@ id: 'aid'
# IP address family used for outgoing request (ipv4, ipv6 or dual) # IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4 #outgoingAddressFamily: ipv4
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS # Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128 #proxy: http://127.0.0.1:3128

View File

@ -114,11 +114,6 @@ id: 'aid'
# IP address family used for outgoing request (ipv4, ipv6 or dual) # IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4 #outgoingAddressFamily: ipv4
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS # Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128 #proxy: http://127.0.0.1:3128
@ -135,6 +130,7 @@ proxyBypassHosts:
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 #proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
# Media Proxy # Media Proxy
# Reference Implementation: https://github.com/misskey-dev/media-proxy
#mediaProxy: https://example.com/proxy #mediaProxy: https://example.com/proxy
# Proxy remote files (default: false) # Proxy remote files (default: false)

View File

@ -16,9 +16,15 @@ files/
misskey-assets/ misskey-assets/
fluent-emojis/ fluent-emojis/
.pnp.* .pnp.*
# .yarn関連
.yarn/* .yarn/*
!.yarn/patches !.yarn/patches
!.yarn/plugins !.yarn/plugins
!.yarn/releases !.yarn/releases
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
.idea/
packages/*/.vscode/
packages/backend/test/docker-compose.yml

3
.dockleignore Normal file
View File

@ -0,0 +1,3 @@
DKL-DI-0005
DKL-DI-0006
DKL-LI-0003

View File

@ -14,6 +14,8 @@ jobs:
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v3.3.0 uses: actions/checkout@v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.3.0
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4

30
.github/workflows/dockle.yml vendored Normal file
View File

@ -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}"

View File

@ -8,6 +8,104 @@
You should also include the user name that made the change. 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) ## 13.2.6 (2023/02/01)
### Changes ### Changes

View File

@ -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. - 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. - 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. - 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. - If this PR includes UI changes, please attach a screenshot in the text.
Thanks for your cooperation 🤗 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 During development, it is useful to use the
``` ```
yarn dev pnpm dev
``` ```
command. command.
@ -112,7 +112,7 @@ command.
- Service Worker is watched by esbuild. - Service Worker is watched by esbuild.
## Testing ## Testing
- Test codes are located in [`/test`](/test). - Test codes are located in [`/packages/backend/test`](/packages/backend/test).
### Run test ### Run test
Create a config file. Create a config file.
@ -121,18 +121,18 @@ cp .github/misskey/test.yml .config/
``` ```
Prepare DB/Redis for testing. 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`. Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
Run all test. Run all test.
``` ```
yarn test pnpm test
``` ```
#### Run specify test #### Run specify test
``` ```
yarn jest -- foo.ts pnpm jest -- foo.ts
``` ```
### e2e tests ### e2e tests
@ -177,9 +177,9 @@ vue-routerとの最大の違いは、niraxは複数のルーターが存在す
これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。 これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
## Notes ## 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を使用する ### INSERTするときにはsaveではなくinsertを使用する
#6441 #6441
@ -265,7 +265,7 @@ MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`
### Migration作成方法 ### Migration作成方法
packages/backendで: packages/backendで:
```sh ```sh
yarn dlx typeorm migration:generate -d ormconfig.js -o <migration name> pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
``` ```
- 生成後、ファイルをmigration下に移してください - 生成後、ファイルをmigration下に移してください

View File

@ -29,6 +29,7 @@ ARG NODE_ENV=production
RUN git submodule update --init RUN git submodule update --init
RUN pnpm build RUN pnpm build
RUN rm -rf .git/
FROM node:${NODE_VERSION}-slim AS runner 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 \ ; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& apt-get update \ && apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
ffmpeg tini \ ffmpeg tini curl \
&& corepack enable \ && corepack enable \
&& groupadd -g "${GID}" misskey \ && 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 USER misskey
WORKDIR /misskey WORKDIR /misskey
@ -58,5 +61,6 @@ COPY --chown=misskey:misskey --from=builder /misskey/fluent-emojis /misskey/flue
COPY --chown=misskey:misskey . ./ COPY --chown=misskey:misskey . ./
ENV NODE_ENV=production ENV NODE_ENV=production
HEALTHCHECK --interval=5s --retries=20 CMD ["/bin/bash", "/misskey/healthcheck.sh"]
ENTRYPOINT ["/usr/bin/tini", "--"] ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["pnpm", "run", "migrateandstart"] CMD ["pnpm", "run", "migrateandstart"]

View File

@ -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. 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) - 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 - Improve CI
- Fix tests - 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 - Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986
- Add more tests - 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 - https://github.com/misskey-dev/misskey/pull/9085
- Measure coverage - ~~Measure coverage~~ → Done ✔️
- https://github.com/misskey-dev/misskey/pull/9081 - https://github.com/misskey-dev/misskey/pull/9081
- Improve documentation - Improve documentation
- Refactoring - Refactoring

View File

@ -133,11 +133,6 @@ id: "aid"
# IP address family used for outgoing request (ipv4, ipv6 or dual) # IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4 #outgoingAddressFamily: ipv4
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS # Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128 #proxy: http://127.0.0.1:3128

4
healthcheck.sh Normal file
View File

@ -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}"

View File

@ -1345,5 +1345,6 @@ _deck:
tl: "الخيط الزمني" tl: "الخيط الزمني"
antenna: "الهوائيات" antenna: "الهوائيات"
list: "القوائم" list: "القوائم"
channel: "القنوات"
mentions: "الإشارات" mentions: "الإشارات"
direct: "مباشرة" direct: "مباشرة"

View File

@ -1441,5 +1441,6 @@ _deck:
tl: "টাইমলাইন" tl: "টাইমলাইন"
antenna: "অ্যান্টেনা" antenna: "অ্যান্টেনা"
list: "লিস্ট" list: "লিস্ট"
channel: "চ্যানেলগুলি"
mentions: "উল্লেখসমূহ" mentions: "উল্লেখসমূহ"
direct: "ডাইরেক্ট নোটগুলি" direct: "ডাইরেক্ট নোটগুলি"

View File

@ -804,4 +804,5 @@ _deck:
tl: "Časová osa" tl: "Časová osa"
antenna: "Antény" antenna: "Antény"
list: "Seznamy" list: "Seznamy"
channel: "Kanály"
mentions: "Zmínění" mentions: "Zmínění"

View File

@ -68,7 +68,7 @@ export: "Export"
files: "Dateien" files: "Dateien"
download: "Herunterladen" download: "Herunterladen"
driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Notizen mit dieser Datei werden ebenso verschwinden." 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." 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." importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen."
lists: "Listen" lists: "Listen"
@ -94,7 +94,7 @@ defaultNoteVisibility: "Standardsichtbarkeit"
follow: "Folgen" follow: "Folgen"
followRequest: "Follow-Anfrage senden" followRequest: "Follow-Anfrage senden"
followRequests: "Follow-Anfragen" followRequests: "Follow-Anfragen"
unfollow: "Nicht mehr folgen" unfollow: "Entfolgen"
followRequestPending: "Follow-Anfrage ausstehend" followRequestPending: "Follow-Anfrage ausstehend"
enterEmoji: "Gib ein Emoji ein" enterEmoji: "Gib ein Emoji ein"
renote: "Renote" renote: "Renote"
@ -129,6 +129,7 @@ unblockConfirm: "Möchtest du diese Blockierung wirklich aufheben?"
suspendConfirm: "Möchtest du diesen Benutzer wirklich sperren?" suspendConfirm: "Möchtest du diesen Benutzer wirklich sperren?"
unsuspendConfirm: "Möchtest du diesen Benutzer wirklich entsperren?" unsuspendConfirm: "Möchtest du diesen Benutzer wirklich entsperren?"
selectList: "Liste auswählen" selectList: "Liste auswählen"
selectChannel: "Kanal auswählen"
selectAntenna: "Antenne auswählen" selectAntenna: "Antenne auswählen"
selectWidget: "Widget auswählen" selectWidget: "Widget auswählen"
editWidgets: "Widgets bearbeiten" editWidgets: "Widgets bearbeiten"
@ -1195,6 +1196,9 @@ _role:
baseRole: "Rollenvorlage" baseRole: "Rollenvorlage"
useBaseValue: "Wert der Rollenvorlage verwenden" useBaseValue: "Wert der Rollenvorlage verwenden"
chooseRoleToAssign: "Zuzuweisende Rolle auswählen" 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" 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." 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" priority: "Priorität"
@ -1866,5 +1870,6 @@ _deck:
tl: "Chronik" tl: "Chronik"
antenna: "Antennen" antenna: "Antennen"
list: "Listen" list: "Listen"
channel: "Kanal"
mentions: "Erwähnungen" mentions: "Erwähnungen"
direct: "Direktnachrichten" direct: "Direktnachrichten"

View File

@ -68,7 +68,7 @@ export: "Export"
files: "Files" files: "Files"
download: "Download" download: "Download"
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Notes with this file attached will also be deleted." 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." 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." importRequested: "You've requested an import. This may take a while."
lists: "Lists" 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?" suspendConfirm: "Are you sure that you want to suspend this account?"
unsuspendConfirm: "Are you sure that you want to unsuspend this account?" unsuspendConfirm: "Are you sure that you want to unsuspend this account?"
selectList: "Select a list" selectList: "Select a list"
selectChannel: "Select a channel"
selectAntenna: "Select an antenna" selectAntenna: "Select an antenna"
selectWidget: "Select a widget" selectWidget: "Select a widget"
editWidgets: "Edit widgets" editWidgets: "Edit widgets"
@ -1195,6 +1196,9 @@ _role:
baseRole: "Role template" baseRole: "Role template"
useBaseValue: "Use role template value" useBaseValue: "Use role template value"
chooseRoleToAssign: "Select the role to assign" 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" 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." descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users."
priority: "Priority" priority: "Priority"
@ -1866,5 +1870,6 @@ _deck:
tl: "Timeline" tl: "Timeline"
antenna: "Antennas" antenna: "Antennas"
list: "List" list: "List"
channel: "Channel"
mentions: "Mentions" mentions: "Mentions"
direct: "Direct notes" direct: "Direct notes"

View File

@ -129,6 +129,7 @@ unblockConfirm: "¿Quiere dejar de bloquear esta cuenta?"
suspendConfirm: "¿Quiere suspender esta cuenta?" suspendConfirm: "¿Quiere suspender esta cuenta?"
unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?" unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?"
selectList: "Seleccione una lista" selectList: "Seleccione una lista"
selectChannel: "Seleccionar canal"
selectAntenna: "Seleccionar antena" selectAntenna: "Seleccionar antena"
selectWidget: "Seleccionar widget" selectWidget: "Seleccionar widget"
editWidgets: "Editar widgets" editWidgets: "Editar widgets"
@ -509,7 +510,7 @@ objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
serverLogs: "Registros del servidor" serverLogs: "Registros del servidor"
deleteAll: "Eliminar todos" deleteAll: "Eliminar todos"
showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo" 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" sounds: "Sonidos"
sound: "Sonidos" sound: "Sonidos"
listen: "Escuchar" listen: "Escuchar"
@ -918,17 +919,326 @@ tools: "Utilidades"
cannotLoad: "No se puede cargar." cannotLoad: "No se puede cargar."
numberOfProfileView: "Número de vistas de perfil" numberOfProfileView: "Número de vistas de perfil"
like: "¡Muy bien!" like: "¡Muy bien!"
unlike: "Quitar 'me gusta'"
numberOfLikes: "Cantidad de 'Me gusta'"
show: "Apariencia" 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" 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: _role:
new: "Crear rol"
edit: "Editar rol"
name: "Nombre del rol"
description: "Descripción del rol"
permission: "Permisos del rol"
descriptionOfPermission: "<b>Moderador</b> Te permite ejecutar acciones básicas de moderación.\n<b>Administradores</b> puede cambiar todas las configuraciones de la instancia."
assignTarget: "Asignar objetivo"
descriptionOfAssignTarget: "<b>Manual</b> Para cambiar manualmente lo que se incluye en este rol.\n<b>Condicional</b> 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: "Prioridad"
_priority: _priority:
low: "Baja" low: "Baja"
middle: "Mediano" middle: "Mediano"
high: "Alta" 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: _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." 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 detección" 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)." sensitivityDescription: "Reducir la sensibilidad puede acarrear a varios falsos positivos, mientras que incrementarla puede reducir las detecciones (falsos negativos)."
setSensitiveFlagAutomatically: "Marcar como NSFW" setSensitiveFlagAutomatically: "Marcar como NSFW"
setSensitiveFlagAutomaticallyDescription: "Los resultados de la detección interna pueden ser retenidos incluso si la opción está desactivada." 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" jobQueue: "Cola de trabajos"
serverMetric: "Estadísticas del servidor" serverMetric: "Estadísticas del servidor"
aiscript: "Consola de AiScript" aiscript: "Consola de AiScript"
aiscriptApp: "Aplicación AiScript"
aichan: "indigo" aichan: "indigo"
userList: "Lista de usuarios" userList: "Lista de usuarios"
_userList: _userList:
chooseList: "Seleccione una lista" chooseList: "Seleccione una lista"
clicker: "Cliqueador"
_cw: _cw:
hide: "Ocultar" hide: "Ocultar"
show: "Ver más" show: "Ver más"
@ -1434,7 +1746,16 @@ _timelines:
social: "Social" social: "Social"
global: "Global" global: "Global"
_play: _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" viewSource: "Ver la fuente"
my: "Mis guiones"
liked: "Guiones que te gustaron"
featured: "Popular" featured: "Popular"
title: "Título" title: "Título"
script: "Script" script: "Script"
@ -1507,6 +1828,7 @@ _notification:
pollEnded: "Estan disponibles los resultados de la encuesta" pollEnded: "Estan disponibles los resultados de la encuesta"
unreadAntennaNote: "Antena {name}" unreadAntennaNote: "Antena {name}"
emptyPushNotificationMessage: "Se han actualizado las notificaciones push" emptyPushNotificationMessage: "Se han actualizado las notificaciones push"
achievementEarned: "Logro desbloqueado"
_types: _types:
all: "Todo" all: "Todo"
follow: "Siguiendo" follow: "Siguiendo"
@ -1548,5 +1870,6 @@ _deck:
tl: "Linea de tiempo" tl: "Linea de tiempo"
antenna: "Antenas" antenna: "Antenas"
list: "Listas" list: "Listas"
channel: "Canal"
mentions: "Menciones" mentions: "Menciones"
direct: "Mensaje directo" direct: "Mensaje directo"

View File

@ -1541,5 +1541,6 @@ _deck:
tl: "Fil" tl: "Fil"
antenna: "Antennes" antenna: "Antennes"
list: "Listes" list: "Listes"
channel: "Canaux"
mentions: "Mentions" mentions: "Mentions"
direct: "Direct" direct: "Direct"

View File

@ -1673,5 +1673,6 @@ _deck:
tl: "Linimasa" tl: "Linimasa"
antenna: "Antena" antenna: "Antena"
list: "Daftar" list: "Daftar"
channel: "Kanal"
mentions: "Sebutan" mentions: "Sebutan"
direct: "Langsung" direct: "Langsung"

View File

@ -35,6 +35,7 @@ const languages = [
'pt-PT', 'pt-PT',
'ru-RU', 'ru-RU',
'sk-SK', 'sk-SK',
'th-TH',
'ug-CN', 'ug-CN',
'uk-UA', 'uk-UA',
'vi-VN', 'vi-VN',

View File

@ -1044,7 +1044,7 @@ _achievements:
flavor: "Grazie per aver usato Misskey!" flavor: "Grazie per aver usato Misskey!"
_noteClipped1: _noteClipped1:
title: "Devo clippare!" title: "Devo clippare!"
description: "Ho raccolto in Clip la prima Nota" description: "Hai raccolto la tua prima Nota in una Clip"
_noteFavorited1: _noteFavorited1:
title: "Guarda le stelle" title: "Guarda le stelle"
description: "Aggiungi una Nota ai preferiti per la prima volta" description: "Aggiungi una Nota ai preferiti per la prima volta"
@ -1080,7 +1080,7 @@ _achievements:
title: "Follow me!" title: "Follow me!"
description: "Hai ottenuto 10 profili Follower" description: "Hai ottenuto 10 profili Follower"
_followers50: _followers50:
title: "Follower a frotte" title: "Un gregge di Follower"
description: "Hai ottenuto 50 Follower" description: "Hai ottenuto 50 Follower"
_followers100: _followers100:
title: "Popolare" title: "Popolare"
@ -1108,7 +1108,7 @@ _achievements:
title: "Caccia al tesoro" title: "Caccia al tesoro"
description: "Hai trovato un tesoro nascosto" description: "Hai trovato un tesoro nascosto"
_client30min: _client30min:
title: "Piccola pausa" title: "Piccola grande pausa"
description: "Hai passato più di 30 minuti su Misskey" description: "Hai passato più di 30 minuti su Misskey"
_noteDeletedWithin1min: _noteDeletedWithin1min:
title: "Ooops!" title: "Ooops!"
@ -1134,7 +1134,7 @@ _achievements:
title: "Hello, world!" title: "Hello, world!"
description: "Hai scritto «Hello world» nel blocco appunti" description: "Hai scritto «Hello world» nel blocco appunti"
_open3windows: _open3windows:
title: "Finestrato" title: "Apri le finestre!"
description: "Hai aperto almeno 3 finestre contemporaneamente" description: "Hai aperto almeno 3 finestre contemporaneamente"
_driveFolderCircularReference: _driveFolderCircularReference:
title: "Riferimento circolare" title: "Riferimento circolare"
@ -1170,7 +1170,7 @@ _achievements:
_cookieClicked: _cookieClicked:
title: "Clicca il biscotto" title: "Clicca il biscotto"
description: "Hai giocato a cliccare il cookie" description: "Hai giocato a cliccare il cookie"
flavor: "Hai autorizzato i cookie?" flavor: "È il sito giusto?"
_brainDiver: _brainDiver:
title: "Brain Diver" title: "Brain Diver"
description: "Pubblica un link a Brain Diver" description: "Pubblica un link a Brain Diver"
@ -1195,6 +1195,9 @@ _role:
baseRole: "Ruolo di base" baseRole: "Ruolo di base"
useBaseValue: "Eredita dal ruolo base" useBaseValue: "Eredita dal ruolo base"
chooseRoleToAssign: "Seleziona il ruolo da assegnare" 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" canEditMembersByModerator: "Anche i Moderatori assegnano profili a questo ruolo"
descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori." descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori."
priority: "Priorità" priority: "Priorità"
@ -1866,5 +1869,6 @@ _deck:
tl: "Timeline" tl: "Timeline"
antenna: "Antenne" antenna: "Antenne"
list: "Liste" list: "Liste"
channel: "Canale"
mentions: "Menzioni" mentions: "Menzioni"
direct: "Diretta" direct: "Diretta"

View File

@ -130,6 +130,7 @@ unblockConfirm: "ブロック解除しますか?"
suspendConfirm: "凍結しますか?" suspendConfirm: "凍結しますか?"
unsuspendConfirm: "解凍しますか?" unsuspendConfirm: "解凍しますか?"
selectList: "リストを選択" selectList: "リストを選択"
selectChannel: "チャンネルを選択"
selectAntenna: "アンテナを選択" selectAntenna: "アンテナを選択"
selectWidget: "ウィジェットを選択" selectWidget: "ウィジェットを選択"
editWidgets: "ウィジェットを編集" editWidgets: "ウィジェットを編集"
@ -258,6 +259,8 @@ noMoreHistory: "これより過去の履歴はありません"
startMessaging: "チャットを開始" startMessaging: "チャットを開始"
nUsersRead: "{n}人が読みました" nUsersRead: "{n}人が読みました"
agreeTo: "{0}に同意" agreeTo: "{0}に同意"
agreeBelow: "下記に同意する"
basicNotesBeforeCreateAccount: "基本的な注意事項"
tos: "利用規約" tos: "利用規約"
start: "始める" start: "始める"
home: "ホーム" home: "ホーム"
@ -863,6 +866,8 @@ failedToFetchAccountInformation: "アカウント情報の取得に失敗しま
rateLimitExceeded: "レート制限を超えました" rateLimitExceeded: "レート制限を超えました"
cropImage: "画像のクロップ" cropImage: "画像のクロップ"
cropImageAsk: "画像をクロップしますか?" cropImageAsk: "画像をクロップしますか?"
cropYes: "クロップする"
cropNo: "そのまま使う"
file: "ファイル" file: "ファイル"
recentNHours: "直近{n}時間" recentNHours: "直近{n}時間"
recentNDays: "直近{n}日" recentNDays: "直近{n}日"
@ -941,6 +946,8 @@ cannotPerformTemporaryDescription: "操作回数が制限を超過するため
preset: "プリセット" preset: "プリセット"
selectFromPresets: "プリセットから選択" selectFromPresets: "プリセットから選択"
achievements: "実績" achievements: "実績"
gotInvalidResponseError: "サーバーの応答が無効です"
gotInvalidResponseErrorDescription: "サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。"
_achievements: _achievements:
earnedAt: "獲得日時" earnedAt: "獲得日時"
@ -1150,7 +1157,7 @@ _achievements:
description: "ここをクリックした" description: "ここをクリックした"
_justPlainLucky: _justPlainLucky:
title: "単なるラッキー" title: "単なるラッキー"
description: "10秒ごとに0.01%の確率で獲得" description: "10秒ごとに0.005%の確率で獲得"
_setNameToSyuilo: _setNameToSyuilo:
title: "神様コンプレックス" title: "神様コンプレックス"
description: "名前を syuilo に設定した" description: "名前を syuilo に設定した"
@ -1186,7 +1193,7 @@ _role:
description: "ロールの説明" description: "ロールの説明"
permission: "ロールの権限" permission: "ロールの権限"
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関する操作を行えます。\n<b>管理者</b>はインスタンスの全ての設定を変更できます。" descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関する操作を行えます。\n<b>管理者</b>はインスタンスの全ての設定を変更できます。"
assignTarget: "アサインターゲット" assignTarget: "アサイン"
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。" descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
manual: "マニュアル" manual: "マニュアル"
conditional: "コンディショナル" conditional: "コンディショナル"
@ -1199,6 +1206,9 @@ _role:
baseRole: "ベースロール" baseRole: "ベースロール"
useBaseValue: "ベースロールの値を使用" useBaseValue: "ベースロールの値を使用"
chooseRoleToAssign: "アサインするロールを選択" chooseRoleToAssign: "アサインするロールを選択"
iconUrl: "アイコン画像のURL"
asBadge: "バッジとして表示"
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可" canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
priority: "優先度" priority: "優先度"
@ -1632,12 +1642,15 @@ _permissions:
"write:gallery-likes": "ギャラリーのいいねを操作する" "write:gallery-likes": "ギャラリーのいいねを操作する"
_auth: _auth:
shareAccessTitle: "アプリへのアクセス許可"
shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?" shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?"
shareAccessAsk: "アカウントへのアクセスを許可しますか?" shareAccessAsk: "アカウントへのアクセスを許可しますか?"
permission: "{name}は次の権限を要求しています"
permissionAsk: "このアプリは次の権限を要求しています" permissionAsk: "このアプリは次の権限を要求しています"
pleaseGoBack: "アプリケーションに戻ってやっていってください" pleaseGoBack: "アプリケーションに戻ってやっていってください"
callback: "アプリケーションに戻っています" callback: "アプリケーションに戻っています"
denied: "アクセスを拒否しました" denied: "アクセスを拒否しました"
pleaseLogin: "アプリケーションにアクセス許可を与えるには、ログインが必要です。"
_antennaSources: _antennaSources:
all: "全てのノート" all: "全てのノート"
@ -1929,5 +1942,6 @@ _deck:
tl: "タイムライン" tl: "タイムライン"
antenna: "アンテナ" antenna: "アンテナ"
list: "リスト" list: "リスト"
channel: "チャンネル"
mentions: "あなた宛て" mentions: "あなた宛て"
direct: "ダイレクト" direct: "ダイレクト"

View File

@ -1628,5 +1628,6 @@ _deck:
tl: "タイムライン" tl: "タイムライン"
antenna: "アンテナ" antenna: "アンテナ"
list: "リスト" list: "リスト"
channel: "チャンネル"
mentions: "あんた宛て" mentions: "あんた宛て"
direct: "ダイレクト" direct: "ダイレクト"

View File

@ -1866,5 +1866,6 @@ _deck:
tl: "타임라인" tl: "타임라인"
antenna: "안테나" antenna: "안테나"
list: "리스트" list: "리스트"
channel: "채널"
mentions: "받은 멘션" mentions: "받은 멘션"
direct: "다이렉트" direct: "다이렉트"

162
locales/lo-LA.yml Normal file
View File

@ -0,0 +1,162 @@
---
_lang_: "ພາສາລາວ"
headlineMisskey: "ເຊື່ອມຕໍ່ເຄືອຂ່າຍໂດຍຫມາຍເຫດ"
introMisskey: "ຍິນດີຕ້ອນຮັບ! Misskey ເປັນແຫຼ່ງເປີດ, ການບໍລິການ microblogging ກະຈາຍ\nສ້າງ \"ບັນທຶກ\" ເພື່ອແບ່ງປັນຄວາມຄິດຂອງທ່ານກັບທຸກໆຄົນທີ່ຢູ່ອ້ອມຮອບທ່ານ 📡\nດ້ວຍ \"ປະຕິກິລິຍາ\", ທ່ານຍັງສາມາດສະແດງຄວາມຮູ້ສຶກຂອງທ່ານຢ່າງໄວວາກ່ຽວກັບບັນທຶກຂອງທຸກໆຄົນ 👍\nມາສຳຫຼວດໂລກໃໝ່! 🚀"
poweredByMisskeyDescription: "{name} ແມ່ນສ່ວນໜຶ່ງຂອງການບໍລິການທີ່ຂັບເຄື່ອນໂດຍແພລດຟອມ open source. <b>Misskey</b> (ເອີ້ນວ່າ \"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: "ກ່າວເຖິງ"

View File

@ -1438,5 +1438,6 @@ _deck:
tl: "Oś czasu" tl: "Oś czasu"
antenna: "Anteny" antenna: "Anteny"
list: "Listy" list: "Listy"
channel: "Kanały"
mentions: "Wspomnienia" mentions: "Wspomnienia"
direct: "Bezpośredni" direct: "Bezpośredni"

View File

@ -721,4 +721,5 @@ _deck:
tl: "Cronologie" tl: "Cronologie"
antenna: "Antene" antenna: "Antene"
list: "Liste" list: "Liste"
channel: "Canale"
mentions: "Mențiuni" mentions: "Mențiuni"

View File

@ -1845,5 +1845,6 @@ _deck:
tl: "Лента" tl: "Лента"
antenna: "Антенны" antenna: "Антенны"
list: "Списки" list: "Списки"
channel: "Каналы"
mentions: "Упоминания" mentions: "Упоминания"
direct: "Личное" direct: "Личное"

View File

@ -1545,5 +1545,6 @@ _deck:
tl: "Časová os" tl: "Časová os"
antenna: "Antény" antenna: "Antény"
list: "Zoznam" list: "Zoznam"
channel: "Kanály"
mentions: "Zmienky" mentions: "Zmienky"
direct: "Priame poznámky" direct: "Priame poznámky"

View File

@ -129,6 +129,7 @@ unblockConfirm: "คุณแน่ใจแล้วเหรอ? ว่าต
suspendConfirm: "นายแน่ใจแล้วเหรอว่าต้องการระงับบัญชีนี้อ่ะ?" suspendConfirm: "นายแน่ใจแล้วเหรอว่าต้องการระงับบัญชีนี้อ่ะ?"
unsuspendConfirm: "นายแน่ใจแล้วหรอ? ว่าต้องการยกเลิกการระงับบัญชีนี้" unsuspendConfirm: "นายแน่ใจแล้วหรอ? ว่าต้องการยกเลิกการระงับบัญชีนี้"
selectList: "เลือกรายการ" selectList: "เลือกรายการ"
selectChannel: "เลือกแชนแนล"
selectAntenna: "เลือกเสาอากาศ" selectAntenna: "เลือกเสาอากาศ"
selectWidget: "เลือกวิดเจ็ต" selectWidget: "เลือกวิดเจ็ต"
editWidgets: "แก้ไขวิดเจ็ต" editWidgets: "แก้ไขวิดเจ็ต"
@ -1147,7 +1148,7 @@ _achievements:
description: "คุณได้คลิกที่นี่" description: "คุณได้คลิกที่นี่"
_justPlainLucky: _justPlainLucky:
title: "แค่ลัคกี้ธรรมดา" title: "แค่ลัคกี้ธรรมดา"
description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.01% ทุก ๆ 10 วินาที" description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.005% ทุก ๆ 10 วินาที"
_setNameToSyuilo: _setNameToSyuilo:
title: "พระเจ้าคอมเพล็กซ์" title: "พระเจ้าคอมเพล็กซ์"
description: "ตั้งชื่อของคุณเป็น \"syuilo\"" description: "ตั้งชื่อของคุณเป็น \"syuilo\""
@ -1182,7 +1183,7 @@ _role:
description: "คำอธิบายบทบาท" description: "คำอธิบายบทบาท"
permission: "สิทธิ์ตามบทบาท" permission: "สิทธิ์ตามบทบาท"
descriptionOfPermission: "<b>ผู้ดูแลกลั่นกรองเนื้อหา</b> สามารถดำเนินการดูแลขั้นพื้นฐานได้นะ\n<b>ผู้ดูแลระบบ</b> สามารถเปลี่ยนการตั้งค่าทั้งหมดของอินสแตนซ์ได้นะ" descriptionOfPermission: "<b>ผู้ดูแลกลั่นกรองเนื้อหา</b> สามารถดำเนินการดูแลขั้นพื้นฐานได้นะ\n<b>ผู้ดูแลระบบ</b> สามารถเปลี่ยนการตั้งค่าทั้งหมดของอินสแตนซ์ได้นะ"
assignTarget: "กำหนดเป้าหมาย" assignTarget: "มอบหมาย"
descriptionOfAssignTarget: "<b>แมนนวล</b> เพื่อเปลี่ยนผู้ที่เป็นส่วนหนึ่งของบทบาทนี้และใครที่ไม่ใช่ด้วยตนเอง\n<b>เงื่อนไข</b> เพื่อให้ผู้ใช้ได้รับการกำหนดและนำออกจากบทบาทนี้โดยอัตโนมัติตามเงื่อนไขชุดหนึ่ง" descriptionOfAssignTarget: "<b>แมนนวล</b> เพื่อเปลี่ยนผู้ที่เป็นส่วนหนึ่งของบทบาทนี้และใครที่ไม่ใช่ด้วยตนเอง\n<b>เงื่อนไข</b> เพื่อให้ผู้ใช้ได้รับการกำหนดและนำออกจากบทบาทนี้โดยอัตโนมัติตามเงื่อนไขชุดหนึ่ง"
manual: "ปรับเอง" manual: "ปรับเอง"
conditional: "มีเงื่อนไข" conditional: "มีเงื่อนไข"
@ -1195,6 +1196,9 @@ _role:
baseRole: "บทบาทพื้นฐาน" baseRole: "บทบาทพื้นฐาน"
useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น" useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น"
chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด" chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
iconUrl: "ไอคอน URL"
asBadge: "แสดงเป็นตรา"
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก" canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ" descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
priority: "ลำดับความสำคัญ" priority: "ลำดับความสำคัญ"
@ -1866,5 +1870,6 @@ _deck:
tl: "ไทม์ไลน์" tl: "ไทม์ไลน์"
antenna: "เสาอากาศ" antenna: "เสาอากาศ"
list: "รายการ" list: "รายการ"
channel: "แชนแนล"
mentions: "พูดถึง" mentions: "พูดถึง"
direct: "ไดเร็ค" direct: "ไดเร็ค"

View File

@ -529,7 +529,7 @@ state: "Стан"
sort: "Сортування" sort: "Сортування"
ascendingOrder: "За зростанням" ascendingOrder: "За зростанням"
descendingOrder: "За спаданням" descendingOrder: "За спаданням"
scratchpad: "Чернетка" scratchpad: "Scratchpad"
scratchpadDescription: "Scratchpad надає середовище для експериментів з AiScript. Ви можете писати, виконувати його і тестувати взаємодію з Misskey." scratchpadDescription: "Scratchpad надає середовище для експериментів з AiScript. Ви можете писати, виконувати його і тестувати взаємодію з Misskey."
output: "Вихід" output: "Вихід"
script: "Скрипт" script: "Скрипт"
@ -1084,22 +1084,32 @@ _achievements:
description: "Перевищити швидкість домашньої стрічки 20npm (нотаток на хвилину)" description: "Перевищити швидкість домашньої стрічки 20npm (нотаток на хвилину)"
_viewInstanceChart: _viewInstanceChart:
title: "Аналітик" title: "Аналітик"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
description: "Вивести \"hello world\" у Скретчпаді"
_clickedClickHere: _clickedClickHere:
title: "Натисніть тут" title: "Натисніть тут"
description: "Натиснуто тут" description: "Натиснуто тут"
_justPlainLucky:
title: "Просто вдача"
description: "Можна отримати з ймовірністю 0,01% кожні 10 секунд"
_setNameToSyuilo: _setNameToSyuilo:
title: "Комплекс бога" title: "Комплекс бога"
description: "Встановлено ім'я \"syuilo\"" description: "Встановлено ім'я \"syuilo\""
_passedSinceAccountCreated1: _passedSinceAccountCreated1:
title: "Перша річниця" title: "Перша річниця"
description: "Минув рік з моменту створення акаунта"
_passedSinceAccountCreated2: _passedSinceAccountCreated2:
title: "Друга річниця" title: "Друга річниця"
description: "Минуло 2 роки з моменту створення акаунта"
_passedSinceAccountCreated3: _passedSinceAccountCreated3:
title: "Третя річниця" title: "Третя річниця"
description: "Минуло 3 роки з моменту створення акаунта" description: "Минуло 3 роки з моменту створення акаунта"
_loggedInOnBirthday: _loggedInOnBirthday:
title: "З Днем народження!" title: "З Днем народження!"
description: "Увійти у свій день народження"
_loggedInOnNewYearsDay: _loggedInOnNewYearsDay:
title: "З Новим роком!"
description: "Увійшли в перший день року" description: "Увійшли в перший день року"
_brainDiver: _brainDiver:
title: "Brain Diver" title: "Brain Diver"
@ -1372,8 +1382,8 @@ _tutorial:
step1_1: "Ласкаво просимо!" step1_1: "Ласкаво просимо!"
step1_2: "Ця сторінка має назву \"стрічка подій\". На ній з'являються записи користувачів на яких ви підписані." step1_2: "Ця сторінка має назву \"стрічка подій\". На ній з'являються записи користувачів на яких ви підписані."
step1_3: "Наразі ваша стрічка порожня, оскільки ви ще не написали жодної нотатки і не підписані на інших." step1_3: "Наразі ваша стрічка порожня, оскільки ви ще не написали жодної нотатки і не підписані на інших."
step2_1: "Перш ніж зробити запис або підписатись на когось, спочатку заповніть свій обліковий запис." step2_1: "Перш ніж зробити запис або підписатись на когось, заповніть свій профіль."
step2_2: "Надання деякої інформації про себе дозволить іншим користувачам підписатись на вас." step2_2: "Надання деякої інформації про себе допоможе іншим користувачам вирішити підписатись на вас."
step3_1: "Ви успішно налаштували свій обліковий запис?" step3_1: "Ви успішно налаштували свій обліковий запис?"
step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані." step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані."
step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми." step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми."
@ -1679,5 +1689,6 @@ _deck:
tl: "Стрічка" tl: "Стрічка"
antenna: "Антени" antenna: "Антени"
list: "Списки" list: "Списки"
channel: "Канали"
mentions: "Згадки" mentions: "Згадки"
direct: "Особисте" direct: "Особисте"

View File

@ -1520,5 +1520,6 @@ _deck:
tl: "Bảng tin" tl: "Bảng tin"
antenna: "Trạm phát sóng" antenna: "Trạm phát sóng"
list: "Danh sách" list: "Danh sách"
channel: "Kênh"
mentions: "Lượt nhắc" mentions: "Lượt nhắc"
direct: "Nhắn riêng" direct: "Nhắn riêng"

View File

@ -1023,17 +1023,23 @@ _achievements:
title: "定期联系Ⅲ" title: "定期联系Ⅲ"
description: "总登录天数400天" description: "总登录天数400天"
_login500: _login500:
title: "老熟人Ⅰ"
description: "总登录天数500天" description: "总登录天数500天"
flavor: "诸君,我喜欢贴文" flavor: "诸君,我喜欢贴文"
_login600: _login600:
title: "老熟人Ⅱ"
description: "总登录天数600天" description: "总登录天数600天"
_login700: _login700:
title: "老熟人Ⅲ"
description: "总登录天数700天" description: "总登录天数700天"
_login800: _login800:
title: "帖子大师Ⅰ"
description: "总登录天数800天" description: "总登录天数800天"
_login900: _login900:
title: "帖子大师Ⅱ"
description: "总登录天数900天" description: "总登录天数900天"
_login1000: _login1000:
title: "帖子大师Ⅲ"
description: "总登录天数1000天" description: "总登录天数1000天"
flavor: "感谢您使用Misskey" flavor: "感谢您使用Misskey"
_noteClipped1: _noteClipped1:
@ -1072,19 +1078,22 @@ _achievements:
description: "第一次被关注" description: "第一次被关注"
_followers10: _followers10:
title: "关注我吧!" title: "关注我吧!"
description: "关注者超过10人" description: "拥有超过10名关注者"
_followers50: _followers50:
title: "三五成群" title: "三五成群"
description: "关注者超过50人" description: "拥有超过50名关注者"
_followers100: _followers100:
title: "胜友如云" title: "胜友如云"
description: "关注者超过100人" description: "拥有超过100名关注者"
_followers300: _followers300:
title: "排列成行" title: "排列成行"
description: "关注者超过300人" description: "拥有超过300名关注者"
_followers500: _followers500:
title: "风向标" title: "信号塔"
description: "关注者超过500人" description: "拥有超过500名关注者"
_followers1000:
title: "大影响家"
description: "拥有超过1000名关注者"
_collectAchievements30: _collectAchievements30:
title: "成就收藏家" title: "成就收藏家"
description: "获得超过30个成就" description: "获得超过30个成就"
@ -1096,6 +1105,7 @@ _achievements:
description: "发布\"I ❤ #Misskey\"帖子" description: "发布\"I ❤ #Misskey\"帖子"
flavor: "感谢您使用 Misskey by 开发团队" flavor: "感谢您使用 Misskey by 开发团队"
_foundTreasure: _foundTreasure:
title: "寻宝"
description: "发现了隐藏的宝藏" description: "发现了隐藏的宝藏"
_client30min: _client30min:
title: "休息一下!" title: "休息一下!"
@ -1104,7 +1114,7 @@ _achievements:
title: "无话可说" title: "无话可说"
description: "发帖后一分钟内就将其删除" description: "发帖后一分钟内就将其删除"
_postedAtLateNight: _postedAtLateNight:
title: "夜行者" title: "夜猫子"
description: "深夜发布帖子" description: "深夜发布帖子"
flavor: "差不多该去睡了喔。" flavor: "差不多该去睡了喔。"
_postedAt0min0sec: _postedAt0min0sec:
@ -1114,13 +1124,21 @@ _achievements:
_selfQuote: _selfQuote:
title: "自我提及" title: "自我提及"
description: "引用了自己的帖子" description: "引用了自己的帖子"
_htl20npm:
title: "流动的时间线"
description: "在首页时间线的流速超过20npm"
_viewInstanceChart:
title: "分析师"
description: "查看了实例信息中的图表"
_outputHelloWorldOnScratchpad: _outputHelloWorldOnScratchpad:
title: "Hello, world!" title: "Hello, world!"
description: "在AiScript控制台中输出 hello world"
_open3windows: _open3windows:
title: "多窗口" title: "多窗口"
description: "打开了三个或更多的窗口" description: "打开了三个或更多的窗口"
_driveFolderCircularReference: _driveFolderCircularReference:
title: "循环引用" title: "循环引用"
description: "试图对网盘中的文件夹进行循环嵌套"
_reactWithoutRead: _reactWithoutRead:
title: "有好好读过吗?" title: "有好好读过吗?"
description: "在含有100字以上的帖子被发出三秒内做出回应" description: "在含有100字以上的帖子被发出三秒内做出回应"
@ -1129,7 +1147,7 @@ _achievements:
description: "点了这里" description: "点了这里"
_justPlainLucky: _justPlainLucky:
title: "超高校级的幸运" title: "超高校级的幸运"
description: "每10秒有0.01的概率获得" description: "每10秒有0.01的概率自动获得"
_setNameToSyuilo: _setNameToSyuilo:
title: "像神一样呐" title: "像神一样呐"
description: "将名称设定为syuilo" description: "将名称设定为syuilo"
@ -1177,6 +1195,9 @@ _role:
baseRole: "基本角色" baseRole: "基本角色"
useBaseValue: "使用基本角色的值" useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "选择要分配的角色" chooseRoleToAssign: "选择要分配的角色"
iconUrl: "图标URL"
asBadge: "作为徽章显示"
descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。"
canEditMembersByModerator: "允许监察者编辑成员" canEditMembersByModerator: "允许监察者编辑成员"
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
priority: "优先级" priority: "优先级"
@ -1848,5 +1869,6 @@ _deck:
tl: "时间线" tl: "时间线"
antenna: "天线" antenna: "天线"
list: "列表" list: "列表"
channel: "频道"
mentions: "提及" mentions: "提及"
direct: "指定用户" direct: "指定用户"

View File

@ -326,7 +326,7 @@ connectService: "己連結"
disconnectService: "己斷開 " disconnectService: "己斷開 "
enableLocalTimeline: "開啟本地時間軸" enableLocalTimeline: "開啟本地時間軸"
enableGlobalTimeline: "啟用全域時間軸" enableGlobalTimeline: "啟用全域時間軸"
disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審員仍可以繼續使用。" disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審員仍可以繼續使用。"
registration: "註冊" registration: "註冊"
enableRegistration: "開啟新使用者註冊" enableRegistration: "開啟新使用者註冊"
invite: "邀請" invite: "邀請"
@ -389,8 +389,8 @@ aboutMisskey: "關於 Misskey"
administrator: "管理員" administrator: "管理員"
token: "權杖" token: "權杖"
twoStepAuthentication: "兩階段驗證" twoStepAuthentication: "兩階段驗證"
moderator: "審員" moderator: "審員"
moderation: "監察" moderation: "審查"
nUsersMentioned: "提到了{n}" nUsersMentioned: "提到了{n}"
securityKey: "安全金鑰" securityKey: "安全金鑰"
securityKeyName: "金鑰名稱" securityKeyName: "金鑰名稱"
@ -607,7 +607,7 @@ testEmail: "測試郵件發送"
wordMute: "被靜音的文字" wordMute: "被靜音的文字"
regexpError: "正規表達式錯誤" regexpError: "正規表達式錯誤"
regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:" regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:"
instanceMute: "實例的靜音" instanceMute: "被靜音的實例"
userSaysSomething: "{name}說了什麼" userSaysSomething: "{name}說了什麼"
makeActive: "啟用" makeActive: "啟用"
display: "檢視" display: "檢視"
@ -939,6 +939,8 @@ cannotPerformTemporaryDescription: "由於超過操作次數限制,暫時無
preset: "預設值" preset: "預設值"
selectFromPresets: "從預設值中選擇" selectFromPresets: "從預設值中選擇"
achievements: "成就" achievements: "成就"
gotInvalidResponseError: "伺服器的回應無效"
gotInvalidResponseErrorDescription: "伺服器可能已關閉或者在維護中,請稍後再試。"
_achievements: _achievements:
earnedAt: "獲得日期" earnedAt: "獲得日期"
_types: _types:
@ -1181,7 +1183,7 @@ _role:
name: "角色名稱" name: "角色名稱"
description: "角色描述 " description: "角色描述 "
permission: "角色的權限" permission: "角色的權限"
descriptionOfPermission: "<b>審核員</b>執行與審核相關的基本操作。\n<b>管理員</b>能變更實例的全部設定" descriptionOfPermission: "<b>審查員</b>執行與審查相關的基本操作。\n<b>管理員</b>能變更實例的全部設定"
assignTarget: "指派目標" assignTarget: "指派目標"
descriptionOfAssignTarget: "<b>手動</b>是以手動管理這個角色包含的人員。\n<b>符合條件</b>是設定條件以自動包含符合條件的使用者。" descriptionOfAssignTarget: "<b>手動</b>是以手動管理這個角色包含的人員。\n<b>符合條件</b>是設定條件以自動包含符合條件的使用者。"
manual: "手動" manual: "手動"
@ -1195,8 +1197,11 @@ _role:
baseRole: "基本角色" baseRole: "基本角色"
useBaseValue: "使用基本角色的值" useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "選擇要指派的角色" chooseRoleToAssign: "選擇要指派的角色"
canEditMembersByModerator: "允許編輯監察員的成員" iconUrl: "圖示的URL"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。" asBadge: "顯示為徽章"
descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。"
canEditMembersByModerator: "允許編輯審查員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
priority: "優先級" priority: "優先級"
_priority: _priority:
low: "低" low: "低"
@ -1233,7 +1238,7 @@ _role:
or: "~或~" or: "~或~"
not: "~否" not: "~否"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審。 伺服器的負荷會稍微增加。" description: "您可以使用機器學習自動檢測敏感媒體並將其用於審。 伺服器的負荷會稍微增加。"
sensitivity: "檢測敏感度" sensitivity: "檢測敏感度"
sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。" sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。"
setSensitiveFlagAutomatically: "設定 NSFW 旗標" setSensitiveFlagAutomatically: "設定 NSFW 旗標"
@ -1866,5 +1871,6 @@ _deck:
tl: "時間軸" tl: "時間軸"
antenna: "天線" antenna: "天線"
list: "清單" list: "清單"
channel: "頻道"
mentions: "提及" mentions: "提及"
direct: "指定使用者" direct: "指定使用者"

View File

@ -1,12 +1,12 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.2.6-simkey", "version": "13.5.5-simkey",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/sim1222/misskey.git" "url": "https://github.com/sim1222/misskey.git"
}, },
"packageManager": "pnpm@7.24.3", "packageManager": "pnpm@7.27.0",
"workspaces": [ "workspaces": [
"packages/frontend", "packages/frontend",
"packages/backend", "packages/backend",
@ -19,7 +19,7 @@
"start": "cd packages/backend && node ./built/boot/index.js", "start": "cd packages/backend && node ./built/boot/index.js",
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js",
"init": "pnpm migrate", "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", "migrateandstart": "pnpm migrate && pnpm start",
"gulp": "pnpm exec gulp build", "gulp": "pnpm exec gulp build",
"watch": "pnpm dev", "watch": "pnpm dev",
@ -28,8 +28,8 @@
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts", "cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
"cy:run": "pnpm cypress run", "cy:run": "pnpm cypress run",
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy: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": "cd packages/backend && pnpm jest",
"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-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
"test": "pnpm jest", "test": "pnpm jest",
"test-and-coverage": "pnpm jest-and-coverage", "test-and-coverage": "pnpm jest-and-coverage",
"format": "pnpm exec gulp format", "format": "pnpm exec gulp format",
@ -38,8 +38,8 @@
"cleanall": "pnpm clean-all" "cleanall": "pnpm clean-all"
}, },
"resolutions": { "resolutions": {
"chokidar": "^3.5.3", "chokidar": "3.5.3",
"lodash": "^4.17.21" "lodash": "4.17.21"
}, },
"dependencies": { "dependencies": {
"execa": "5.1.1", "execa": "5.1.1",
@ -49,19 +49,19 @@
"gulp-replace": "1.1.4", "gulp-replace": "1.1.4",
"gulp-terser": "2.1.0", "gulp-terser": "2.1.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"typescript": "4.9.4" "typescript": "4.9.5"
}, },
"devDependencies": { "devDependencies": {
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.49.0", "@typescript-eslint/eslint-plugin": "5.51.0",
"@typescript-eslint/parser": "5.49.0", "@typescript-eslint/parser": "5.51.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.4.0", "cypress": "12.5.1",
"eslint": "^8.32.0", "eslint": "8.33.0",
"start-server-and-test": "1.15.3" "start-server-and-test": "1.15.3"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tensorflow/tfjs-core": "^4.2.0" "@tensorflow/tfjs-core": "4.2.0"
} }
} }

View File

@ -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`);
}
}

View File

@ -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"`);
}
}

View File

@ -1,6 +1,6 @@
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { loadConfig } from './built/config.js'; import { loadConfig } from './built/config.js';
import { entities } from './built/postgre.js'; import { entities } from './built/postgres.js';
const config = loadConfig(); const config = loadConfig();

View File

@ -19,34 +19,34 @@
"test-and-coverage": "pnpm jest-and-coverage" "test-and-coverage": "pnpm jest-and-coverage"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tensorflow/tfjs": "^4.2.0", "@tensorflow/tfjs": "4.2.0",
"@tensorflow/tfjs-node": "4.2.0" "@tensorflow/tfjs-node": "4.2.0"
}, },
"dependencies": { "dependencies": {
"@bull-board/api": "^4.11.0", "@bull-board/api": "4.11.1",
"@bull-board/fastify": "^4.11.0", "@bull-board/fastify": "4.11.1",
"@bull-board/ui": "^4.11.0", "@bull-board/ui": "4.11.1",
"@discordapp/twemoji": "14.0.2", "@discordapp/twemoji": "14.0.2",
"@fastify/accepts": "4.1.0", "@fastify/accepts": "4.1.0",
"@fastify/cookie": "^8.3.0", "@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.0", "@fastify/cors": "8.2.0",
"@fastify/http-proxy": "^8.4.0", "@fastify/http-proxy": "8.4.0",
"@fastify/multipart": "7.4.0", "@fastify/multipart": "7.4.0",
"@fastify/static": "6.7.0", "@fastify/static": "6.8.0",
"@fastify/view": "7.4.1", "@fastify/view": "7.4.1",
"@nestjs/common": "9.2.1", "@nestjs/common": "9.3.7",
"@nestjs/core": "9.2.1", "@nestjs/core": "9.3.7",
"@nestjs/testing": "9.2.1", "@nestjs/testing": "9.3.7",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2", "@sinonjs/fake-timers": "10.0.2",
"accepts": "^1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "5.3.1", "archiver": "5.3.1",
"autwh": "0.1.0", "autwh": "0.1.0",
"aws-sdk": "2.1295.0", "aws-sdk": "2.1295.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.4", "blurhash": "2.0.4",
"bull": "4.10.2", "bull": "4.10.3",
"cacheable-lookup": "6.1.0", "cacheable-lookup": "6.1.0",
"cbor": "8.1.0", "cbor": "8.1.0",
"chalk": "5.2.0", "chalk": "5.2.0",
@ -62,11 +62,11 @@
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.2.0", "file-type": "18.2.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"form-data": "^4.0.0", "form-data": "4.0.0",
"got": "^12.5.3", "got": "12.5.3",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"ioredis": "4.28.5", "ioredis": "4.28.5",
"ip-cidr": "3.0.11", "ip-cidr": "3.1.0",
"is-svg": "4.3.2", "is-svg": "4.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "21.1.0", "jsdom": "21.1.0",
@ -75,22 +75,22 @@
"jsrsasign": "10.6.1", "jsrsasign": "10.6.1",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-js": "0.0.14", "misskey-js": "0.0.15",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.0", "node-fetch": "3.3.0",
"nodemailer": "6.9.0", "nodemailer": "6.9.1",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "^0.10.0", "oauth": "0.10.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.8.0", "pg": "8.9.0",
"private-ip": "3.0.0", "private-ip": "3.0.0",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"pug": "3.0.2", "pug": "3.0.2",
"punycode": "2.3.0", "punycode": "2.3.0",
"pureimage": "0.3.15", "pureimage": "0.3.17",
"qrcode": "1.5.1", "qrcode": "1.5.1",
"random-seed": "0.3.0", "random-seed": "0.3.0",
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
@ -102,23 +102,22 @@
"rss-parser": "3.12.0", "rss-parser": "3.12.0",
"rxjs": "7.8.0", "rxjs": "7.8.0",
"s-age": "1.1.2", "s-age": "1.1.2",
"sanitize-html": "2.8.1", "sanitize-html": "2.9.0",
"seedrandom": "^3.0.5", "seedrandom": "3.0.5",
"semver": "7.3.8", "semver": "7.3.8",
"sharp": "0.31.3", "sharp": "0.31.3",
"speakeasy": "2.0.0", "speakeasy": "2.0.0",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "2.7.0", "summaly": "2.7.0",
"syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2", "systeminformation": "5.17.8",
"systeminformation": "5.17.4", "tinycolor2": "1.6.0",
"tinycolor2": "1.5.2",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.2", "tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.2", "tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.11", "typeorm": "0.3.12",
"typescript": "4.9.4", "typescript": "4.9.5",
"ulid": "2.3.0", "ulid": "2.3.0",
"unzipper": "0.10.11", "unzipper": "0.10.11",
"uuid": "9.0.0", "uuid": "9.0.0",
@ -129,28 +128,28 @@
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.4.1", "@jest/globals": "29.4.2",
"@redocly/openapi-core": "1.0.0-beta.120", "@redocly/openapi-core": "1.0.0-beta.123",
"@swc/cli": "^0.1.59", "@swc/cli": "0.1.61",
"@swc/core": "1.3.29", "@swc/core": "1.3.34",
"@swc/jest": "0.2.24", "@swc/jest": "0.2.24",
"@types/accepts": "1.3.5", "@types/accepts": "1.3.5",
"@types/archiver": "5.3.1", "@types/archiver": "5.3.1",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/bull": "4.10.0", "@types/bull": "4.10.0",
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
"@types/color-convert": "^2.0.0", "@types/color-convert": "2.0.0",
"@types/content-disposition": "^0.5.5", "@types/content-disposition": "0.5.5",
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.20", "@types/fluent-ffmpeg": "2.1.20",
"@types/ioredis": "4.28.10", "@types/ioredis": "4.28.10",
"@types/jest": "29.4.0", "@types/jest": "29.4.0",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/jsdom": "20.0.1", "@types/jsdom": "21.1.0",
"@types/jsonld": "1.5.8", "@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.5", "@types/jsrsasign": "10.5.5",
"@types/mime-types": "2.1.1", "@types/mime-types": "2.1.1",
"@types/node": "18.11.18", "@types/node": "18.13.0",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.7", "@types/nodemailer": "6.4.7",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
@ -167,7 +166,6 @@
"@types/sharp": "0.31.1", "@types/sharp": "0.31.1",
"@types/sinonjs__fake-timers": "8.1.2", "@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7", "@types/speakeasy": "2.0.7",
"@types/syslog-pro": "^1.0.0",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3", "@types/tmp": "0.2.3",
"@types/unzipper": "0.10.5", "@types/unzipper": "0.10.5",
@ -176,13 +174,13 @@
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.49.0", "@typescript-eslint/eslint-plugin": "5.51.0",
"@typescript-eslint/parser": "5.49.0", "@typescript-eslint/parser": "5.51.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.32.0", "eslint": "8.33.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"execa": "6.1.0", "execa": "6.1.0",
"jest": "29.4.1", "jest": "29.4.2",
"jest-mock": "^29.4.1" "jest-mock": "29.4.2"
} }
} }

View File

@ -4,7 +4,7 @@ import { DataSource } from 'typeorm';
import { createRedisConnection } from '@/redis.js'; import { createRedisConnection } from '@/redis.js';
import { DI } from './di-symbols.js'; import { DI } from './di-symbols.js';
import { loadConfig } from './config.js'; import { loadConfig } from './config.js';
import { createPostgreDataSource } from './postgre.js'; import { createPostgresDataSource } from './postgres.js';
import { RepositoryModule } from './models/RepositoryModule.js'; import { RepositoryModule } from './models/RepositoryModule.js';
import type { Provider, OnApplicationShutdown } from '@nestjs/common'; import type { Provider, OnApplicationShutdown } from '@nestjs/common';
@ -18,7 +18,7 @@ const $config: Provider = {
const $db: Provider = { const $db: Provider = {
provide: DI.db, provide: DI.db,
useFactory: async (config) => { useFactory: async (config) => {
const db = createPostgreDataSource(config); const db = createPostgresDataSource(config);
return await db.initialize(); return await db.initialize();
}, },
inject: [DI.config], inject: [DI.config],

View File

@ -65,11 +65,6 @@ export type Source = {
deliverJobMaxAttempts?: number; deliverJobMaxAttempts?: number;
inboxJobMaxAttempts?: number; inboxJobMaxAttempts?: number;
syslog: {
host: string;
port: number;
};
mediaProxy?: string; mediaProxy?: string;
proxyRemoteFiles?: boolean; proxyRemoteFiles?: boolean;
@ -92,6 +87,8 @@ export type Mixin = {
userAgent: string; userAgent: string;
clientEntry: string; clientEntry: string;
clientManifestExists: boolean; clientManifestExists: boolean;
mediaProxy: string;
externalMediaProxyEnabled: boolean;
}; };
export type Config = Source & Mixin; export type Config = Source & Mixin;
@ -113,7 +110,7 @@ const path = process.env.NODE_ENV === 'test'
export function loadConfig() { export function loadConfig() {
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8')); 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 ? const clientManifest = clientManifestExists ?
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8')) JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
: { 'src/init.ts': { file: 'src/init.ts' } }; : { 'src/init.ts': { file: 'src/init.ts' } };
@ -140,6 +137,13 @@ export function loadConfig() {
mixin.clientEntry = clientManifest['src/init.ts']; mixin.clientEntry = clientManifest['src/init.ts'];
mixin.clientManifestExists = clientManifestExists; 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; if (!config.redis.prefix) config.redis.prefix = mixin.host;
return Object.assign(config, mixin); return Object.assign(config, mixin);

View File

@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { CreateNotificationService } from '@/core/CreateNotificationService.js';
const ACHIEVEMENT_TYPES = [ export const ACHIEVEMENT_TYPES = [
'notes1', 'notes1',
'notes10', 'notes10',
'notes100', 'notes100',

View File

@ -10,10 +10,9 @@ import { isUserRelated } from '@/misc/is-user-related.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import { Cache } from '@/misc/cache.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/schema.js';
import { DI } from '@/di-symbols.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 { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js'; import { StreamMessages } from '@/server/api/stream/types.js';
@ -23,7 +22,6 @@ import type { OnApplicationShutdown } from '@nestjs/common';
export class AntennaService implements OnApplicationShutdown { export class AntennaService implements OnApplicationShutdown {
private antennasFetched: boolean; private antennasFetched: boolean;
private antennas: Antenna[]; private antennas: Antenna[];
private blockingCache: Cache<User['id'][]>;
constructor( constructor(
@Inject(DI.redisSubscriber) @Inject(DI.redisSubscriber)
@ -32,9 +30,6 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.mutingsRepository) @Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository, private mutingsRepository: MutingsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@ -52,14 +47,13 @@ export class AntennaService implements OnApplicationShutdown {
private utilityService: UtilityService, private utilityService: UtilityService,
private idService: IdService, private idService: IdService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService, private pushNotificationService: PushNotificationService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private antennaEntityService: AntennaEntityService, private antennaEntityService: AntennaEntityService,
) { ) {
this.antennasFetched = false; this.antennasFetched = false;
this.antennas = []; this.antennas = [];
this.blockingCache = new Cache<User['id'][]>(1000 * 60 * 5);
this.redisSubscriber.on('message', this.onRedisMessage); this.redisSubscriber.on('message', this.onRedisMessage);
} }
@ -109,7 +103,7 @@ export class AntennaService implements OnApplicationShutdown {
read: read, read: read,
}); });
this.globalEventServie.publishAntennaStream(antenna.id, 'note', note); this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
if (!read) { if (!read) {
const mutings = await this.mutingsRepository.find({ const mutings = await this.mutingsRepository.find({
@ -139,7 +133,7 @@ export class AntennaService implements OnApplicationShutdown {
setTimeout(async () => { setTimeout(async () => {
const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false }); const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false });
if (unread) { if (unread) {
this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna); this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', { this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
antenna: { id: antenna.id, name: antenna.name }, antenna: { id: antenna.id, name: antenna.name },
note: await this.noteEntityService.pack(note), note: await this.noteEntityService.pack(note),
@ -156,10 +150,6 @@ export class AntennaService implements OnApplicationShutdown {
if (note.visibility === 'specified') return false; if (note.visibility === 'specified') return false;
if (note.visibility === 'followers') 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; if (!antenna.withReplies && note.replyId != null) return false;
if (antenna.src === 'home') { if (antenna.src === 'home') {

View File

@ -62,7 +62,6 @@ import PerUserNotesChart from './chart/charts/per-user-notes.js';
import PerUserPvChart from './chart/charts/per-user-pv.js'; import PerUserPvChart from './chart/charts/per-user-pv.js';
import DriveChart from './chart/charts/drive.js'; import DriveChart from './chart/charts/drive.js';
import PerUserReactionsChart from './chart/charts/per-user-reactions.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 PerUserFollowingChart from './chart/charts/per-user-following.js';
import PerUserDriveChart from './chart/charts/per-user-drive.js'; import PerUserDriveChart from './chart/charts/per-user-drive.js';
import ApRequestChart from './chart/charts/ap-request.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 $PerUserPvChart: Provider = { provide: 'PerUserPvChart', useExisting: PerUserPvChart };
const $DriveChart: Provider = { provide: 'DriveChart', useExisting: DriveChart }; const $DriveChart: Provider = { provide: 'DriveChart', useExisting: DriveChart };
const $PerUserReactionsChart: Provider = { provide: 'PerUserReactionsChart', useExisting: PerUserReactionsChart }; const $PerUserReactionsChart: Provider = { provide: 'PerUserReactionsChart', useExisting: PerUserReactionsChart };
const $HashtagChart: Provider = { provide: 'HashtagChart', useExisting: HashtagChart };
const $PerUserFollowingChart: Provider = { provide: 'PerUserFollowingChart', useExisting: PerUserFollowingChart }; const $PerUserFollowingChart: Provider = { provide: 'PerUserFollowingChart', useExisting: PerUserFollowingChart };
const $PerUserDriveChart: Provider = { provide: 'PerUserDriveChart', useExisting: PerUserDriveChart }; const $PerUserDriveChart: Provider = { provide: 'PerUserDriveChart', useExisting: PerUserDriveChart };
const $ApRequestChart: Provider = { provide: 'ApRequestChart', useExisting: ApRequestChart }; const $ApRequestChart: Provider = { provide: 'ApRequestChart', useExisting: ApRequestChart };
@ -315,7 +313,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
PerUserPvChart, PerUserPvChart,
DriveChart, DriveChart,
PerUserReactionsChart, PerUserReactionsChart,
HashtagChart,
PerUserFollowingChart, PerUserFollowingChart,
PerUserDriveChart, PerUserDriveChart,
ApRequestChart, ApRequestChart,
@ -437,7 +434,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$PerUserPvChart, $PerUserPvChart,
$DriveChart, $DriveChart,
$PerUserReactionsChart, $PerUserReactionsChart,
$HashtagChart,
$PerUserFollowingChart, $PerUserFollowingChart,
$PerUserDriveChart, $PerUserDriveChart,
$ApRequestChart, $ApRequestChart,
@ -559,7 +555,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
PerUserPvChart, PerUserPvChart,
DriveChart, DriveChart,
PerUserReactionsChart, PerUserReactionsChart,
HashtagChart,
PerUserFollowingChart, PerUserFollowingChart,
PerUserDriveChart, PerUserDriveChart,
ApRequestChart, ApRequestChart,
@ -680,7 +675,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$PerUserPvChart, $PerUserPvChart,
$DriveChart, $DriveChart,
$PerUserReactionsChart, $PerUserReactionsChart,
$HashtagChart,
$PerUserFollowingChart, $PerUserFollowingChart,
$PerUserDriveChart, $PerUserDriveChart,
$ApRequestChart, $ApRequestChart,

View File

@ -26,7 +26,7 @@ export class CreateNotificationService {
private notificationEntityService: NotificationEntityService, private notificationEntityService: NotificationEntityService,
private idService: IdService, private idService: IdService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService, private pushNotificationService: PushNotificationService,
) { ) {
} }
@ -60,7 +60,7 @@ export class CreateNotificationService {
const packed = await this.notificationEntityService.pack(notification, {}); const packed = await this.notificationEntityService.pack(notification, {});
// Publish notification event // Publish notification event
this.globalEventServie.publishMainStream(notifieeId, 'notification', packed); this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(async () => { setTimeout(async () => {
@ -77,7 +77,7 @@ export class CreateNotificationService {
} }
//#endregion //#endregion
this.globalEventServie.publishMainStream(notifieeId, 'unreadNotification', packed); this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));

View File

@ -120,7 +120,7 @@ export class CustomEmojiService {
const url = isLocal const url = isLocal
? emojiUrl ? emojiUrl
: this.config.proxyRemoteFiles : this.config.proxyRemoteFiles
? `${this.config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}` ? `${this.config.mediaProxy}/emoji.webp?${query({ url: emojiUrl })}`
: emojiUrl; : emojiUrl;
return url; return url;

View File

@ -14,7 +14,7 @@ export class DeleteAccountService {
private userSuspendService: UserSuspendService, private userSuspendService: UserSuspendService,
private queueService: QueueService, private queueService: QueueService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
} }
@ -38,6 +38,6 @@ export class DeleteAccountService {
}); });
// Terminate streaming // Terminate streaming
this.globalEventServie.publishUserEvent(user.id, 'terminate', {}); this.globalEventService.publishUserEvent(user.id, 'terminate', {});
} }
} }

View File

@ -60,6 +60,7 @@ export class DownloadService {
retry: { retry: {
limit: 0, limit: 0,
}, },
enableUnixSockets: false,
}).on('response', (res: Got.Response) => { }).on('response', (res: Got.Response) => {
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) { if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) {
if (this.isPrivateIp(res.ip)) { if (this.isPrivateIp(res.ip)) {

View File

@ -4,7 +4,6 @@ import type { User } from '@/models/entities/User.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { Hashtag } from '@/models/entities/Hashtag.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 type { HashtagsRepository, UsersRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -20,7 +19,6 @@ export class HashtagService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private hashtagChart: HashtagChart,
) { ) {
} }
@ -143,9 +141,5 @@ export class HashtagService {
} as Hashtag); } as Hashtag);
} }
} }
if (!isUserAttached) {
this.hashtagChart.update(tag, user);
}
} }
} }

View File

@ -1,5 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as SyslogPro from 'syslog-pro';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import Logger from '@/logger.js'; import Logger from '@/logger.js';
@ -8,29 +7,14 @@ import type { KEYWORD } from 'color-convert/conversions';
@Injectable() @Injectable()
export class LoggerService { export class LoggerService {
private syslogClient;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: 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 @bindThis
public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) { public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
return new Logger(domain, color, store, this.syslogClient); return new Logger(domain, color, store);
} }
} }

View File

@ -175,7 +175,7 @@ export class NoteCreateService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private idService: IdService, private idService: IdService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private queueService: QueueService, private queueService: QueueService,
private noteReadService: NoteReadService, private noteReadService: NoteReadService,
private createNotificationService: CreateNotificationService, private createNotificationService: CreateNotificationService,
@ -535,7 +535,7 @@ export class NoteCreateService {
// Pack the note // Pack the note
const noteObj = await this.noteEntityService.pack(note); const noteObj = await this.noteEntityService.pack(note);
this.globalEventServie.publishNotesStream(noteObj); this.globalEventService.publishNotesStream(noteObj);
this.webhookService.getActiveWebhooks().then(webhooks => { this.webhookService.getActiveWebhooks().then(webhooks => {
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note')); webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
@ -561,7 +561,7 @@ export class NoteCreateService {
if (!threadMuted) { if (!threadMuted) {
nm.push(data.reply.userId, 'reply'); 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')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@ -584,7 +584,7 @@ export class NoteCreateService {
// Publish event // Publish event
if ((user.id !== data.renote.userId) && data.renote.userHost === null) { 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')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@ -684,7 +684,7 @@ export class NoteCreateService {
detail: true, 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')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
for (const webhook of webhooks) { for (const webhook of webhooks) {

View File

@ -34,7 +34,7 @@ export class NoteDeleteService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private relayService: RelayService, private relayService: RelayService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
@ -63,7 +63,7 @@ export class NoteDeleteService {
} }
if (!quiet) { if (!quiet) {
this.globalEventServie.publishNoteStream(note.id, 'deleted', { this.globalEventService.publishNoteStream(note.id, 'deleted', {
deletedAt: deletedAt, deletedAt: deletedAt,
}); });

View File

@ -40,7 +40,7 @@ export class NoteReadService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private notificationService: NotificationService, private notificationService: NotificationService,
private antennaService: AntennaService, private antennaService: AntennaService,
private pushNotificationService: PushNotificationService, private pushNotificationService: PushNotificationService,
@ -87,13 +87,13 @@ export class NoteReadService {
if (exist == null) return; if (exist == null) return;
if (params.isMentioned) { if (params.isMentioned) {
this.globalEventServie.publishMainStream(userId, 'unreadMention', note.id); this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
} }
if (params.isSpecified) { if (params.isSpecified) {
this.globalEventServie.publishMainStream(userId, 'unreadSpecifiedNote', note.id); this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
} }
if (note.channelId) { if (note.channelId) {
this.globalEventServie.publishMainStream(userId, 'unreadChannel', note.id); this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
} }
}, 2000); }, 2000);
} }
@ -155,7 +155,7 @@ export class NoteReadService {
}).then(mentionsCount => { }).then(mentionsCount => {
if (mentionsCount === 0) { if (mentionsCount === 0) {
// 全て既読になったイベントを発行 // 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllUnreadMentions'); this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
} }
}); });
@ -165,7 +165,7 @@ export class NoteReadService {
}).then(specifiedCount => { }).then(specifiedCount => {
if (specifiedCount === 0) { if (specifiedCount === 0) {
// 全て既読になったイベントを発行 // 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
} }
}); });
@ -175,7 +175,7 @@ export class NoteReadService {
}).then(channelNoteCount => { }).then(channelNoteCount => {
if (channelNoteCount === 0) { if (channelNoteCount === 0) {
// 全て既読になったイベントを発行 // 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllChannels'); this.globalEventService.publishMainStream(userId, 'readAllChannels');
} }
}); });
@ -200,14 +200,14 @@ export class NoteReadService {
}); });
if (count === 0) { if (count === 0) {
this.globalEventServie.publishMainStream(userId, 'readAntenna', antenna); this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id }); this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
} }
} }
this.userEntityService.getHasUnreadAntenna(userId).then(unread => { this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
if (!unread) { if (!unread) {
this.globalEventServie.publishMainStream(userId, 'readAllAntennas'); this.globalEventService.publishMainStream(userId, 'readAllAntennas');
this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined); this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
} }
}); });

View File

@ -1,17 +1,17 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Not } from 'typeorm'; import { Not } from 'typeorm';
import { DI } from '@/di-symbols.js'; 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 type { Note } from '@/models/entities/Note.js';
import { RelayService } from '@/core/RelayService.js'; import { RelayService } from '@/core/RelayService.js';
import type { CacheableUser } from '@/models/entities/User.js'; import type { CacheableUser } from '@/models/entities/User.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
@Injectable() @Injectable()
export class PollService { export class PollService {
@ -28,14 +28,11 @@ export class PollService {
@Inject(DI.pollVotesRepository) @Inject(DI.pollVotesRepository)
private pollVotesRepository: PollVotesRepository, private pollVotesRepository: PollVotesRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private relayService: RelayService, private relayService: RelayService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private createNotificationService: CreateNotificationService, private userBlockingService: UserBlockingService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
) { ) {
@ -52,11 +49,8 @@ export class PollService {
// Check blocking // Check blocking
if (note.userId !== user.id) { if (note.userId !== user.id) {
const block = await this.blockingsRepository.findOneBy({ const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
blockerId: note.userId, if (blocked) {
blockeeId: user.id,
});
if (block) {
throw new Error('blocked'); throw new Error('blocked');
} }
} }
@ -88,7 +82,7 @@ export class PollService {
const index = choice + 1; // In SQL, array index is 1 based 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}'`); 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, choice: choice,
userId: user.id, userId: user.id,
}); });

View File

@ -1,10 +1,10 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm'; import { Brackets, ObjectLiteral } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository } from '@/models/index.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 { bindThis } from '@/decorators.js';
import type { SelectQueryBuilder } from 'typeorm';
@Injectable() @Injectable()
export class QueryService { export class QueryService {
@ -32,7 +32,7 @@ export class QueryService {
) { ) {
} }
public makePaginationQuery<T>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder<T> { public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder<T> {
if (sinceId && untilId) { if (sinceId && untilId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });

View File

@ -18,7 +18,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.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<string, string> = { const legacies: Record<string, string> = {
'like': '👍', 'like': '👍',
@ -73,8 +74,9 @@ export class ReactionService {
private metaService: MetaService, private metaService: MetaService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private userBlockingService: UserBlockingService,
private idService: IdService, private idService: IdService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private createNotificationService: CreateNotificationService, 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) { public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string) {
// Check blocking // Check blocking
if (note.userId !== user.id) { if (note.userId !== user.id) {
const block = await this.blockingsRepository.findOneBy({ const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
blockerId: note.userId, if (blocked) {
blockeeId: user.id,
});
if (block) {
throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7'); throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
} }
} }
@ -157,7 +156,7 @@ export class ReactionService {
select: ['name', 'host', 'originalUrl', 'publicUrl'], select: ['name', 'host', 'originalUrl', 'publicUrl'],
}); });
this.globalEventServie.publishNoteStream(note.id, 'reacted', { this.globalEventService.publishNoteStream(note.id, 'reacted', {
reaction: decodedReaction.reaction, reaction: decodedReaction.reaction,
emoji: emoji != null ? { emoji: emoji != null ? {
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`, 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); 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, reaction: this.decodeReaction(exist.reaction).reaction,
userId: user.id, userId: user.id,
}); });

View File

@ -202,6 +202,19 @@ export class RoleService implements OnApplicationShutdown {
return [...assignedRoles, ...matchedCondRoles]; 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 @bindThis
public async getUserPolicies(userId: User['id'] | null): Promise<RolePolicies> { public async getUserPolicies(userId: User['id'] | null): Promise<RolePolicies> {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();

View File

@ -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 { IdService } from '@/core/IdService.js';
import type { CacheableUser, User } from '@/models/entities/User.js'; import type { CacheableUser, User } from '@/models/entities/User.js';
import type { Blocking } from '@/models/entities/Blocking.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 { GlobalEventService } from '@/core/GlobalEventService.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { DI } from '@/di-symbols.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 type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
import Logger from '@/logger.js'; import Logger from '@/logger.js';
import { UserEntityService } from '@/core/entities/UserEntityService.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 { LoggerService } from '@/core/LoggerService.js';
import { WebhookService } from '@/core/WebhookService.js'; import { WebhookService } from '@/core/WebhookService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { Cache } from '@/misc/cache.js';
import { StreamMessages } from '@/server/api/stream/types.js';
@Injectable() @Injectable()
export class UserBlockingService { export class UserBlockingService implements OnApplicationShutdown {
private logger: Logger; private logger: Logger;
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
private blockingsByUserIdCache: Cache<User['id'][]>;
constructor( constructor(
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -42,13 +50,44 @@ export class UserBlockingService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private queueService: QueueService, private queueService: QueueService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private webhookService: WebhookService, private webhookService: WebhookService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private loggerService: LoggerService, private loggerService: LoggerService,
) { ) {
this.logger = this.loggerService.getLogger('user-block'); this.logger = this.loggerService.getLogger('user-block');
this.blockingsByUserIdCache = new Cache<User['id'][]>(Infinity);
this.redisSubscriber.on('message', this.onMessage);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
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 @bindThis
@ -72,6 +111,11 @@ export class UserBlockingService {
await this.blockingsRepository.insert(blocking); await this.blockingsRepository.insert(blocking);
this.globalEventService.publishInternalEvent('blockingCreated', {
blockerId: blocker.id,
blockeeId: blockee.id,
});
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking)); const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking));
this.queueService.deliver(blocker, content, blockee.inbox); this.queueService.deliver(blocker, content, blockee.inbox);
@ -97,15 +141,15 @@ export class UserBlockingService {
if (this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(followee, followee, { this.userEntityService.pack(followee, followee, {
detail: true, 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)) { if (this.userEntityService.isLocalUser(follower)) {
this.userEntityService.pack(followee, follower, { this.userEntityService.pack(followee, follower, {
detail: true, detail: true,
}).then(async packed => { }).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventServie.publishMainStream(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')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@ -152,8 +196,8 @@ export class UserBlockingService {
this.userEntityService.pack(followee, follower, { this.userEntityService.pack(followee, follower, {
detail: true, detail: true,
}).then(async packed => { }).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventServie.publishMainStream(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')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@ -210,10 +254,31 @@ export class UserBlockingService {
await this.blockingsRepository.delete(blocking.id); await this.blockingsRepository.delete(blocking.id);
this.globalEventService.publishInternalEvent('blockingDeleted', {
blockerId: blocker.id,
blockeeId: blockee.id,
});
// deliver if remote bloking // deliver if remote bloking
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker)); const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
this.queueService.deliver(blocker, content, blockee.inbox); this.queueService.deliver(blocker, content, blockee.inbox);
} }
} }
@bindThis
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
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);
}
} }

View File

@ -12,10 +12,11 @@ import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { WebhookService } from '@/core/WebhookService.js'; import { WebhookService } from '@/core/WebhookService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { CreateNotificationService } from '@/core/CreateNotificationService.js';
import { DI } from '@/di-symbols.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import Logger from '../logger.js'; import Logger from '../logger.js';
const logger = new Logger('following/create'); const logger = new Logger('following/create');
@ -48,21 +49,18 @@ export class UserFollowingService {
@Inject(DI.followRequestsRepository) @Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository, private followRequestsRepository: FollowRequestsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.instancesRepository) @Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository, private instancesRepository: InstancesRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private userBlockingService: UserBlockingService,
private idService: IdService, private idService: IdService,
private queueService: QueueService, private queueService: QueueService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private createNotificationService: CreateNotificationService, private createNotificationService: CreateNotificationService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private webhookService: WebhookService, private webhookService: WebhookService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private globalEventService: GlobalEventService,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
) { ) {
@ -77,14 +75,8 @@ export class UserFollowingService {
// check blocking // check blocking
const [blocking, blocked] = await Promise.all([ const [blocking, blocked] = await Promise.all([
this.blockingsRepository.findOneBy({ this.userBlockingService.checkBlocked(follower.id, followee.id),
blockerId: follower.id, this.userBlockingService.checkBlocked(followee.id, follower.id),
blockeeId: followee.id,
}),
this.blockingsRepository.findOneBy({
blockerId: followee.id,
blockeeId: follower.id,
}),
]); ]);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) {
@ -94,11 +86,11 @@ export class UserFollowingService {
return; return;
} else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) { } else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) {
// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。 // リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
await this.blockingsRepository.delete(blocking.id); await this.userBlockingService.unblock(follower, followee);
} else { } else {
// それ以外は単純に例外 // それ以外は単純に例外
if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking'); if (blocking) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking');
if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked'); if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
} }
const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id }); const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id });
@ -227,8 +219,8 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, follower, { this.userEntityService.pack(followee.id, follower, {
detail: true, detail: true,
}).then(async packed => { }).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
this.globalEventServie.publishMainStream(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')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@ -242,7 +234,7 @@ export class UserFollowingService {
// Publish followed event // Publish followed event
if (this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(follower.id, followee).then(async packed => { 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')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@ -288,8 +280,8 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, follower, { this.userEntityService.pack(followee.id, follower, {
detail: true, detail: true,
}).then(async packed => { }).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventServie.publishMainStream(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')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@ -357,18 +349,12 @@ export class UserFollowingService {
// check blocking // check blocking
const [blocking, blocked] = await Promise.all([ const [blocking, blocked] = await Promise.all([
this.blockingsRepository.findOneBy({ this.userBlockingService.checkBlocked(follower.id, followee.id),
blockerId: follower.id, this.userBlockingService.checkBlocked(followee.id, follower.id),
blockeeId: followee.id,
}),
this.blockingsRepository.findOneBy({
blockerId: followee.id,
blockeeId: follower.id,
}),
]); ]);
if (blocking != null) throw new Error('blocking'); if (blocking) throw new Error('blocking');
if (blocked != null) throw new Error('blocked'); if (blocked) throw new Error('blocked');
const followRequest = await this.followRequestsRepository.insert({ const followRequest = await this.followRequestsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
@ -388,11 +374,11 @@ export class UserFollowingService {
// Publish receiveRequest event // Publish receiveRequest event
if (this.userEntityService.isLocalUser(followee)) { 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, { this.userEntityService.pack(followee.id, followee, {
detail: true, 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', { this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', {
@ -440,7 +426,7 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, followee, { this.userEntityService.pack(followee.id, followee, {
detail: true, detail: true,
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
} }
@bindThis @bindThis
@ -468,7 +454,7 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, followee, { this.userEntityService.pack(followee.id, followee, {
detail: true, detail: true,
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
} }
@bindThis @bindThis
@ -583,8 +569,8 @@ export class UserFollowingService {
detail: true, detail: true,
}); });
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packedFollowee); this.globalEventService.publishUserEvent(follower.id, 'unfollow', packedFollowee);
this.globalEventServie.publishMainStream(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')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) { for (const webhook of webhooks) {

View File

@ -25,7 +25,7 @@ export class UserListService {
private idService: IdService, private idService: IdService,
private userFollowingService: UserFollowingService, private userFollowingService: UserFollowingService,
private roleService: RoleService, private roleService: RoleService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private proxyAccountService: ProxyAccountService, private proxyAccountService: ProxyAccountService,
) { ) {
} }
@ -46,7 +46,7 @@ export class UserListService {
userListId: list.id, userListId: list.id,
} as UserListJoining); } 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)) { if (this.userEntityService.isRemoteUser(target)) {

View File

@ -18,7 +18,7 @@ export class UserMutingService {
private idService: IdService, private idService: IdService,
private queueService: QueueService, private queueService: QueueService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
} }

View File

@ -274,7 +274,7 @@ export class ApRendererService {
} as any; } as any;
if (reaction.startsWith(':')) { if (reaction.startsWith(':')) {
const name = reaction.replace(/:/g, ''); const name = reaction.replaceAll(':', '');
const emoji = await this.emojisRepository.findOneBy({ const emoji = await this.emojisRepository.findOneBy({
name, name,
host: IsNull(), host: IsNull(),

View File

@ -48,6 +48,10 @@ export class ApImageService {
throw new Error('invalid image: url not privided'); 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}`); this.logger.info(`Creating the Image: ${image.url}`);
const instance = await this.metaService.fetch(); const instance = await this.metaService.fetch();

View File

@ -1,8 +1,7 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { forwardRef, Inject, Injectable } from '@nestjs/common';
import promiseLimit from 'promise-limit'; import promiseLimit from 'promise-limit';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { MessagingMessagesRepository, PollsRepository, EmojisRepository } from '@/models/index.js'; import type { MessagingMessagesRepository, PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js';
import type { UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { CacheableRemoteUser } from '@/models/entities/User.js'; import type { CacheableRemoteUser } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.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 { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { MessagingService } from '@/core/MessagingService.js'; import { MessagingService } from '@/core/MessagingService.js';
import { bindThis } from '@/decorators.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports // eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { ApLoggerService } from '../ApLoggerService.js'; import { ApLoggerService } from '../ApLoggerService.js';
@ -32,7 +32,6 @@ import { ApQuestionService } from './ApQuestionService.js';
import { ApImageService } from './ApImageService.js'; import { ApImageService } from './ApImageService.js';
import type { Resolver } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js';
import type { IObject, IPost } from '../type.js'; import type { IObject, IPost } from '../type.js';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class ApNoteService { export class ApNoteService {
@ -134,6 +133,16 @@ export class ApNoteService {
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); 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}`); this.logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ // 投稿者をフェッチ
@ -307,7 +316,7 @@ export class ApNoteService {
apEmojis, apEmojis,
poll, poll,
uri: note.id, uri: note.id,
url: getOneApHrefNullable(note.url), url: url,
}, silent); }, silent);
} }

View File

@ -29,6 +29,7 @@ import { UserNotePining } from '@/models/entities/UserNotePining.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import type { UtilityService } from '@/core/UtilityService.js'; import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.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 { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js'; import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
@ -43,37 +44,6 @@ import type { IActor, IObject, IApPropertyValue } from '../type.js';
const nameLength = 128; const nameLength = 128;
const summaryLength = 2048; 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() @Injectable()
export class ApPersonService implements OnModuleInit { export class ApPersonService implements OnModuleInit {
private utilityService: UtilityService; 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 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 // Create user
let user: IRemoteUser; let user: IRemoteUser;
try { try {
@ -313,7 +289,7 @@ export class ApPersonService implements OnModuleInit {
await transactionalEntityManager.save(new UserProfile({ await transactionalEntityManager.save(new UserProfile({
userId: user.id, userId: user.id,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
url: getOneApHrefNullable(person.url), url: url,
fields, fields,
birthday: bday ? bday[0] : null, birthday: bday ? bday[0] : null,
location: person['vcard:Address'] ?? 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 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 = { const updates = {
lastFetchedAt: new Date(), lastFetchedAt: new Date(),
inbox: person.inbox, inbox: person.inbox,
@ -489,7 +471,7 @@ export class ApPersonService implements OnModuleInit {
} }
await this.userProfilesRepository.update({ userId: exist.id }, { await this.userProfilesRepository.update({ userId: exist.id }, {
url: getOneApHrefNullable(person.url), url: url,
fields, fields,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
birthday: bday ? bday[0] : null, birthday: bday ? bday[0] : null,
@ -540,22 +522,16 @@ export class ApPersonService implements OnModuleInit {
name: string, name: string,
value: string value: string
}[] = []; }[] = [];
const services: { [x: string]: any } = {};
if (Array.isArray(attachments)) { if (Array.isArray(attachments)) {
for (const attachment of attachments.filter(isPropertyValue)) { for (const attachment of attachments.filter(isPropertyValue)) {
if (isPropertyValue(attachment.identifier)) {
addService(services, attachment.identifier);
} else {
fields.push({ fields.push({
name: attachment.name, name: attachment.name,
value: this.mfmService.fromHtml(attachment.value), value: this.mfmService.fromHtml(attachment.value),
}); });
} }
} }
}
return { fields, services }; return { fields };
} }
@bindThis @bindThis

View File

@ -10,7 +10,6 @@ import PerUserNotesChart from './charts/per-user-notes.js';
import PerUserPvChart from './charts/per-user-pv.js'; import PerUserPvChart from './charts/per-user-pv.js';
import DriveChart from './charts/drive.js'; import DriveChart from './charts/drive.js';
import PerUserReactionsChart from './charts/per-user-reactions.js'; import PerUserReactionsChart from './charts/per-user-reactions.js';
import HashtagChart from './charts/hashtag.js';
import PerUserFollowingChart from './charts/per-user-following.js'; import PerUserFollowingChart from './charts/per-user-following.js';
import PerUserDriveChart from './charts/per-user-drive.js'; import PerUserDriveChart from './charts/per-user-drive.js';
import ApRequestChart from './charts/ap-request.js'; import ApRequestChart from './charts/ap-request.js';
@ -31,7 +30,6 @@ export class ChartManagementService implements OnApplicationShutdown {
private perUserPvChart: PerUserPvChart, private perUserPvChart: PerUserPvChart,
private driveChart: DriveChart, private driveChart: DriveChart,
private perUserReactionsChart: PerUserReactionsChart, private perUserReactionsChart: PerUserReactionsChart,
private hashtagChart: HashtagChart,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private perUserDriveChart: PerUserDriveChart, private perUserDriveChart: PerUserDriveChart,
private apRequestChart: ApRequestChart, private apRequestChart: ApRequestChart,
@ -46,7 +44,6 @@ export class ChartManagementService implements OnApplicationShutdown {
this.perUserPvChart, this.perUserPvChart,
this.driveChart, this.driveChart,
this.perUserReactionsChart, this.perUserReactionsChart,
this.hashtagChart,
this.perUserFollowingChart, this.perUserFollowingChart,
this.perUserDriveChart, this.perUserDriveChart,
this.apRequestChart, this.apRequestChart,

View File

@ -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);

View File

@ -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<typeof schema> {
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<Partial<KVs<typeof schema>>> {
return {};
}
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@bindThis
public async update(hashtag: string, user: { id: User['id'], host: User['host'] }): Promise<void> {
await this.commit({
'local.users': this.userEntityService.isLocalUser(user) ? [user.id] : [],
'remote.users': this.userEntityService.isLocalUser(user) ? [] : [user.id],
}, hashtag);
}
}

View File

@ -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 PerUserPvChart } from './charts/entities/per-user-pv.js';
import { entity as DriveChart } from './charts/entities/drive.js'; import { entity as DriveChart } from './charts/entities/drive.js';
import { entity as PerUserReactionsChart } from './charts/entities/per-user-reactions.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 PerUserFollowingChart } from './charts/entities/per-user-following.js';
import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js'; import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js';
import { entity as ApRequestChart } from './charts/entities/ap-request.js'; import { entity as ApRequestChart } from './charts/entities/ap-request.js';
@ -27,7 +26,6 @@ export const entities = [
PerUserPvChart.hour, PerUserPvChart.day, PerUserPvChart.hour, PerUserPvChart.day,
DriveChart.hour, DriveChart.day, DriveChart.hour, DriveChart.day,
PerUserReactionsChart.hour, PerUserReactionsChart.day, PerUserReactionsChart.hour, PerUserReactionsChart.day,
HashtagChart.hour, HashtagChart.day,
PerUserFollowingChart.hour, PerUserFollowingChart.day, PerUserFollowingChart.hour, PerUserFollowingChart.day,
PerUserDriveChart.hour, PerUserDriveChart.day, PerUserDriveChart.hour, PerUserDriveChart.day,
ApRequestChart.hour, ApRequestChart.day, ApRequestChart.hour, ApRequestChart.day,

View File

@ -54,7 +54,7 @@ export class ChannelEntityService {
name: channel.name, name: channel.name,
description: channel.description, description: channel.description,
userId: channel.userId, userId: channel.userId,
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner, false) : null, bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
usersCount: channel.usersCount, usersCount: channel.usersCount,
notesCount: channel.notesCount, notesCount: channel.notesCount,

View File

@ -20,6 +20,7 @@ type PackOptions = {
withUser?: boolean, withUser?: boolean,
}; };
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
@Injectable() @Injectable()
export class DriveFileEntityService { export class DriveFileEntityService {
@ -71,27 +72,42 @@ export class DriveFileEntityService {
} }
@bindThis @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) { if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
return appendQuery(this.config.mediaProxy, query({ if (!(mode === 'static' && file.type.startsWith('video'))) {
url: file.uri, return proxiedUrl(file.uri);
thumbnail: thumbnail ? '1' : undefined, }
}));
} }
// リモートかつ期限切れはローカルプロキシを試みる // リモートかつ期限切れはローカルプロキシを試みる
if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { 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('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外 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 @bindThis
@ -166,8 +182,8 @@ export class DriveFileEntityService {
isSensitive: file.isSensitive, isSensitive: file.isSensitive,
blurhash: file.blurhash, blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file), properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false), url: opts.self ? file.url : this.getPublicUrl(file),
thumbnailUrl: this.getPublicUrl(file, true), thumbnailUrl: this.getPublicUrl(file, 'static'),
comment: file.comment, comment: file.comment,
folderId: file.folderId, folderId: file.folderId,
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
@ -201,8 +217,8 @@ export class DriveFileEntityService {
isSensitive: file.isSensitive, isSensitive: file.isSensitive,
blurhash: file.blurhash, blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file), properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false), url: opts.self ? file.url : this.getPublicUrl(file),
thumbnailUrl: this.getPublicUrl(file, true), thumbnailUrl: this.getPublicUrl(file, 'static'),
comment: file.comment, comment: file.comment,
folderId: file.folderId, folderId: file.folderId,
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {

View File

@ -56,11 +56,13 @@ export class RoleEntityService {
name: role.name, name: role.name,
description: role.description, description: role.description,
color: role.color, color: role.color,
iconUrl: role.iconUrl,
target: role.target, target: role.target,
condFormula: role.condFormula, condFormula: role.condFormula,
isPublic: role.isPublic, isPublic: role.isPublic,
isAdministrator: role.isAdministrator, isAdministrator: role.isAdministrator,
isModerator: role.isModerator, isModerator: role.isModerator,
asBadge: role.asBadge,
canEditMembersByModerator: role.canEditMembersByModerator, canEditMembersByModerator: role.canEditMembersByModerator,
policies: policies, policies: policies,
usersCount: assigns.length, usersCount: assigns.length,

View File

@ -314,10 +314,10 @@ export class UserEntityService implements OnModuleInit {
@bindThis @bindThis
public async getAvatarUrl(user: User): Promise<string> { public async getAvatarUrl(user: User): Promise<string> {
if (user.avatar) { 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) { } else if (user.avatarId) {
const avatar = await this.driveFilesRepository.findOneByOrFail({ id: 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 { } else {
return this.getIdenticonUrl(user.id); return this.getIdenticonUrl(user.id);
} }
@ -326,7 +326,7 @@ export class UserEntityService implements OnModuleInit {
@bindThis @bindThis
public getAvatarUrlSync(user: User): string { public getAvatarUrlSync(user: User): string {
if (user.avatar) { 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 { } else {
return this.getIdenticonUrl(user.id); return this.getIdenticonUrl(user.id);
} }
@ -415,6 +415,11 @@ export class UserEntityService implements OnModuleInit {
} : undefined) : undefined, } : undefined) : undefined,
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user), 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 ? { ...(opts.detail ? {
url: profile!.url, url: profile!.url,
@ -422,7 +427,7 @@ export class UserEntityService implements OnModuleInit {
createdAt: user.createdAt.toISOString(), createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.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, bannerBlurhash: user.banner?.blurhash ?? null,
isLocked: user.isLocked, isLocked: user.isLocked,
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote), isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
@ -454,6 +459,7 @@ export class UserEntityService implements OnModuleInit {
id: role.id, id: role.id,
name: role.name, name: role.name,
color: role.color, color: role.color,
iconUrl: role.iconUrl,
description: role.description, description: role.description,
isModerator: role.isModerator, isModerator: role.isModerator,
isAdministrator: role.isAdministrator, isAdministrator: role.isAdministrator,
@ -489,7 +495,6 @@ export class UserEntityService implements OnModuleInit {
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id), hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
hasUnreadNotification: this.getHasUnreadNotification(user.id), hasUnreadNotification: this.getHasUnreadNotification(user.id),
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
integrations: profile!.integrations,
mutedWords: profile!.mutedWords, mutedWords: profile!.mutedWords,
mutedInstances: profile!.mutedInstances, mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: profile!.mutingNotificationTypes, mutingNotificationTypes: profile!.mutingNotificationTypes,

View File

@ -5,7 +5,7 @@
* The getter will return a .bind version of the function * The getter will return a .bind version of the function
* and memoize the result against a symbol on the instance * 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; let fn = descriptor.value;
if (typeof fn !== 'function') { if (typeof fn !== 'function') {
@ -34,7 +34,7 @@ export function bindThis(target, key, descriptor) {
}); });
return boundFn; return boundFn;
}, },
set(value) { set(value: any) {
fn = value; fn = value;
}, },
}; };

View File

@ -17,15 +17,13 @@ export default class Logger {
private context: Context; private context: Context;
private parentLogger: Logger | null = null; private parentLogger: Logger | null = null;
private store: boolean; 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 = { this.context = {
name: context, name: context,
color: color, color: color,
}; };
this.store = store; this.store = store;
this.syslogClient = syslogClient;
} }
@bindThis @bindThis
@ -47,7 +45,7 @@ export default class Logger {
} }
const time = dateFormat(new Date(), 'HH:mm:ss'); const time = dateFormat(new Date(), 'HH:mm:ss');
const worker = cluster.isPrimary ? '*' : cluster.worker.id; const worker = cluster.isPrimary ? '*' : cluster.worker!.id;
const l = const l =
level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') : level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') :
level === 'warning' ? chalk.yellow('WARN') : level === 'warning' ? chalk.yellow('WARN') :
@ -69,20 +67,6 @@ export default class Logger {
console.log(important ? chalk.bold(log) : log); console.log(important ? chalk.bold(log) : log);
if (level === 'error' && data) console.log(data); 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 @bindThis

View File

@ -1,5 +1,7 @@
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
export class Cache<T> { export class Cache<T> {
public cache: Map<string | null, { date: number; value: T; }>; public cache: Map<string | null, { date: number; value: T; }>;
private lifetime: number; private lifetime: number;

View File

@ -51,7 +51,7 @@ export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
bg.addColorStop(0, bgColors[0]); bg.addColorStop(0, bgColors[0]);
bg.addColorStop(1, bgColors[1]); bg.addColorStop(1, bgColors[1]);
ctx.fillStyle = bg; ctx.fillStyle = bg as any;
ctx.beginPath(); ctx.beginPath();
ctx.fillRect(0, 0, size, size); ctx.fillRect(0, 0, size, size);

View File

@ -11,10 +11,9 @@ export class I18n<T extends Record<string, any>> {
// string にしているのは、ドット区切りでのパス指定を許可するため // string にしているのは、ドット区切りでのパス指定を許可するため
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
@bindThis
public t(key: string, args?: Record<string, any>): string { public t(key: string, args?: Record<string, any>): string {
try { 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) { if (args) {
for (const [k, v] of Object.entries(args)) { for (const [k, v] of Object.entries(args)) {

View File

@ -1,14 +1,14 @@
export function nyaize(text: string): string { export function nyaize(text: string): string {
return text return text
// ja-JP // ja-JP
.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ') .replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
// en-US // en-US
.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya') .replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya')
.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan') .replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan')
.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan') .replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan')
// ko-KR // ko-KR
.replace(/[나-낳]/g, match => String.fromCharCode( .replace(/[나-낳]/g, match => String.fromCharCode(
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0) match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
)) ))
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥') .replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥'); .replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');

View File

@ -279,57 +279,6 @@ export class Meta {
}) })
public swPrivateKey: string | null; 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', { @Column('varchar', {
length: 128, length: 128,
nullable: true, nullable: true,

View File

@ -102,6 +102,11 @@ export class Role {
}) })
public color: string | null; public color: string | null;
@Column('varchar', {
length: 512, nullable: true,
})
public iconUrl: string | null;
@Column('enum', { @Column('enum', {
enum: ['manual', 'conditional'], enum: ['manual', 'conditional'],
default: 'manual', default: 'manual',
@ -118,6 +123,12 @@ export class Role {
}) })
public isPublic: boolean; public isPublic: boolean;
// trueの場合ユーザー名の横にバッジとして表示
@Column('boolean', {
default: false,
})
public asBadge: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View File

@ -184,11 +184,6 @@ export class UserProfile {
@JoinColumn() @JoinColumn()
public pinnedPage: Page | null; public pinnedPage: Page | null;
@Column('jsonb', {
default: {},
})
public integrations: Record<string, any>;
@Index() @Index()
@Column('boolean', { @Column('boolean', {
default: false, select: false, default: false, select: false,

View File

@ -323,10 +323,6 @@ export const packedMeDetailedOnlySchema = {
type: 'boolean', type: 'boolean',
nullable: false, optional: false, nullable: false, optional: false,
}, },
integrations: {
type: 'object',
nullable: true, optional: false,
},
mutedWords: { mutedWords: {
type: 'array', type: 'array',
nullable: false, optional: false, nullable: false, optional: false,

View File

@ -197,7 +197,7 @@ export const entities = [
const log = process.env.NODE_ENV !== 'production'; const log = process.env.NODE_ENV !== 'production';
export function createPostgreDataSource(config: Config) { export function createPostgresDataSource(config: Config) {
return new DataSource({ return new DataSource({
type: 'postgres', type: 'postgres',
host: config.db.host, host: config.db.host,

View File

@ -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 PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
import DriveChart from '@/core/chart/charts/drive.js'; import DriveChart from '@/core/chart/charts/drive.js';
import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.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 PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js';
@ -37,7 +36,6 @@ export class CleanChartsProcessorService {
private perUserPvChart: PerUserPvChart, private perUserPvChart: PerUserPvChart,
private driveChart: DriveChart, private driveChart: DriveChart,
private perUserReactionsChart: PerUserReactionsChart, private perUserReactionsChart: PerUserReactionsChart,
private hashtagChart: HashtagChart,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private perUserDriveChart: PerUserDriveChart, private perUserDriveChart: PerUserDriveChart,
private apRequestChart: ApRequestChart, private apRequestChart: ApRequestChart,
@ -61,7 +59,6 @@ export class CleanChartsProcessorService {
this.perUserPvChart.clean(), this.perUserPvChart.clean(),
this.driveChart.clean(), this.driveChart.clean(),
this.perUserReactionsChart.clean(), this.perUserReactionsChart.clean(),
this.hashtagChart.clean(),
this.perUserFollowingChart.clean(), this.perUserFollowingChart.clean(),
this.perUserDriveChart.clean(), this.perUserDriveChart.clean(),
this.apRequestChart.clean(), this.apRequestChart.clean(),

View File

@ -12,9 +12,9 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import { createTemp, createTempDir } from '@/misc/create-temp.js'; import { createTemp, createTempDir } from '@/misc/create-temp.js';
import { DownloadService } from '@/core/DownloadService.js'; import { DownloadService } from '@/core/DownloadService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type Bull from 'bull'; import type Bull from 'bull';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class ExportCustomEmojisProcessorService { export class ExportCustomEmojisProcessorService {
@ -82,6 +82,10 @@ export class ExportCustomEmojisProcessorService {
}); });
for (const emoji of customEmojis) { 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 ext = mime.extension(emoji.type ?? 'image/png');
const fileName = emoji.name + (ext ? '.' + ext : ''); const fileName = emoji.name + (ext ? '.' + ext : '');
const emojiPath = path + '/' + fileName; const emojiPath = path + '/' + fileName;

View File

@ -81,6 +81,10 @@ export class ImportCustomEmojisProcessorService {
for (const record of meta.emojis) { for (const record of meta.emojis) {
if (!record.downloaded) continue; 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 emojiInfo = record.emoji;
const emojiPath = outputPath + '/' + record.fileName; const emojiPath = outputPath + '/' + record.fileName;
await this.emojisRepository.delete({ await this.emojisRepository.delete({

View File

@ -11,13 +11,12 @@ import InstanceChart from '@/core/chart/charts/instance.js';
import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js';
import DriveChart from '@/core/chart/charts/drive.js'; import DriveChart from '@/core/chart/charts/drive.js';
import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.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 PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type Bull from 'bull'; import type Bull from 'bull';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class ResyncChartsProcessorService { export class ResyncChartsProcessorService {
@ -35,7 +34,6 @@ export class ResyncChartsProcessorService {
private perUserNotesChart: PerUserNotesChart, private perUserNotesChart: PerUserNotesChart,
private driveChart: DriveChart, private driveChart: DriveChart,
private perUserReactionsChart: PerUserReactionsChart, private perUserReactionsChart: PerUserReactionsChart,
private hashtagChart: HashtagChart,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private perUserDriveChart: PerUserDriveChart, private perUserDriveChart: PerUserDriveChart,
private apRequestChart: ApRequestChart, private apRequestChart: ApRequestChart,

View File

@ -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 PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
import DriveChart from '@/core/chart/charts/drive.js'; import DriveChart from '@/core/chart/charts/drive.js';
import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.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 PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js';
@ -37,7 +36,6 @@ export class TickChartsProcessorService {
private perUserPvChart: PerUserPvChart, private perUserPvChart: PerUserPvChart,
private driveChart: DriveChart, private driveChart: DriveChart,
private perUserReactionsChart: PerUserReactionsChart, private perUserReactionsChart: PerUserReactionsChart,
private hashtagChart: HashtagChart,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private perUserDriveChart: PerUserDriveChart, private perUserDriveChart: PerUserDriveChart,
private apRequestChart: ApRequestChart, private apRequestChart: ApRequestChart,
@ -61,7 +59,6 @@ export class TickChartsProcessorService {
this.perUserPvChart.tick(false), this.perUserPvChart.tick(false),
this.driveChart.tick(false), this.driveChart.tick(false),
this.perUserReactionsChart.tick(false), this.perUserReactionsChart.tick(false),
this.hashtagChart.tick(false),
this.perUserFollowingChart.tick(false), this.perUserFollowingChart.tick(false),
this.perUserDriveChart.tick(false), this.perUserDriveChart.tick(false),
this.apRequestChart.tick(false), this.apRequestChart.tick(false),

View File

@ -1,3 +1,4 @@
import { IncomingMessage } from 'node:http';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import fastifyAccepts from '@fastify/accepts'; import fastifyAccepts from '@fastify/accepts';
import httpSignature from '@peertube/http-signature'; import httpSignature from '@peertube/http-signature';
@ -19,6 +20,7 @@ import { QueryService } from '@/core/QueryService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
import type { FindOptionsWhere } from 'typeorm'; import type { FindOptionsWhere } from 'typeorm';
@ -97,7 +99,8 @@ export class ActivityPubServerService {
return; return;
} }
this.queueService.inbox(request.body, signature); // TODO: request.bodyのバリデーション
this.queueService.inbox(request.body as IActivity, signature);
reply.code(202); reply.code(202);
} }
@ -413,20 +416,21 @@ export class ActivityPubServerService {
@bindThis @bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.addConstraintStrategy({ // addConstraintStrategy の型定義がおかしいため
(fastify.addConstraintStrategy as any)({
name: 'apOrHtml', name: 'apOrHtml',
storage() { storage() {
const store = {}; const store = {} as any;
return { return {
get(key) { get(key: string) {
return store[key] ?? null; return store[key] ?? null;
}, },
set(key, value) { set(key: string, value: any) {
store[key] = value; store[key] = value;
}, },
}; };
}, },
deriveConstraint(request, ctx) { deriveConstraint(request: IncomingMessage) {
const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]); const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]);
const isAp = typeof accepted === 'string' && !accepted.match(/html/); const isAp = typeof accepted === 'string' && !accepted.match(/html/);
return isAp ? 'ap' : 'html'; return isAp ? 'ap' : 'html';
@ -536,6 +540,7 @@ export class ActivityPubServerService {
return (this.apRendererService.renderActivity(this.apRendererService.renderKey(user, keypair))); return (this.apRendererService.renderActivity(this.apRendererService.renderKey(user, keypair)));
} else { } else {
reply.code(400); reply.code(400);
return;
} }
}); });

View File

@ -137,38 +137,42 @@ export class FileServerService {
try { try {
if (file.state === 'remote') { if (file.state === 'remote') {
const convertFile = async () => { let image: IImageStreamable | null = null;
if (file.fileRole === 'thumbnail') { if (file.fileRole === 'thumbnail') {
if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(file.mime)) { if (isMimeImage(file.mime, 'sharp-convertible-image')) {
return this.imageProcessingService.convertToWebpStream( reply.header('Cache-Control', 'max-age=31536000, immutable');
file.path,
498, const url = new URL(`${this.config.mediaProxy}/static.webp`);
280 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/')) { } else if (file.mime.startsWith('video/')) {
return await this.videoProcessingService.generateVideoThumbnail(file.path); image = await this.videoProcessingService.generateVideoThumbnail(file.path);
} }
} }
if (file.fileRole === 'webpublic') { if (file.fileRole === 'webpublic') {
if (['image/svg+xml'].includes(file.mime)) { if (['image/svg+xml'].includes(file.mime)) {
return this.imageProcessingService.convertToWebpStream( reply.header('Cache-Control', 'max-age=31536000, immutable');
file.path,
2048, const url = new URL(`${this.config.mediaProxy}/svg.webp`);
2048, url.searchParams.set('url', file.url);
{ ...webpDefault, lossless: true }
) file.cleanup();
return await reply.redirect(301, url.toString());
} }
} }
return { if (!image) {
image = {
data: fs.createReadStream(file.path), data: fs.createReadStream(file.path),
ext: file.ext, ext: file.ext,
type: file.mime, type: file.mime,
}; };
}; }
const image = await convertFile();
if ('pipe' in image.data && typeof image.data.pipe === 'function') { if ('pipe' in image.data && typeof image.data.pipe === 'function') {
// image.dataがstreamなら、stream終了後にcleanup // 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('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
return image.data; return image.data;
} }
@ -217,6 +220,23 @@ export class FileServerService {
return; 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 // Create temp file
const file = await this.getStreamAndTypeFromUrl(url); const file = await this.getStreamAndTypeFromUrl(url);
if (file === '404') { if (file === '404') {
@ -235,8 +255,21 @@ export class FileServerService {
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image'); const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image');
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-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; 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)) { if (!isAnimationConvertibleImage && !('static' in request.query)) {
image = { image = {
data: fs.createReadStream(file.path), data: fs.createReadStream(file.path),
@ -246,7 +279,7 @@ export class FileServerService {
} else { } else {
const data = sharp(file.path, { animated: !('static' in request.query) }) const data = sharp(file.path, { animated: !('static' in request.query) })
.resize({ .resize({
height: 128, height: 'emoji' in request.query ? 128 : 320,
withoutEnlargement: true, withoutEnlargement: true,
}) })
.webp(webpDefault); .webp(webpDefault);
@ -257,16 +290,11 @@ export class FileServerService {
type: 'image/webp', type: 'image/webp',
}; };
} }
} else if ('static' in request.query && isConvertibleImage) { } else if ('static' in request.query) {
image = this.imageProcessingService.convertToWebpStream(file.path, 498, 280); 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); image = this.imageProcessingService.convertToWebpStream(file.path, 200, 200);
} else if ('badge' in request.query) { } else if ('badge' in request.query) {
if (!isConvertibleImage) {
// 画像でないなら404でお茶を濁す
throw new StatusError('Unexpected mime', 404);
}
const mask = sharp(file.path) const mask = sharp(file.path)
.resize(96, 96, { .resize(96, 96, {
fit: 'inside', fit: 'inside',
@ -370,7 +398,7 @@ export class FileServerService {
@bindThis @bindThis
private async getFileFromKey(key: string): Promise< 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; } | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; }
| '404' | '404'
| '204' | '204'
@ -392,6 +420,7 @@ export class FileServerService {
const result = await this.downloadAndDetectTypeFromUrl(file.uri); const result = await this.downloadAndDetectTypeFromUrl(file.uri);
return { return {
...result, ...result,
url: file.uri,
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original', fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
file, file,
} }

View File

@ -111,9 +111,6 @@ export class NodeinfoServerService {
enableHcaptcha: meta.enableHcaptcha, enableHcaptcha: meta.enableHcaptcha,
enableRecaptcha: meta.enableRecaptcha, enableRecaptcha: meta.enableRecaptcha,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
enableTwitterIntegration: meta.enableTwitterIntegration,
enableGithubIntegration: meta.enableGithubIntegration,
enableDiscordIntegration: meta.enableDiscordIntegration,
enableEmail: meta.enableEmail, enableEmail: meta.enableEmail,
enableServiceWorker: meta.enableServiceWorker, enableServiceWorker: meta.enableServiceWorker,
proxyAccountName: proxyAccount ? proxyAccount.username : null, proxyAccountName: proxyAccount ? proxyAccount.username : null,

View File

@ -7,9 +7,6 @@ import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ServerService } from './ServerService.js'; import { ServerService } from './ServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js'; import { WellKnownServerService } from './WellKnownServerService.js';
import { GetterService } from './api/GetterService.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 { ChannelsService } from './api/stream/ChannelsService.js';
import { ActivityPubServerService } from './ActivityPubServerService.js'; import { ActivityPubServerService } from './ActivityPubServerService.js';
import { ApiLoggerService } from './api/ApiLoggerService.js'; import { ApiLoggerService } from './api/ApiLoggerService.js';
@ -54,9 +51,6 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
ServerService, ServerService,
WellKnownServerService, WellKnownServerService,
GetterService, GetterService,
DiscordServerService,
GithubServerService,
TwitterServerService,
ChannelsService, ChannelsService,
ApiCallService, ApiCallService,
ApiLoggerService, ApiLoggerService,

View File

@ -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なので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('emoji', '1'); url.searchParams.set('emoji', '1');
@ -166,6 +166,7 @@ export class ServerService {
return 'Verify succeeded!'; return 'Verify succeeded!';
} else { } else {
reply.code(404); reply.code(404);
return;
} }
}); });

View File

@ -12,9 +12,6 @@ import endpoints, { IEndpoint } from './endpoints.js';
import { ApiCallService } from './ApiCallService.js'; import { ApiCallService } from './ApiCallService.js';
import { SignupApiService } from './SignupApiService.js'; import { SignupApiService } from './SignupApiService.js';
import { SigninApiService } from './SigninApiService.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'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
@Injectable() @Injectable()
@ -38,9 +35,6 @@ export class ApiServerService {
private apiCallService: ApiCallService, private apiCallService: ApiCallService,
private signupApiService: SignupApiService, private signupApiService: SignupApiService,
private signinApiService: SigninApiService, private signinApiService: SigninApiService,
private githubServerService: GithubServerService,
private discordServerService: DiscordServerService,
private twitterServerService: TwitterServerService,
) { ) {
//this.createServer = this.createServer.bind(this); //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.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) => { fastify.get('/v1/instance/peers', async (request, reply) => {
const instances = await this.instancesRepository.find({ const instances = await this.instancesRepository.find({
select: ['host'], select: ['host'],

View File

@ -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_apRequest from './endpoints/charts/ap-request.js';
import * as ep___charts_drive from './endpoints/charts/drive.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_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_instance from './endpoints/charts/instance.js';
import * as ep___charts_notes from './endpoints/charts/notes.js'; import * as ep___charts_notes from './endpoints/charts/notes.js';
import * as ep___charts_user_drive from './endpoints/charts/user/drive.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_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_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_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_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_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 }; 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_apRequest,
$charts_drive, $charts_drive,
$charts_federation, $charts_federation,
$charts_hashtag,
$charts_instance, $charts_instance,
$charts_notes, $charts_notes,
$charts_user_drive, $charts_user_drive,
@ -1107,7 +1104,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$charts_apRequest, $charts_apRequest,
$charts_drive, $charts_drive,
$charts_federation, $charts_federation,
$charts_hashtag,
$charts_instance, $charts_instance,
$charts_notes, $charts_notes,
$charts_user_drive, $charts_user_drive,

View File

@ -34,7 +34,7 @@ export class RateLimiterService {
const min = (): void => { const min = (): void => {
const minIntervalLimiter = new Limiter({ const minIntervalLimiter = new Limiter({
id: `${actor}:${limitation.key}:min`, id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval * factor, duration: limitation.minInterval! * factor,
max: 1, max: 1,
db: this.redisClient, db: this.redisClient,
}); });
@ -62,8 +62,8 @@ export class RateLimiterService {
const max = (): void => { const max = (): void => {
const limiter = new Limiter({ const limiter = new Limiter({
id: `${actor}:${limitation.key}`, id: `${actor}:${limitation.key}`,
duration: limitation.duration * factor, duration: limitation.duration! * factor,
max: limitation.max / factor, max: limitation.max! / factor,
db: this.redisClient, db: this.redisClient,
}); });

View File

@ -10,9 +10,9 @@ import { getIpHash } from '@/misc/get-ip-hash.js';
import type { ILocalUser } from '@/models/entities/User.js'; import type { ILocalUser } from '@/models/entities/User.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js';
import { bindThis } from '@/decorators.js';
import { RateLimiterService } from './RateLimiterService.js'; import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js'; import { SigninService } from './SigninService.js';
import { bindThis } from '@/decorators.js';
import type { FastifyRequest, FastifyReply } from 'fastify'; import type { FastifyRequest, FastifyReply } from 'fastify';
@Injectable() @Injectable()
@ -131,7 +131,7 @@ export class SigninApiService {
createdAt: new Date(), createdAt: new Date(),
userId: user.id, userId: user.id,
ip: request.ip, ip: request.ip,
headers: request.headers, headers: request.headers as any,
success: false, success: false,
}); });

View File

@ -25,7 +25,7 @@ export class SigninService {
} }
@bindThis @bindThis
public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser, redirect = false) { public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser) {
setImmediate(async () => { setImmediate(async () => {
// Append signin history // Append signin history
const record = await this.signinsRepository.insert({ const record = await this.signinsRepository.insert({
@ -33,7 +33,7 @@ export class SigninService {
createdAt: new Date(), createdAt: new Date(),
userId: user.id, userId: user.id,
ip: request.ip, ip: request.ip,
headers: request.headers, headers: request.headers as any,
success: true, success: true,
}).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0]));
@ -41,19 +41,6 @@ export class SigninService {
this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record)); 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); reply.code(200);
return { return {
id: user.id, id: user.id,
@ -61,5 +48,4 @@ export class SigninService {
}; };
} }
} }
}

Some files were not shown because too many files have changed in this diff Show More