Compare commits

...

68 Commits

Author SHA1 Message Date
syuilo
9bde9edcf6 Merge branch 'develop' 2023-02-04 14:23:38 +09:00
syuilo
a12f07c42b 13.3.2 2023-02-04 14:23:29 +09:00
syuilo
e7334c4fb0 Update CHANGELOG.md 2023-02-04 14:21:08 +09:00
syuilo
38f9d1e764 fix(client): validate urls to improve security 2023-02-04 14:20:07 +09:00
tamaina
2dfed75402 perf(server): improvement of external mediaProxy (#9787)
* perf(server): improvement of external mediaProxy

* add a comment

* ✌️

* /filesでsharpの処理を行わずリダイレクトする

* fix

* thumbnail => static

* Fix #9788

* add avatar mode

* add url

* fix

* static.webp

* remove encodeURIComponent from media proxy path

* remove existance check
2023-02-04 13:38:51 +09:00
syuilo
0c12e80106 perf(server): cache blocking 2023-02-04 12:40:40 +09:00
syuilo
b7522f69e7 fix typo 2023-02-04 10:02:03 +09:00
syuilo
24705a7e39 Merge branch 'develop' 2023-02-04 09:12:26 +09:00
syuilo
8add8025a0 13.3.1 2023-02-04 09:12:18 +09:00
syuilo
32fa79d928 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-04 09:11:36 +09:00
syuilo
534be6ff25 Update CHANGELOG.md 2023-02-04 09:11:33 +09:00
syuilo
f684c07567 New Crowdin updates (#9789)
* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)
2023-02-04 09:10:16 +09:00
syuilo
788ae2f6ca fix(client): validate urls to improve security 2023-02-04 09:10:01 +09:00
syuilo
572000f868 clean up 2023-02-04 09:01:26 +09:00
Rox Squires
57f5df2d22 Fix | Vue-plyr CORS issue (#9790)
* Added Video player

Added vue-plyr as the video play

* Create node.js.yml

* Delete node.js.yml

* Added vue-plyr into pnpm-lock.yaml

* tweak

* Fixed the pnpm-lock.yaml

For some reason on the dependencies there was to instances of vue-plyr

* Added MkMediaAudio

* Update MkMediaList.vue

* CORS checks

* Update MkMediaVideo.vue

* Update MkMediaVideo.vue

* Fixed CORS

the property made the video player use the CORS policy that stopped instance not using media caching not able to load the video from remote instance

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-02-04 09:00:16 +09:00
tamaina
b2a67ba5ca fix(client): オートコンプリートでUnicode絵文字がカスタム絵文字として表示されてしまうのを修正 2023-02-03 21:21:36 +00:00
tamaina
d78e15cc1a fix(client): カスタム絵文字にアニメーション画像を再生しない設定が適用されていない問題を修正 2023-02-03 20:37:15 +00:00
syuilo
ceab34f5f3 Merge branch 'develop' 2023-02-03 17:57:40 +09:00
syuilo
3a62625bbc 13.3.0 2023-02-03 17:57:27 +09:00
Roxy Squires
ad6844ac4a Bug | Fixed the error when running pnpm i --frozen-lockfile (#9782)
* Added Video player

Added vue-plyr as the video play

* Create node.js.yml

* Delete node.js.yml

* Added vue-plyr into pnpm-lock.yaml

* tweak

* Fixed the pnpm-lock.yaml

For some reason on the dependencies there was to instances of vue-plyr

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-02-03 17:46:16 +09:00
syuilo
a8c252a613 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-03 17:44:28 +09:00
syuilo
1d39f785f1 perf: use replaceAll instead of regex 2023-02-03 17:44:25 +09:00
Roxy Squires
4b8b29b862 enhance - Added vue-plyr as the standard video player (#9766)
* Added Video player

Added vue-plyr as the video play

* Create node.js.yml

* Delete node.js.yml

* Added vue-plyr into pnpm-lock.yaml

* tweak

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-02-03 17:15:25 +09:00
tamaina
0d148bd23b Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-03 08:07:25 +00:00
tamaina
ebedb81e3f update idb-proxy.ts
Maybe fixed #9769
2023-02-03 08:07:17 +00:00
syuilo
d195406fdc New Crowdin updates (#9760)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Spanish)
2023-02-03 15:14:02 +09:00
syuilo
5173ed37f9 Update CHANGELOG.md 2023-02-03 15:13:28 +09:00
syuilo
825551d64f drop syslog
Close #9774
2023-02-03 15:08:36 +09:00
syuilo
449761bada Update CHANGELOG.md 2023-02-03 15:06:24 +09:00
syuilo
5859df389f Create 1675404035646-cleanup.js 2023-02-03 15:02:54 +09:00
syuilo
562b02310f drop twitter/github/discord integrations
Close #9775
2023-02-03 15:01:31 +09:00
syuilo
65ed702d87 update deps 2023-02-03 14:44:09 +09:00
syuilo
c559a9843f drop hashtag chart 2023-02-03 14:10:14 +09:00
syuilo
88c3957085 enhance(client): hidden ads when canHideAds is true 2023-02-03 14:03:34 +09:00
Masaya Suzuki
01778e11dc CONTRIBUTING: テストが配置されている場所の記述修正 (#9772) 2023-02-03 03:11:50 +09:00
Masaya Suzuki
9d9e8a3c4e CONTRIBUTING: yarn -> pnpm (#9771) 2023-02-03 03:11:26 +09:00
syuilo
ed3e035ad6 refactor: use test 2023-02-02 18:18:25 +09:00
syuilo
07f885fea8 refactor 2023-02-02 18:08:34 +09:00
syuilo
2cc98226ca improve RoleService test 2023-02-02 18:06:23 +09:00
tamaina
8a6f73c5ff enhance: PizzaxデータをindexedDBに保存するように (#9225)
* Revert "Revert #8098"

This reverts commit 8b9dc962ae.

* fix

* use deepClone instead of deepclone

* defaultStore.loaded

* fix load

* wait ready

* use top-level await, await in device-kind.ts
2023-02-02 16:43:56 +09:00
syuilo
00e3453ce1 improve role test 2023-02-02 14:28:29 +09:00
syuilo
16646dd77a Update README.md 2023-02-02 10:31:13 +09:00
syuilo
1f39d1fe26 test: add test of RoleService 2023-02-02 10:26:59 +09:00
syuilo
e8f3c587c9 Update pnpm-lock.yaml 2023-02-02 10:26:43 +09:00
syuilo
4b43745e7c fix(test): add @jest/globals 2023-02-02 10:26:29 +09:00
syuilo
9db2f60053 refactor(client): use top-level await 2023-02-02 09:00:34 +09:00
syuilo
4610d8dfe3 refactor: fix type 2023-02-01 20:15:11 +09:00
syuilo
fa296efdf6 refactor: fix type 2023-02-01 20:13:22 +09:00
syuilo
d9d98f84bf refactor: fix type 2023-02-01 20:12:42 +09:00
tamaina
7c3143b8e5 enhance(backend): enhance SchemaType handling of anyOf (#9762)
* enhance(backend): enhance anyOf handling

* clean up
2023-02-01 20:04:01 +09:00
syuilo
387fcd5c5d refactor: fix type 2023-02-01 17:29:28 +09:00
syuilo
ebc6437977 refactor: tweak variable name 2023-02-01 16:24:50 +09:00
syuilo
dbc23b5d20 Merge branch 'develop' 2023-02-01 11:29:30 +09:00
syuilo
843f1aed4f 13.2.6 2023-02-01 11:29:17 +09:00
syuilo
e42938cad6 Update CHANGELOG.md 2023-02-01 11:27:37 +09:00
YS
2a41f6c383 enhance: Unicode絵文字名逆引き効率化 (#9757)
* Unicode絵文字名前取得を連想配列で行う

* Unicode絵文字事前カテゴリ集計

* Mapを使用

* Update packages/frontend/src/scripts/emojilist.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
2023-02-01 11:25:13 +09:00
syuilo
671d21a2c1 New Crowdin updates (#9737)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)
2023-02-01 11:21:58 +09:00
syuilo
515692d7a6 update aiscript 2023-02-01 11:20:28 +09:00
Nya Candy
00d28826b9 fix(try): ld signature normalizer (#9758) 2023-01-31 19:37:39 +09:00
tamaina
5b38f76254 update CHANGELOG.md 2023-01-28 12:37:28 +00:00
tamaina
ca7dbd6010 gitignore docker-compose.yml 2023-01-28 11:34:34 +00:00
tamaina
133644e5a9 Rename docker-compose.yml to docker-compose.yml.example 2023-01-28 11:33:44 +00:00
tamaina
04d60426c7 modify CHANGELOG.md 2023-01-28 06:22:38 +00:00
tamaina
8282bbd07c fix(client): Chromeで検索ダイアログで変換確定するとそのまま検索されてしまう
Fix #9598
2023-01-28 06:15:29 +00:00
yupix
7190bd00c9 feat: classicモードでテーマが自動変更された際元に戻すように (#9669)
* feat: classicモードでテーマが自動変更された際元に戻すように

* docs: update CHANGELOG.md

* fix: prefixを miux:ui_temp から ui_temp に変更
2023-01-27 13:52:51 +09:00
syuilo
44b9539818 Merge branch 'develop' 2023-01-27 12:33:36 +09:00
syuilo
b2ed4c9508 13.2.5 2023-01-27 12:33:20 +09:00
syuilo
c7b5c8b19e swがビルドできないのを修正 2023-01-27 12:33:15 +09:00
143 changed files with 3844 additions and 4381 deletions

View File

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

View File

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

1
.gitignore vendored
View File

@@ -32,6 +32,7 @@ coverage
!/.config/example.yml
!/.config/docker_example.yml
!/.config/docker_example.env
docker-compose.yml
# misskey
/build

View File

@@ -9,6 +9,47 @@
You should also include the user name that made the change.
-->
## 13.3.2 (2023/02/04)
### Improvements
- 外部メディアプロキシへの対応を強化しました
外部メディアプロキシのFastify実装を作りました
https://github.com/misskey-dev/media-proxy
- Server: improve performance
### Bugfixes
- Client: validate urls to improve security
## 13.3.1 (2023/02/04)
### Bugfixes
- Client: カスタム絵文字にアニメーション画像を再生しない設定が適用されていない問題を修正
- Client: オートコンプリートでUnicode絵文字がカスタム絵文字として表示されてしまうのを修正
- Client: Fix Vue-plyr CORS issue
- Client: validate urls to improve security
## 13.3.0 (2023/02/03)
### Changes
- twitter/github/discord連携機能が削除されました
- ハッシュタグごとのチャートが削除されました
- syslogのサポートが削除されました
### Improvements
- ロールで広告の非表示が有効になっている場合は最初から広告を非表示にするように
## 13.2.6 (2023/02/01)
### Changes
- docker-compose.ymlをdocker-compose.yml.exampleにしました。docker-compose.ymlとしてコピーしてから使用してください。
### Improvements
- 絵文字ピッカーのパフォーマンスを改善
- AiScriptを0.12.4に更新
### Bugfixes
- Server: リレーと通信できない問題を修正
- Client: classicモード使用時にwindowサイズによってdefaultに変更された後に、windowサイズが元に戻ったらclassicに戻すように修正 #9669
- Client: Chromeで検索ダイアログで変換確定するとそのまま検索されてしまう問題を修正
## 13.2.4 (2023/01/27)
### Improvements
- リモートカスタム絵文字表示時のパフォーマンスを改善

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.
- If you have added a feature or fixed a bug, please add a test case if possible.
- Please make sure that tests and Lint are passed in advance.
- You can run it with `yarn test` and `yarn lint`. [See more info](#testing)
- You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing)
- If this PR includes UI changes, please attach a screenshot in the text.
Thanks for your cooperation 🤗
@@ -102,7 +102,7 @@ If your language is not listed in Crowdin, please open an issue.
During development, it is useful to use the
```
yarn dev
pnpm dev
```
command.
@@ -112,7 +112,7 @@ command.
- Service Worker is watched by esbuild.
## Testing
- Test codes are located in [`/test`](/test).
- Test codes are located in [`/packages/backend/test`](/packages/backend/test).
### Run test
Create a config file.
@@ -127,12 +127,12 @@ Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.y
Run all test.
```
yarn test
pnpm test
```
#### Run specify test
```
yarn jest -- foo.ts
pnpm jest -- foo.ts
```
### e2e tests
@@ -177,9 +177,9 @@ vue-routerとの最大の違いは、niraxは複数のルーターが存在す
これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
## Notes
### How to resolve conflictions occurred at yarn.lock?
### How to resolve conflictions occurred at pnpm-lock.yaml?
Just execute `yarn` to fix it.
Just execute `pnpm` to fix it.
### INSERTするときにはsaveではなくinsertを使用する
#6441
@@ -265,7 +265,7 @@ MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`
### Migration作成方法
packages/backendで:
```sh
yarn dlx typeorm migration:generate -d ormconfig.js -o <migration name>
pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
```
- 生成後、ファイルをmigration下に移してください

View File

@@ -24,6 +24,8 @@
---
[![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey)
</div>
<div>

View File

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

View File

@@ -68,7 +68,7 @@ export: "Export"
files: "Dateien"
download: "Herunterladen"
driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Notizen mit dieser Datei werden ebenso verschwinden."
unfollowConfirm: "Möchtest du {name} nicht mehr folgen?"
unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?"
exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt."
importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen."
lists: "Listen"
@@ -94,7 +94,7 @@ defaultNoteVisibility: "Standardsichtbarkeit"
follow: "Folgen"
followRequest: "Follow-Anfrage senden"
followRequests: "Follow-Anfragen"
unfollow: "Nicht mehr folgen"
unfollow: "Entfolgen"
followRequestPending: "Follow-Anfrage ausstehend"
enterEmoji: "Gib ein Emoji ein"
renote: "Renote"

View File

@@ -68,7 +68,7 @@ export: "Export"
files: "Files"
download: "Download"
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Notes with this file attached will also be deleted."
unfollowConfirm: "Are you sure that you want to unfollow {name}?"
unfollowConfirm: "Are you sure you want to unfollow {name}?"
exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed."
importRequested: "You've requested an import. This may take a while."
lists: "Lists"

View File

@@ -509,7 +509,7 @@ objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
serverLogs: "Registros del servidor"
deleteAll: "Eliminar todos"
showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo"
newNoteRecived: "Tienes una nota nuevo"
newNoteRecived: "Tienes una nota nueva"
sounds: "Sonidos"
sound: "Sonidos"
listen: "Escuchar"
@@ -918,17 +918,323 @@ tools: "Utilidades"
cannotLoad: "No se puede cargar."
numberOfProfileView: "Número de vistas de perfil"
like: "¡Muy bien!"
unlike: "Quitar 'me gusta'"
numberOfLikes: "Cantidad de 'Me gusta'"
show: "Apariencia"
neverShow: "No mostrar de nuevo"
remindMeLater: "Recordar después"
didYouLikeMisskey: "¿Te gusta Misskey?"
pleaseDonate: "Misskey es software libre, y es usado por {host} . Por favor, ¡considera donar al proyecto principal para que podamos continuar!"
roles: "Roles"
role: "Roles"
normalUser: "Usuario normal"
undefined: "Indefinido"
assign: "Asignar"
unassign: "Quitar"
color: "Color"
manageCustomEmojis: "Administrar emojis personalizados"
youCannotCreateAnymore: "Se alcanzó el límite de creación"
cannotPerformTemporary: "Indisponible temporalmente"
cannotPerformTemporaryDescription: "Esta acción no se puede realizar porque se excedió el límite de ejecución. Espera un poco y prueba de nuevo."
preset: "Predefinido"
selectFromPresets: "Escoger desde predefinidos"
achievements: "Logros"
_achievements:
earnedAt: "Desbloqueado el"
_types:
_notes1:
title: "¡Hola Misskey!"
description: "Publicaste tu primera nota"
flavor: "¡Pasándola bien con Misskey!"
_notes10:
title: "Algunas notas"
description: "10 notas publicadas"
_notes100:
title: "¡Muchas notas!"
description: "100 notas publicadas"
_notes500:
title: "¡Cubierto de notas!"
description: "500 notas publicadas"
_notes1000:
title: "¡Una montaña de notas!"
description: "1000 notas publicadas"
_notes5000:
title: "¡Exceso de notas!"
description: "5000 notas publicadas"
_notes10000:
title: "¡Súpernota!"
description: "10000 notas publicadas"
_notes20000:
title: "Necesito... Más... ¡Notas!"
description: "20000 notas publicadas"
_notes30000:
title: "¡Notas! ¡Notas! ¡Notas!"
description: "30000 notas publicadas"
_notes40000:
title: "Fábrica de notas"
description: "40000 notas publicadas"
_notes50000:
title: "¡Un planeta de notas!"
description: "50000 notas publicadas"
_notes60000:
title: "¡Un cuásar de notas!"
description: "60000 notas publicadas"
_notes70000:
title: "¡Un hoyo negro de notas!"
description: "70000 notas publicadas"
_notes80000:
title: "¡Una galaxia de notas!"
description: "80000 notas publicadas"
_notes90000:
title: "¡Todo un universo de notas!"
description: "90000 notas publicadas"
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "100000 notas publicadas"
flavor: "¿Tienes tanto para publicar?"
_login3:
title: "Principiante I"
description: "Días desde el inicio de sesión: 3"
flavor: "Desde hoy, soy Misskero"
_login7:
title: "Principiante II"
description: "Días desde el inicio de sesión: 7"
flavor: "¿Ya te acostumbraste?"
_login15:
title: "Principiante III"
description: "Días desde el inicio de sesión: 15"
_login30:
title: "Misskero I"
description: "Días desde el inicio de sesión: 30"
_login60:
title: "Misskero II"
description: "Días desde el inicio de sesión: 60"
_login100:
title: "Misskero III"
description: "Días desde el inicio de sesión: 100"
flavor: "Para este usuario, Misskaína"
_login200:
title: "Regular I"
description: "Días desde el inicio de sesión: 200"
_login300:
title: "Regular II"
description: "Días desde el inicio de sesión: 300"
_login400:
title: "Regular III"
description: "Días desde el inicio de sesión: 400"
_login500:
title: "Veterano I"
description: "Días desde el inicio de sesión: 500"
flavor: "Chicos, me encantan las libretas..."
_login600:
title: "Veterano II"
description: "Días desde el inicio de sesión: 600"
_login700:
title: "Veterano III"
description: "Días desde el inicio de sesión: 700"
_login800:
title: "Maestro I"
description: "Días desde el inicio de sesión: 800"
_login900:
title: "Maestro II"
description: "Días desde el inicio de sesión: 900"
_login1000:
title: "Maestro III"
description: "Días desde el inicio de sesión: 1000"
flavor: "¡Gracias por usar Misskey!"
_noteClipped1:
title: "No puedo evitar clipearte..."
description: "Hacer un clip por primera vez"
_noteFavorited1:
title: "Contemplando las estrellas"
description: "Poner una nota como favorito por primera vez"
_myNoteFavorited1:
title: "¡Quiero una estrella!"
description: "Tu nota ha sido marcada como favorito por primera vez"
_profileFilled:
title: "¡Listo!"
description: "Perfil completado"
_markedAsCat:
title: "Soy un gato"
description: "Configurar la cuenta como cuenta de un gato"
flavor: "Aún no tengo nombre"
_following1:
title: "Primera vez siguiendo a alguien"
description: "Seguir a un usuario"
_following10:
title: "Ahí la llevas, ahí la llevas..."
description: "10 usuarios seguidos"
_following50:
title: "¡Un puñado de amigos!"
description: "50 cuentas seguidas"
_following100:
title: "100 amigos"
description: "100 cuentas seguidas"
_following300:
title: "¡Sobrecarga de amigos!"
description: "300 cuentas seguidas"
_followers1:
title: "¡Tu primer seguidor!"
description: "1 seguidor ganado"
_followers10:
title: "¡Sígueme!"
description: "10 seguidores ganados"
_followers50:
title: "Viniendo en manada"
description: "50 seguidores ganados"
_followers100:
title: "Popular"
description: "100 cuentas seguidas"
_followers300:
title: "Por favor, hagan una fila"
description: "300 seguidores ganados"
_followers500:
title: "¡Toda una torre de radio!"
description: "500 seguidores ganados"
_followers1000:
title: "\"Influyente\""
description: "1000 seguidores gandos"
_collectAchievements30:
title: "Coleccionista"
description: "30 logros ganados"
_viewAchievements3min:
title: "¡Te gustan los logros!"
description: "Mirando tus logros por 3 minutos"
_iLoveMisskey:
title: "¡AMO Misskey!"
description: "\"I ❤ #Misskey\" Publicado"
flavor: "El equipo de desarrollo de Misskey, en verdad, ¡aprecia tu apoyo!"
_foundTreasure:
title: "Búsqueda del tesoro"
description: "Encontraste un tesoro"
_client30min:
title: "Un descansito"
description: "30 minutos dedicados a Misskey"
_noteDeletedWithin1min:
title: "Ah... Mejor no..."
description: "Borrar una nota antes que de pase 1 minuto"
_postedAtLateNight:
title: "Nocturno"
description: "Una nota publicada por la noche"
flavor: "¡Ya casi es hora de dormir!"
_postedAt0min0sec:
title: "Reloj parlante"
description: "Publicar una nota a las 00:00 de la madrugada"
flavor: "Tic, tic, tic ¡TUUUUUN!"
_selfQuote:
title: "Autoreferencia"
description: "Citar tu propia nota"
_htl20npm:
title: "Línea de tiempo fluyendo"
description: "La velocidad de tu línea de tiempo excede las 20 npm (notas por minuto)"
_viewInstanceChart:
title: "Analista"
description: "Gráficas de la instancia mostradas"
_outputHelloWorldOnScratchpad:
title: "¡Hola mundo!"
description: "Escribir \"hello world\" en el compositor"
_open3windows:
title: "Multiventana"
description: "Tener más de 3 ventanas al mismo tiempo"
_driveFolderCircularReference:
title: "Referencia circular"
description: "Intento de crear carpetas recursivamente"
_reactWithoutRead:
title: "¡Sí lo leíste bien?"
description: "Reaccionar a los 3 segundos de publicación de una nota con más de 100 caracteres"
_clickedClickHere:
title: "Pícale aquí"
description: "Le picó ahí"
_justPlainLucky:
title: "Pura suerte"
description: "Obtenido con una probabilidad del 0.01% cada 10 segundos"
_setNameToSyuilo:
title: "Complejo de superioridad"
description: "Configurar el nombre como 'Syuilo'"
_passedSinceAccountCreated1:
title: "Primer aniversario"
description: "Pasó un año desde la creación de la cuenta"
_passedSinceAccountCreated2:
title: "Segundo aniversario"
description: "Pasaron dos años desde la creación de la cuenta"
_passedSinceAccountCreated3:
title: "Tercer aniversario"
description: "Pasaron tres años desde la creación de la cuenta"
_loggedInOnBirthday:
title: "¡Feliz cumpleaños!"
description: "En linea el día de tu cumpleaños"
_loggedInOnNewYearsDay:
title: "¡Feliz Año Nuevo!"
description: "En linea en año nuevo"
flavor: "¡Gracias por tu apoyo a la instancia durante todo este año!"
_cookieClicked:
title: "Un juego para picarle a una galleta"
description: "Picaste una galleta"
flavor: "¿Está mal este juego?"
_brainDiver:
title: "Brain Diver"
description: "Publicaste un vínculo a \"Brain Diver\""
flavor: "Misskey-Misskey La-Tu-Ma"
_role:
new: "Crear rol"
edit: "Editar rol"
name: "Nombre del rol"
description: "Descripción del rol"
permission: "Permisos del rol"
descriptionOfPermission: "<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"
canEditMembersByModerator: "Permitir a los moderadores editar los miembros"
descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo."
priority: "Prioridad"
_priority:
low: "Baja"
middle: "Mediano"
high: "Alta"
_options:
gtlAvailable: "Explorar la línea de tiempo global"
ltlAvailable: "Explorar la línea de tiempo local"
canPublicNote: "Permitir la publicación"
canInvite: "Puede crear códigos de invitación"
canManageCustomEmojis: "Administrar emojis personalizados"
driveCapacity: "Capacidad de almacenamiento"
pinMax: "Máximo de notas fijadas"
antennaMax: "Máximo de antenas"
wordMuteMax: "Máximo de caracteres en palabras silenciadas"
webhookMax: "Máximo de Webhooks"
clipMax: "Máximo de clips"
noteEachClipsMax: "Máximo de notas con clip"
userListMax: "Máximo de listas de usuarios"
userEachUserListsMax: "Máximo de usuarios en una lista"
rateLimitFactor: "Limitador"
descriptionOfRateLimitFactor: "Límites más bajos son menos restrictivos, más altos menos restrictivos"
canHideAds: "Puede ocultar anuncios"
_condition:
isLocal: "Usuario local"
isRemote: "Usuario remoto"
createdLessThan: "Menos de X han pasado desde la creación de la cuenta"
createdMoreThan: "Más de X han pasado desde la creación de la cuenta"
followersLessThanOrEq: "Tiene X o menos seguidores"
followersMoreThanOrEq: "Tiene X o más seguidores"
followingLessThanOrEq: "Sigue X o menos cuentas"
followingMoreThanOrEq: "Sigue X o más cuentas"
and: "Condicional AND"
or: "Condicional OR"
not: "Condicional NOT"
_sensitiveMediaDetection:
description: "Reduce el esfuerzo de la moderación el el servidor a través del reconocimiento automático de contenido NSFW usando 'Machine Learning'. Esto puede incrementar ligeramente la carga en el servidor."
sensitivity: "Sensibilidad de detección"
description: "Reduce el esfuerzo de la moderación en el servidor a través del reconocimiento automático de contenido NSFW usando 'Machine Learning'. Esto puede incrementar ligeramente la carga en el servidor."
sensitivity: "Sensibilidad de la detección"
sensitivityDescription: "Reducir la sensibilidad puede acarrear a varios falsos positivos, mientras que incrementarla puede reducir las detecciones (falsos negativos)."
setSensitiveFlagAutomatically: "Marcar como NSFW"
setSensitiveFlagAutomaticallyDescription: "Los resultados de la detección interna pueden ser retenidos incluso si la opción está desactivada."
@@ -1328,10 +1634,12 @@ _widgets:
jobQueue: "Cola de trabajos"
serverMetric: "Estadísticas del servidor"
aiscript: "Consola de AiScript"
aiscriptApp: "Aplicación AiScript"
aichan: "indigo"
userList: "Lista de usuarios"
_userList:
chooseList: "Seleccione una lista"
clicker: "Cliqueador"
_cw:
hide: "Ocultar"
show: "Ver más"
@@ -1434,7 +1742,16 @@ _timelines:
social: "Social"
global: "Global"
_play:
new: "Crear guión"
edit: "Editar guión"
created: "Guión creado"
updated: "Guión editado"
deleted: "Guión eliminado"
pageSetting: "Configuración de guión"
editThisPage: "Editar este guión"
viewSource: "Ver la fuente"
my: "Mis guiones"
liked: "Guiones que te gustaron"
featured: "Popular"
title: "Título"
script: "Script"
@@ -1507,6 +1824,7 @@ _notification:
pollEnded: "Estan disponibles los resultados de la encuesta"
unreadAntennaNote: "Antena {name}"
emptyPushNotificationMessage: "Se han actualizado las notificaciones push"
achievementEarned: "Logro desbloqueado"
_types:
all: "Todo"
follow: "Siguiendo"

View File

@@ -13,6 +13,7 @@ fetchingAsApObject: "Mengambil data dari Fediverse..."
ok: "OK"
gotIt: "Saya mengerti"
cancel: "Batalkan"
noThankYou: "Tidak sekarang."
enterUsername: "Masukkan nama pengguna"
renotedBy: "direnote oleh {user}"
noNotes: "Tidak ada catatan"
@@ -206,6 +207,7 @@ done: "Selesai"
processing: "Memproses"
preview: "Pratinjau"
default: "Bawaan"
defaultValueIs: "Bawaan: {value}"
noCustomEmojis: "Tidak ada emoji kustom"
noJobs: "Tidak ada kerja"
federating: "memfederasi"
@@ -349,6 +351,8 @@ recaptcha: "reCAPTCHA"
enableRecaptcha: "Nyalakan reCAPTCHA"
recaptchaSiteKey: "Site key"
recaptchaSecretKey: "Secret Key"
turnstile: "Turnstile"
enableTurnstile: "Nyalakan Turnstile"
turnstileSiteKey: "Site key"
turnstileSecretKey: "Secret Key"
avoidMultiCaptchaConfirm: "Menggunakan banyak Captcha dapat menyebabkan gangguan. Apakah kamu ingin untuk menonaktifkan Captcha yang lain? Kamu dapat membiarkan fitur ini tetap aktif dengan menekan tombol batal."
@@ -454,6 +458,7 @@ uiLanguage: "Bahasa antarmuka pengguna"
groupInvited: "Telah diundang ke grup"
aboutX: "Tentang {x}"
emojiStyle: "Gaya emoji"
native: "Native"
disableDrawer: "Jangan gunakan menu bergaya laci"
youHaveNoGroups: "Kamu tidak memiliki grup"
joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri."
@@ -857,10 +862,21 @@ rateLimitExceeded: "Batas sudah terlampaui"
cropImage: "potong gambar"
cropImageAsk: "Ingin memotong gambar?"
file: "Berkas"
recentNHours: "{n} jam terakhir"
recentNDays: "{n} hari terakhir"
noEmailServerWarning: "Mail Server tidak disetel."
thereIsUnresolvedAbuseReportWarning: "Ada laporan yang belum diselesaikan."
recommended: "Disarankan"
check: "Cek"
driveCapOverrideLabel: "Ubah kapasitas drive untuk user ini"
driveCapOverrideCaption: "Setel ulang kapasitas ke bawaan dengan memasukkan nilai 0 atau lebih rendah."
requireAdminForView: "Kamu harus login dengan akun administrator untuk melihat ini."
isSystemAccount: "Akun yang dibuat dan otomatis dioperasikan oleh sistem."
typeToConfirm: "Mohon masukkan {x} untuk mengonfirmasi"
deleteAccount: "Hapus Akun"
document: "Dokumen"
numberOfPageCache: "Jumlah halaman ditembolokkan"
numberOfPageCacheDescription: "Menaikkan jumlah ini akan meningkatkan kenyamanan untuk pengguna, namun dapat menyebabkan lonjakan beban pada peladen dan juga memori yang digunakan."
logoutConfirm: "Anda yakin ingin keluar?"
lastActiveDate: "Terakhir digunakan"
statusbar: "Bilah status"
@@ -870,20 +886,189 @@ colored: "Diwarnai"
refreshInterval: "Jeda pembaharuan"
label: "Label"
type: "Tipe"
speed: "Kecepatan"
slow: "Lambat"
fast: "Cepat"
sensitiveMediaDetection: "Deteksi media NSFW"
localOnly: "Hanya lokal"
remoteOnly: "Hanya remot"
failedToUpload: "Gagal mengunggah"
cannotUploadBecauseInappropriate: "Berkas ini tidak dapat diunggah karena sebagian dari berkas terdeteksi berpotensi NSFW."
cannotUploadBecauseNoFreeSpace: "Gagal mengunggah karena kekurangan kapasitas Drive."
beta: "Beta"
enableAutoSensitive: "Penandaan NSFW otomatis"
enableAutoSensitiveDescription: "Mendeteksi otomatis dan menandai media NSFW menggunakan Machine Learning jika memungkinkan. Meskipun opsi ini dimatikan, ada kemungkinan dinyalakan secara menyeluruh pada instansi peladen."
activeEmailValidationDescription: "Membolehkan validasi alamat surel ketat dengan mengecek apakah alamat surel tersebut temporer dan bisa berkomunikasi dengan surel tersebut. Ketidak tidak dicentang, hanya format surel yang divalidasi."
navbar: "Bilah navigasi"
shuffle: "Acak"
account: "Akun"
move: "Pindah"
pushNotification: "Pemberitahuan push"
subscribePushNotification: "Nyalakan pemberitahuan push"
unsubscribePushNotification: "Matikan pemberitahuan push"
pushNotificationAlreadySubscribed: "Pemberitahuan push telah dinyalakan"
pushNotificationNotSupported: "Browser atau instansi kamu tidak mendukung pemberitahuan push"
sendPushNotificationReadMessage: "Hapus pemberitahuan push ketika pemberitahuan relevan atau pesan telah dibaca"
sendPushNotificationReadMessageCaption: "Pemberitahuan berisi teks「{emptyPushNotificationMessage}」akan ditampilkan dalam waktu pendek. Ini mungkin dapat menambah pemakaian baterai pada perangkat kamu."
windowMaximize: "Maksimalkan"
windowRestore: "Kembalikan"
caption: "Keterangan"
loggedInAsBot: "Sedang login sebagai bot"
tools: "Alat"
cannotLoad: "Tidak dapat memuat"
numberOfProfileView: "tayang profil"
like: "Suka"
unlike: "Tidak Suka"
numberOfLikes: "Jumlah yang disukai"
show: "Tampilkan"
neverShow: "Jangan tampilkan lagi"
remindMeLater: "Mungkin nanti"
didYouLikeMisskey: "Apakah kamu mulai menyukai Misskey?"
pleaseDonate: "{host} menggunakan perangkat lunak bebas yaitu Misskey. Kami sangat mengapresiasi sekali donasi dari kamu agar pengembangan Misskey tetap dapat berlanjut!"
roles: "Peran"
role: "Peran"
color: "Warna"
_achievements:
_types:
_login7:
description: "Login selama 7 hari"
flavor: "Sudah mulai terbiasa?"
_login15:
title: "Pemula III"
description: "Login selama 15 hari"
_login30:
title: "Misskist I"
description: "Login selama 30 hari"
_login60:
title: "Misskist II"
description: "Login selama 60 hari"
_login100:
title: "Misskist III"
description: "Login selama 100 hari"
flavor: "Violent Misskist"
_login200:
title: "Reguler I"
description: "Login selama 200 hari"
_login300:
title: "Reguler II"
description: "Login selama 300 hari"
_login400:
title: "Reguler III"
description: "Login selama 400 hari"
_login500:
title: "Veteran I"
description: "Login selama 500 hari"
flavor: "Kawanku, aku suka catatan."
_login600:
title: "Veteran II"
description: "Login selama 600 hari"
_login700:
title: "Veteran III"
description: "Login selama 700 hari"
_login800:
title: "Sepuh Catatan I"
description: "Login selama 800 hari"
_login900:
title: "Sepuh Catatan II"
description: "Login selama 900 hari"
_login1000:
title: "Sepuh Catatan III"
description: "Login selama 1000 hari"
flavor: "Terima kasih telah menggunakan Misskey!"
_noteClipped1:
title: "Harus... Ngeklip..."
description: "Klip catatan pertamamu"
_noteFavorited1:
title: "Pengamat Bintang"
description: "Favoritkan catatan pertamamu"
_myNoteFavorited1:
title: "Pencari Bintang"
description: "Minta orang lain memfavoritkan salah satu catatanmu"
_profileFilled:
title: "Siap Sedia"
description: "Atur profil kamu"
_markedAsCat:
title: "Aku Seekor Kucing"
description: "Tandai akunmu sebagai kucing"
flavor: "Aku beri kamu nama nanti"
_following1:
title: "Ikuti pengguna lain pertamamu"
description: "Ikuti pengguna"
_following10:
title: "Terusin... terusin..."
description: "Ikuti 10 pengguna lain"
_following50:
title: "Banyak teman"
description: "Ikuti 50 pengguna lain"
_following100:
title: "100 Teman"
description: "Ikuti 100 pengguna lain"
_clickedClickHere:
description: "Kamu telah mengeklik disini"
_justPlainLucky:
title: "Lagi Beruntung"
description: "Mendapatkan kesempatan dengan kemungkinan 0.01% setiap 10 detik"
_setNameToSyuilo:
title: "God Complex"
description: "Atur namamu jadi \"syuilo\""
_passedSinceAccountCreated1:
title: "Perayaan Satu Tahun"
description: "Satu tahun telah lewat sejak akunmu dibuat"
_passedSinceAccountCreated2:
title: "Perayaan Dua Tahun"
description: "Dua tahun telah lewat sejak akunmu dibuat"
_passedSinceAccountCreated3:
title: "Perayaan Tiga Tahun"
description: "Tiga tahun telah lewat sejak akunmu dibuat"
_loggedInOnBirthday:
title: "Selamat Ulang Tahun"
description: "Login di hari ulang tahunmu"
_loggedInOnNewYearsDay:
title: "Selamat Tahun Baru!"
description: "Login di hari pertama tahun baru"
_cookieClicked:
title: "Permainan dimana kamu mengeklik kue"
description: "Mengeklik kue"
flavor: "Tunggu, apakah kamu sedang berada di website yang benar?"
_brainDiver:
title: "Brain Diver"
description: "Posting tautan mengenai Brain Diver"
flavor: "Misskey-Misskey La-Tu-Ma"
_role:
new: "Buat peran"
edit: "Sunting peran"
name: "Nama peran"
description: "Deskripsi peran"
permission: "Perijinan peran"
descriptionOfPermission: "<b>Moderator</b> dapat melakukan operasi moderasi dasar.\n<b>Administrator</b> dapat mengubah seluruh pengaturan instansi."
assignTarget: "Tipe tugas"
descriptionOfAssignTarget: "<b>Manual</b> untuk mengganti secara manual siapa yang mendapatkan peran ini dan siapa yang tidak.\n<b>Kondisional</b> untuk pengguna secara otomatis dimasukkan atau dihapus dari peran berdasarkan kondisi yang ditentukan."
manual: "Manual"
conditional: "Kondisional"
condition: "Kondisi"
isConditionalRole: "Ini adalah peran kondisional"
isPublic: "Publikkan Peran"
descriptionOfIsPublic: "Siapapun dapat melihat daftar pengguna yang ditugaskan pada peran ini. Tambahan juga peran ini akan ditampilkan ke dalam profil pengguna tentang peran yang ditugaskan."
options: "Opsi peran"
policies: "Kebijakan"
baseRole: "Templat peran"
useBaseValue: "Gunakan nilai templat peran"
chooseRoleToAssign: "Pilih peran yang ditugaskan"
canEditMembersByModerator: "Perbolehkan moderator untuk menyunting daftar anggota untuk peran ini"
descriptionOfCanEditMembersByModerator: "Ketika dinyalakan, moderator beserta administrator dapat menugaskan ataupun mencabut pengguna ke peran ini. Ketika dimatikan, hanya administrator saja yang dapat menugaskan pengguna ke peran ini."
priority: "Prioritas"
_priority:
low: "Rendah"
middle: "Sedang"
high: "Tinggi"
_options:
gtlAvailable: "Dapat melihat linimasa global"
ltlAvailable: "Dapat melihat linimasa lokal"
canPublicNote: "Dapat mengirim catatan publik"
canInvite: "Dapat membuat kode undangan instansi"
canManageCustomEmojis: "Dapat mengelola Emoji kustom"
driveCapacity: "Kapasitas Drive"
pinMax: "Jumlah maksimal catatan yang disematkan"
_emailUnavailable:
used: "Alamat surel ini telah digunakan"
format: "Format tidak valid."
@@ -1167,6 +1352,7 @@ _tutorial:
step7_1: "Yay, Selamat! Kamu sudah menyelesaikan tutorial dasar Misskey."
step7_2: "Jika kamu ingin mempelajari lebih lanjut tentang Misskey, cobalah berkunjung ke bagian {help}."
step7_3: "Semoga berhasil dan bersenang-senanglah! 🚀"
step8_3: "Kamu dapat mengganti pengaturan ini nanti."
_2fa:
alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor."
registerDevice: "Daftarkan perangkat baru"
@@ -1241,10 +1427,13 @@ _widgets:
trends: "Tren"
clock: "Jam"
rss: "Pembaca RSS"
rssTicker: "RSS-Ticker"
activity: "Aktivitas"
photos: "Foto"
digitalClock: "Jam digital"
unixClock: "Jam UNIX"
federation: "Federasi"
instanceCloud: "Instansi awan"
postForm: "Buat catatan"
slideshow: "Slideshow"
button: "Tombol"
@@ -1254,8 +1443,10 @@ _widgets:
aiscript: "Konsol AiScript"
aiscriptApp: "Aplikasi AiScript"
aichan: "Ai"
userList: "Daftar pengguna"
_userList:
chooseList: "Pilih daftar"
clicker: "Pengeklik"
_cw:
hide: "Sembunyikan"
show: "Lihat konten"
@@ -1319,6 +1510,7 @@ _profile:
changeBanner: "Ubah header"
_exportOrImport:
allNotes: "Semua catatan"
favoritedNotes: "Catatan favorit"
followingList: "Ikuti"
muteList: "Bisukan"
blockingList: "Blokir"
@@ -1437,7 +1629,9 @@ _notification:
yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima"
youWereInvitedToGroup: "Telah diundang ke grup"
pollEnded: "Hasil Kuesioner telah keluar"
unreadAntennaNote: "Antena {name}"
emptyPushNotificationMessage: "Pembaruan notifikasi dorong"
achievementEarned: "Pencapaian didapatkan"
_types:
all: "Semua"
follow: "Ikuti"
@@ -1459,6 +1653,7 @@ _deck:
alwaysShowMainColumn: "Selalu tampilkan kolom utama"
columnAlign: "Luruskan kolom"
addColumn: "Tambahkan kolom"
configureColumn: "Atur kolom"
swapLeft: "Pindah ke kiri"
swapRight: "Pindah ke kanan"
swapUp: "Pindah ke atas"
@@ -1466,6 +1661,11 @@ _deck:
stackLeft: "Tumpukkan di kolom kiri"
popRight: "Keluarkan di kanan"
profile: "Profil"
newProfile: "Profil baru"
deleteProfile: "Hapus profil"
introduction: "Buat antarmuka sempurna untukmu dengan menata kolom secara bebas!"
introduction2: "Klik \"+\" pada kanan layar untuk menambahkan kolom baru kapanpun yang kamu mau."
widgetsIntroduction: "Mohon pilih \"Sunting gawit\" pada menu kolom dan tambahkan gawit."
_columns:
main: "Utama"
widgets: "Widget"

View File

@@ -22,7 +22,7 @@ instance: "Инстанс"
settings: "Настройки"
basicSettings: "Основные настройки"
otherSettings: "Прочие настройки"
openInWindow: "Открывать в плавающих окнах"
openInWindow: "Открыть в плавающем окне"
profile: "Профиль"
timeline: "Лента"
noAccountDescription: "Пользователь ничего не написал про себя"
@@ -273,7 +273,7 @@ light: "Светлый"
dark: "Тёмный"
lightThemes: "Светлые темы"
darkThemes: "Тёмные темы"
syncDeviceDarkMode: "Синхронизировать с темным режимом устройства"
syncDeviceDarkMode: "Синхронизировать с тёмной темой системы"
drive: "Диск"
fileName: "Имя файла"
selectFile: "Выберите файл"
@@ -456,6 +456,7 @@ uiLanguage: "Язык интерфейса"
groupInvited: "Приглашение в группу"
aboutX: "Описание {x}"
emojiStyle: "Стиль эмодзи"
native: "Системные"
disableDrawer: "Не использовать выдвижные меню"
youHaveNoGroups: "У вас нет ни одной группы"
joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные"
@@ -603,6 +604,7 @@ smtpSecureInfo: "Выключите при использовании STARTTLS."
testEmail: "Проверка доставки электронной почты"
wordMute: "Скрытие слов"
regexpError: "Ошибка в регулярном выражении"
regexpErrorDescription: "В списке {tab} скрытых слов, в строке {line} обнаружена синтаксическая ошибка:"
instanceMute: "Глушение инстансов"
userSaysSomething: "{name} что-то сообщает"
makeActive: "Активировать"
@@ -804,7 +806,7 @@ translate: "Перевод"
translatedFrom: "Перевод. Язык оригинала — {x}"
accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи"
usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже."
aiChanMode: "ИИ режим"
aiChanMode: "Режим Ай"
keepCw: "Сохраняйте Предупреждения о содержимом"
pubSub: "Учётные записи Pub/Sub"
lastCommunication: "Последнее сообщение"
@@ -821,8 +823,8 @@ manageAccounts: "Управление аккаунтом"
makeReactionsPublic: "Опубликовать список реакций"
makeReactionsPublicDescription: "Список сделанных вами реакций доступен для просмотра всем желающим."
classic: "Классика"
muteThread: "Заглушить цепочку"
unmuteThread: "Отменить глушение цепочки"
muteThread: "Скрыть цепочку"
unmuteThread: "Отменить сокрытие цепочки"
ffVisibility: "Видимость подписок и подписчиков"
ffVisibilityDescription: "Здесь можно настроить, кто будет видеть ваши подписки и подписчиков."
continueThread: "Показать следующие ответы"
@@ -891,6 +893,7 @@ cannotUploadBecauseNoFreeSpace: "Файл не может быть загруж
beta: "Бета"
enableAutoSensitive: "Автоматическое определение NSFW"
enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена ​​автоматически в зависимости от инстанта."
activeEmailValidationDescription: "Если включено, будет проводиться более строгая проверка адреса электронной почты, в том числе на то, что он действительный и не временный. Если же отключено, то проверяется только корректность написания адреса."
navbar: "Панель навигации"
shuffle: "Перемешать"
account: "Учётные записи"
@@ -1096,6 +1099,9 @@ _achievements:
title: "Я люблю Misskey"
description: "Написана заметка «I ❤ #Misskey»"
flavor: "Спасибо за поддержку Misskey! Ваша команда разработчиков"
_foundTreasure:
title: "Охота за сокровищами"
description: "Найдено спрятанное сокровище"
_client30min:
title: "Перерыв на обед"
description: "Прошло 30 минут с момента запуска клиента"
@@ -1116,6 +1122,9 @@ _achievements:
_htl20npm:
title: "В потоке"
description: "Достигнута скорость домашней ленты в 20 з/мин (заметок минуту)"
_viewInstanceChart:
title: "Аналитик"
description: "Просмотрены статистические диаграммы инстанса"
_outputHelloWorldOnScratchpad:
title: "Привет, мир!"
description: "Выведен текст «hello world» в Когтеточке"
@@ -1189,7 +1198,34 @@ _role:
middle: "Средне"
high: "Высокий"
_options:
gtlAvailable: "Может просматривать глобальную ленту"
ltlAvailable: "Может просматривать местную ленту"
canPublicNote: "Может публиковать общедоступные заметки"
canInvite: "Может создавать пригласительные коды"
canManageCustomEmojis: "Управлять пользовательскими эмодзи"
driveCapacity: "Доступное пространство на «диске»"
pinMax: "Доступное количество закреплённых заметок"
antennaMax: "Доступное количество антенн"
wordMuteMax: "Доступное количество знаков в списке скрытия слов"
clipMax: "Максимальное количество подборок"
noteEachClipsMax: "Максимальное количество заметок в подборке"
userListMax: "Максимальное количество списков аккаунтов"
userEachUserListsMax: "Максимальное количество аккаунтов в списке"
rateLimitFactor: "Ограничение активности"
descriptionOfRateLimitFactor: "Меньшее значение — слабые ограничения, большее — сильные"
canHideAds: "Может скрыть рекламу"
_condition:
isLocal: "Местный"
isRemote: "Неместный"
createdLessThan: "Аккаунт младше, чем..."
createdMoreThan: "Аккаунт старше, чем..."
followersLessThanOrEq: "Количество подписчиков не превышает…"
followersMoreThanOrEq: "Количество подписчиков не меньше чем…"
followingLessThanOrEq: "Количество подписок не превышает…"
followingMoreThanOrEq: "Количество подписок не меньше чем…"
and: "Выполнено несколько условий:.."
or: "Выполнено любое из условий:.."
not: "Кроме тех, у кого…"
_sensitiveMediaDetection:
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
setSensitiveFlagAutomatically: "Установить флаг NSFW"
@@ -1237,10 +1273,23 @@ _plugin:
installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете."
manage: "Управление расширениями"
_preferencesBackups:
saveConfirm: "Сохранить бэкап как {name}?"
deleteConfirm: "Удалить резервную копию {name}?"
renameConfirm: "Переименовать резервную копию с \"{old}\" на \"{new}\"?"
noBackups: "Резервной копии не существует. Вы можете создать резервную копию в настройках на этом инстансе с помощью \"Создать новую резервную копию\"."
list: "Существующие резервные копии"
saveNew: "Создать резервную копию"
loadFile: "Прочесть из файла"
apply: "Восстановить на это устройство"
save: "Обновить из текущих настроек"
inputName: "Введите название для резервной копии"
cannotSave: "Сохранить не удалось"
nameAlreadyExists: "Резервная копия под названием «{name}» уже существует. Придумайте другое."
applyConfirm: "Правда хотите загрузить резервную копию «{name}» на это устройство? Этим будут потеряны текущие настройки."
saveConfirm: "Сохранить резервную копию под названием «{name}»?"
deleteConfirm: "Удалить резервную копию «{name}»?"
renameConfirm: "Переименовать резервную копию «{old}» в «{new}»?"
noBackups: "Здесь ещё нет резервных копий. Вы можете создать резервную копию настроек на этом сайте с помощью кнопки «Создать резервную копию»."
createdAt: "Создана {date} в {time}"
updatedAt: "Обновлена {date} в {time}"
cannotLoad: "Загрузить не удалось"
invalidFile: "Некорректный формат файла"
_registry:
scope: "Область"
key: "Ключ"
@@ -1324,6 +1373,8 @@ _mfm:
sparkleDescription: "Добавляет эффект искрящихся частиц."
rotate: "Повернуть"
rotateDescription: "Поворачивает на заданный угол."
plain: "Буквально"
plainDescription: "MFM внутри отключается, и текст отображается как есть"
_instanceTicker:
none: "Не показывать"
remote: "Только для других сайтов"
@@ -1353,12 +1404,14 @@ _wordMute:
muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)."
softDescription: "Соответствующие условиям заметки будут спрятаны из вашей ленты."
hardDescription: "Соответстующие условиям заметки вообще не будут попадать в вашу ленту. Даже если вы поменяете условия, отсеенные таким образом заметки уже не появятся."
soft: "Мягкий"
hard: "Жёсткий"
soft: "Мягко"
hard: "Жёстко"
mutedNotes: "Скрытые заметки"
_instanceMute:
instanceMuteDescription: "Заметки и репосты с указанных здесь инстансов, а также ответы пользователям оттуда же не будут отображаться."
instanceMuteDescription2: "Пишите каждый инстанс на отдельной строке"
title: "Скрывает заметки с заданных инстансов."
heading: "Список заглушенных инстансов"
heading: "Список скрытых инстансов"
_theme:
explore: "Обзор"
install: "Установить тему"
@@ -1479,12 +1532,16 @@ _tutorial:
step7_1: "На этом вводный урок по использованию Misskey закончен. Спасибо, что прошли его до конца!"
step7_2: "Хотите изучить Misskey глубже — добро пожаловать в раздел «{help}»."
step7_3: "Приятно вам провести время с Misskey🚀"
step8_1: "Ах, да, не хотите ли включить push-уведомления?"
step8_2: "С push-уведомлениями вы будете в курсе репостов, ответов, реакций и всего такого, даже когда закрыли Misskey."
step8_3: "Эту настройку вы всегда сможете поменять"
_2fa:
alreadyRegistered: "Двухфакторная аутентификация уже настроена."
registerDevice: "Зарегистрируйте ваше устройство"
registerKey: "Зарегистрировать ключ"
step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}."
step2: "Далее отсканируйте отображаемый QR-код при помощи приложения."
step2Url: "Если пользуетесь приложением на компьютере, можете ввести в него эту строку (URL):"
step3: "И наконец, введите код, который покажет приложение."
step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом."
securityKeyInfo: "Вы можете настроить вход с помощью аппаратного ключа безопасности, поддерживающего FIDO2, или отпечатка пальца или PIN-кода на устройстве."
@@ -1501,7 +1558,7 @@ _permissions:
"write:following": "Изменять спискок подписок"
"read:messaging": "Смотреть сообщения"
"write:messaging": "Писать и удалять сообщения"
"read:mutes": "Смотреть спискок скрытых пользователей"
"read:mutes": "Смотреть список скрытых пользователей"
"write:mutes": "Изменять список скрытых пользователей"
"write:notes": "Писать и удалять заметки"
"read:notifications": "Смотреть уведомления"
@@ -1552,10 +1609,13 @@ _widgets:
trends: "Актуальное"
clock: "Часы"
rss: "Просмотр RSS"
rssTicker: "Бегущая строка RSS"
activity: "Активность"
photos: "Фото"
digitalClock: "Цифровые часы"
unixClock: "Часы UNIX"
federation: "Федерация"
instanceCloud: "Облако инстансов"
postForm: "Форма отправки"
slideshow: "Показ слайдов"
button: "Кнопка"
@@ -1563,9 +1623,12 @@ _widgets:
jobQueue: "Очередь заданий"
serverMetric: "Показатели сервера"
aiscript: "Консоль AiScript"
aiscriptApp: "Приложение на AiScript"
aichan: "Ай"
userList: "Список аккаунтов"
_userList:
chooseList: "Выберите список"
clicker: "Счётчик щелчков"
_cw:
hide: "Спрятать"
show: "Показать еще"
@@ -1628,12 +1691,13 @@ _profile:
changeAvatar: "Поменять аватар"
changeBanner: "Поменять изображение в шапке"
_exportOrImport:
allNotes: "Все записи\n"
allNotes: "Все заметки\n"
favoritedNotes: "Избранное"
followingList: "Подписки"
muteList: "Скрытые"
blockingList: "Заблокированные"
userLists: "Списки"
excludeMutingUsers: "За исключением заглушенных пользователей"
excludeMutingUsers: "За исключением скрытых пользователей"
excludeInactiveUsers: "Без неактивных учётных записей"
_charts:
federation: "Федерация"
@@ -1737,6 +1801,8 @@ _notification:
youReceivedFollowRequest: "У вас новый запрос на подписку."
yourFollowRequestAccepted: "Ваш запрос на подписку одобрен."
youWereInvitedToGroup: "Вы приглашены в группу."
pollEnded: "Подведены окончательные итоги опроса"
emptyPushNotificationMessage: "Обновлены push-уведомления"
achievementEarned: "Получено достижение"
_types:
all: "Все"
@@ -1746,11 +1812,13 @@ _notification:
renote: "Репосты"
quote: "Цитаты"
reaction: "Реакции"
pollEnded: "Окончания опросов"
receiveFollowRequest: "Получен запрос на подписку"
followRequestAccepted: "Запрос на подписку одобрен"
groupInvited: "Приглашение в группы"
app: "Уведомления из приложений"
_actions:
followBack: "отвечает взаимной подпиской"
reply: "Ответить"
renote: "Репост"
_deck:
@@ -1764,7 +1832,12 @@ _deck:
swapDown: "Переставить ниже"
stackLeft: "В столбик влево"
popRight: "Из столбика вправо"
profile: "Профиль"
profile: "Расстановка"
newProfile: "Новая расстановка"
deleteProfile: "Удаление расстановки"
introduction: "Создайте идеальный интерфейс расставляя колонки как угодно"
introduction2: "Чтобы добавлять колонки в любом месте, жмите «+» справа экрана."
widgetsIntroduction: "Чтобы добавлять виджеты, выбирайте «Редактировать виджеты» в меню колонки."
_columns:
main: "Основная"
widgets: "Виджеты"

View File

@@ -944,48 +944,236 @@ _achievements:
_types:
_notes1:
title: "เพียงแค่ตั้งค่า msky ของฉัน"
description: "โพสต์โน้ตครั้งแรกของคุณ"
flavor: "ขอให้มีช่วงเวลาที่ดีกับ Misskey นะคะ!"
_notes10:
title: "โน้ตบางอย่าง"
description: "โพสต์ 10 โน้ต"
_notes100:
title: "โน้ตจำนวนมาก"
description: "โพสต์ 100 โน้ต"
_notes500:
title: "ครอบคลุมในโน้ต"
description: "โพสต์ 500 โน้ต"
_notes1000:
title: "ภูเขาแห่งโน้ต"
description: "โพสต์ 1,000 โน้ต"
_notes5000:
title: "โน้ตล้น"
description: "โพสต์ 5,000 โน้ต"
_notes10000:
title: "ซุปเปอร์โน้ต"
description: "โพสต์ 10,000 โน้ต"
_notes20000:
title: "ต้องการ... เพิ่มเติม... โน้ต..."
description: "โพสต์ 20,000 โน้ต"
_notes30000:
title: "โน้ต โน้ต โน้ต!"
description: "โพสต์ 30,000 โน้ต"
_notes40000:
title: "โน้ตโรงงาน"
description: "โพสต์ 40,000 โน้ต"
_notes50000:
title: "ดาวเคราะห์แห่งโน้ต"
description: "โพสต์ 50,000 โน้ต"
_notes60000:
title: "โน้ตควอซาร์"
description: "โพสต์ 60,000 โน้ต"
_notes70000:
title: "โน้ตหลุมดำ"
description: "โพสต์ 70,000 โน้ต"
_notes80000:
title: "โน้ต กาแล็กซี่"
description: "โพสต์ 80,000 โน้ต"
_notes90000:
title: "โน้ต จักรวาล"
description: "โพสต์ 90,000 โน้ต"
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "โพสต์ 100,000 โน้ต"
flavor: "นายแน่ใจล่ะก็ มีอะไรพูดมาได้นะ"
_login3:
title: "มือใหม่ I"
description: "เข้าสู่ระบบเป็นเวลารวม 3 วัน"
flavor: "เริ่มตั้งแต่วันนี้ เรียกฉันว่ามิสคิสต์"
_login7:
title: "มือใหม่ II"
description: "เข้าสู่ระบบเป็นเวลารวม 7 วัน"
flavor: "รู้สึกเหมือนคุณได้แขวนของสิ่งต่างๆ หรือยังคะ?"
_login15:
title: "มือใหม่ III"
description: "เข้าสู่ระบบเป็นเวลารวม 15 วัน"
_login30:
title: "มิสคิสท์ I"
description: "เข้าสู่ระบบเป็นเวลารวม 30 วัน"
_login60:
title: "มิสคิสท์ II"
description: "เข้าสู่ระบบเป็นเวลารวม 60 วัน"
_login100:
title: "มิสคิสท์ III"
description: "เข้าสู่ระบบเป็นเวลารวม 100 วัน"
flavor: "ความรุนแรง Misskist"
_login200:
title: "ลูกค้าประจำ I"
description: "เข้าสู่ระบบเป็นเวลารวม 200 วัน"
_login300:
title: "ลูกค้าประจำ II"
description: "เข้าสู่ระบบเป็นเวลารวม 300 วัน"
_login400:
title: "ลูกค้าประจำ III"
description: "เข้าสู่ระบบเป็นเวลารวม 400 วัน"
_login500:
title: "ผู้เชี่ยวชาญ I"
description: "เข้าสู่ระบบเป็นเวลารวม 500 วัน"
flavor: "เพื่อนของผมนะมักจะกล่าวว่าผมนะชอบจดโน้ต"
_login600:
title: "ผู้เชี่ยวชาญ II"
description: "เข้าสู่ระบบเป็นเวลารวม 600 วัน"
_login700:
title: "ผู้เชี่ยวชาญ III"
description: "เข้าสู่ระบบเป็นเวลารวม 700 วัน"
_login800:
title: "ปรมาจารย์ด้านโน้ต I"
description: "เข้าสู่ระบบเป็นเวลารวม 800 วัน"
_login900:
title: "ปรมาจารย์ด้านโน้ต II"
description: "เข้าสู่ระบบเป็นเวลารวม 900 วัน"
_login1000:
title: "ปรมาจารย์ด้านโน้ต III"
description: "เข้าสู่ระบบเป็นเวลารวม 1,000 วัน"
flavor: "ขอบคุณที่ใช้ Misskey นะ !"
_noteClipped1:
title: "จะต้อง... คลิป..."
description: "คลิปโน้ตตัวแรกของคุณ"
_noteFavorited1:
title: "สตาร์เกเซอร์"
description: "ชื่นชอบโน้ตแรกของคุณ"
_myNoteFavorited1:
title: "แสวงหาดวงดาว"
description: "มีคนอื่นๆที่ชื่นชอบหนึ่งในโน้ตของคุณ"
_profileFilled:
title: "เตรียมไว้อย่างดี"
description: "ตั้งค่าโปรไฟล์ของคุณ"
_markedAsCat:
title: "ฉันเป็นแมว"
description: "ทำเครื่องหมายบัญชีของคุณว่าเป็นแมว"
flavor: "ฉันจะให้ชื่อคุณภายหลังนะ"
_following1:
title: "กำลังติดตามผู้ใช้คนแรกของคุณ"
description: "ติดตามผู้ใช้"
_following10:
title: "ทำต่อไป... ทำต่อไป..."
description: "ติดตาม 10 บัญชีผู้ใช้"
_following50:
title: "มีเพื่อนมากมาย"
description: "ติดตาม 50 บัญชี"
_following100:
title: "เพื่อน 100 คน"
description: "ติดตาม 100 บัญชี"
_following300:
title: "เพื่อนโอเวอร์โหลด"
description: "ติดตาม 300 บัญชี"
_followers1:
title: "ผู้ติดตามคนแรก"
description: "ได้รับ 1 ผู้ติดตาม"
_followers10:
title: "ติดตามฉัน!"
description: "ได้รับ 10 คนผู้ติดตาม"
_followers50:
title: "มากันเป็นฝูง"
description: "ได้รับ 50 ผู้ติดตาม"
_followers100:
title: "บุคคลที่เป็นที่นิยม"
description: "ได้รับ 100 ผู้ติดตาม"
_followers300:
title: "กรุณาสร้างบรรทัดเดียวนะคะ"
description: "ได้รับ 300 คนผู้ติดตาม"
_followers500:
title: "เสาสัญญาณ"
description: "ได้รับ 500 คนผู้ติดตาม"
_followers1000:
title: "ผู้ทรงอิทธิพล"
description: "ได้รับ 1,000 ผู้ติดตาม"
_collectAchievements30:
title: "นักสะสมความสำเร็จ"
description: "ได้รับความสำเร็จ 30 ครั้ง"
_viewAchievements3min:
title: "ชอบบรรลุผลสําเร็จ"
description: "มองดูรายการความสำเร็จของคุณเป็นเวลาอย่างน้อย 3 นาที"
_iLoveMisskey:
title: "ฉันรัก Misskey"
description: "โพสต์ \"I ❤ #Misskey\""
flavor: "ทีมผู้พัฒนา Misskey ได้ขอบคุณสำหรับการสนับสนุนของคุณ!"
_foundTreasure:
title: "ล่าสมบัติ"
description: "คุณพบสมบัติที่ซ่อนอยู่"
_client30min:
title: "พักผ่อนสักหน่อย"
description: "ใช้เวลา 30 นาทีบน Misskey"
_noteDeletedWithin1min:
title: "ไม่เป็นไร"
description: "ลบโน้ตภายในหนึ่งนาทีหลังจากที่โพสต์"
_postedAtLateNight:
title: "กลางคืน"
description: "โพสต์โน้ตตอนดึกๆ"
flavor: "ได้เวลาเข้านอนแล้วนะ"
_postedAt0min0sec:
title: "นาฬิกาพูดได้"
description: "โพสต์บนโน้ตเมื่อเวลา 00:00 น."
flavor: "คลิก คลิก คลิก แกล๊งๆ"
_selfQuote:
title: "อ้างอิงตนเอง"
description: "อ้างโน้ตย่อของคุณเอง"
_htl20npm:
title: "ไทม์ไลน์ไหล"
description: "มีการทำความเร็วของไทม์ไลน์ที่บ้านของคุณเกิน 20 npm (โน้ตต่อนาที)"
_viewInstanceChart:
title: "วิเคราะห์"
description: "ดูแผนภูมิอินสแตนซ์ของคุณ"
_outputHelloWorldOnScratchpad:
title: "หวัดดีชาวโลก!"
description: "เอาพุต \"hello world\" ใน Scratchpad"
_open3windows:
title: "มัลติวินโดว์"
description: "มีการเปิดหน้าต่างอย่างน้อย 3 หน้าต่างพร้อมกัน"
_driveFolderCircularReference:
title: "อ้างอิงวงจร"
description: "พยายามสร้างโฟลเดอร์ที่ซ้อนกันแบบวนซ้ำในไดรฟ์"
_reactWithoutRead:
title: "คุณอ่านมันจริงๆหรือเปล่า?"
description: "มีการโต้ตอบกับโน้ตที่มีความยาวมากกว่า 100 ตัวอักษรภายใน 3 วินาทีหลังจากที่โพสต์"
_clickedClickHere:
title: "คลิ๊กที่นี่"
description: "คุณได้คลิกที่นี่"
_justPlainLucky:
title: "แค่ลัคกี้ธรรมดา"
description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.01% ทุก ๆ 10 วินาที"
_setNameToSyuilo:
title: "พระเจ้าคอมเพล็กซ์"
description: "ตั้งชื่อของคุณเป็น \"syuilo\""
_passedSinceAccountCreated1:
title: "ครบรอบหนึ่งปี"
description: "ผ่านไปหนึ่งปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
_passedSinceAccountCreated2:
title: "ครบรอบสองปี"
description: "ผ่านไปสองปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
_passedSinceAccountCreated3:
title: "ครบรอบสามปี"
description: "ผ่านไปสามปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
_loggedInOnBirthday:
title: "สุขสันต์วันเกิด"
description: "เข้าสู่ระบบในวันเกิดของคุณ"
_loggedInOnNewYearsDay:
title: "สวัสดีปีใหม่!"
description: "เข้าสู่ระบบในวันแรกของปี"
flavor: "อีกปีที่ยอดเยี่ยมในโอกาสนี้เลย"
_cookieClicked:
title: "เกมที่คุณคลิกที่คุกกี้"
description: "คลิกคุกกี้"
flavor: "เดี๋ยวก่อนนะ คุณอยู่ในเว็บไซต์ที่ถูกต้องแน่อย่างงั้นเหรอ?"
_brainDiver:
title: "Brain Diver"
description: "โพสต์ลิงก์ไปยัง Brain Diver"
flavor: "Misskey-Misskey La-Tu-Ma"
_role:
new: "บทบาทใหม่"

View File

@@ -529,7 +529,7 @@ state: "Стан"
sort: "Сортування"
ascendingOrder: "За зростанням"
descendingOrder: "За спаданням"
scratchpad: "Чернетка"
scratchpad: "Scratchpad"
scratchpadDescription: "Scratchpad надає середовище для експериментів з AiScript. Ви можете писати, виконувати його і тестувати взаємодію з Misskey."
output: "Вихід"
script: "Скрипт"
@@ -904,7 +904,7 @@ _achievements:
earnedAt: "Відкрито"
_types:
_notes1:
title: "налаштовую свій msky"
title: "Привіт, Misskey!"
description: "Перша нотатка"
flavor: "Приємного часу з Misskey!"
_notes10:
@@ -1042,6 +1042,7 @@ _achievements:
title: "Популярна особа"
description: "Кількість підписників досягла 100"
_followers300:
title: "Ставайте в чергу"
description: "Кількість підписників досягла 300"
_followers500:
title: "Радіовежа"
@@ -1049,19 +1050,70 @@ _achievements:
_followers1000:
title: "Інфлюенсер"
description: "Кількість підписників досягла 1000"
_collectAchievements30:
title: "Збирач досягнень"
description: "Отримано 30 досягнень"
_viewAchievements3min:
title: "Шанувальник досягнень"
description: "Переглядати список досягнень принаймні 3 хвилини"
_iLoveMisskey:
title: "I Love Misskey"
description: "Відправлено \"I ❤ #Misskey\""
flavor: "Дякуємо вам, що користуєтесь Misskey! команда розробників"
_foundTreasure:
title: "Пошуки скарбів"
description: "Ви знайшли прихований скарб"
_client30min:
title: "Коротка перерва"
description: "З моменту запуску клієнта минуло 30 хвилин"
_noteDeletedWithin1min:
title: "Не зважай"
description: "Допис видалено протягом 1 хвилини після публікації"
_postedAtLateNight:
title: "Нічне життя"
description: "Відправити нотатку посеред ночі"
flavor: "Час лягати спати"
_postedAt0min0sec:
title: "Сигнал часу"
description: "Відправити нотатку о 00:00"
_selfQuote:
title: "Самопосилання"
description: "Процитувати власну нотатку"
_htl20npm:
title: "Плинна стрічка"
description: "Перевищити швидкість домашньої стрічки 20npm (нотаток на хвилину)"
_viewInstanceChart:
title: "Аналітик"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
description: "Вивести \"hello world\" у Скретчпаді"
_clickedClickHere:
title: "Натисніть тут"
description: "Натиснуто тут"
_justPlainLucky:
title: "Просто вдача"
description: "Можна отримати з ймовірністю 0,01% кожні 10 секунд"
_setNameToSyuilo:
title: "Комплекс бога"
description: "Встановлено ім'я \"syuilo\""
_passedSinceAccountCreated1:
title: "Перша річниця"
description: "Минув рік з моменту створення акаунта"
_passedSinceAccountCreated2:
title: "Друга річниця"
description: "Минуло 2 роки з моменту створення акаунта"
_passedSinceAccountCreated3:
title: "Третя річниця"
description: "Минуло 3 роки з моменту створення акаунта"
_loggedInOnBirthday:
title: "З Днем народження!"
description: "Увійти у свій день народження"
_loggedInOnNewYearsDay:
title: "З Новим роком!"
description: "Увійшли в перший день року"
_brainDiver:
title: "Brain Diver"
description: "Відправити посилання на \"Brain Diver\""
flavor: "Misskey-Misskey La-Tu-Ma"
_role:
priority: "Пріоритет"
@@ -1335,7 +1387,7 @@ _tutorial:
step3_1: "Ви успішно налаштували свій обліковий запис?"
step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані."
step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми."
step3_4: "Не знаєте що написати? Спробуйте \"налаштовую свій msky\"!"
step3_4: "Не знаєте що написати? Спробуйте \"Привіт, Misskey!\""
step4_1: "Ви розмістили свій перший запис?"
step4_2: "Ура! Ваш перший запис відображається на вашій стрічці подій."
step5_1: "Настав час оживити вашу стрічку подій підписавшись на інших користувачів."

View File

@@ -995,52 +995,163 @@ _achievements:
_login3:
title: "初学者 I"
description: "连续登录3天"
flavor: "今天开始我就是Misskist"
_login7:
title: "初学者 II"
description: "连续登录7天"
flavor: "您开始习惯了吗?"
_login15:
title: "初学者 III"
description: "连续登录15天"
_login30:
title: "Misskist "
description: "连续登录30天"
_login60:
title: "Misskist Ⅱ"
description: "连续登录60天"
_login100:
title: "Misskist Ⅲ"
description: "总登入100天"
flavor: "那个用户是Misskist喔"
_login200:
title: "定期联系Ⅰ"
description: "总登录天数200天"
_login300:
title: "定期联系Ⅱ"
description: "总登录天数300天"
_login400:
title: "定期联系Ⅲ"
description: "总登录天数400天"
_login500:
description: "总登录天数500天"
flavor: "诸君,我喜欢贴文"
_login600:
description: "总登录天数600天"
_login700:
description: "总登录天数700天"
_login800:
description: "总登录天数800天"
_login900:
description: "总登录天数900天"
_login1000:
description: "总登录天数1000天"
flavor: "感谢您使用Misskey"
_noteClipped1:
title: "忍不住要收藏到便签"
description: "第一次将贴文贴进便签"
_noteFavorited1:
title: "观星者"
description: "第一次将帖子加入收藏"
_myNoteFavorited1:
title: "想要星星"
description: "自己的帖子被其他人加入收藏了"
_profileFilled:
title: "整装待发"
description: "设置了个人资料"
_markedAsCat:
title: "我是猫"
description: "将账户设定为一只猫"
flavor: "还没有名字"
_following1:
title: "首次关注"
description: "第一次关注别人"
_following10:
title: "关注,跟随"
description: "关注超过10人"
_following50:
title: "我的朋友很多"
description: "关注超过50人"
_following100:
title: "我的朋友很多"
description: "关注超过100人"
_following300:
title: "朋友成群"
description: "关注数超过300"
_followers1:
title: "最初的关注者"
description: "第一次被关注"
_followers10:
title: "关注我吧!"
description: "拥有超过10名关注者"
_followers50:
title: "三五成群"
description: "拥有超过50名关注者"
_followers100:
title: "胜友如云"
description: "拥有超过100名关注者"
_followers300:
title: "排列成行"
description: "拥有超过300名关注者"
_followers500:
title: "信号塔"
description: "拥有超过500名关注者"
_followers1000:
description: "拥有超过1000名关注者"
_collectAchievements30:
title: "成就收藏家"
description: "获得超过30个成就"
_viewAchievements3min:
title: "成就爱好者"
description: "盯着成就看三分钟"
_iLoveMisskey:
title: "I Love Misskey"
description: "发布\"I ❤ #Misskey\"帖子"
flavor: "感谢您使用 Misskey by 开发团队"
_foundTreasure:
title: "寻宝"
description: "发现了隐藏的宝藏"
_client30min:
title: "休息一下!"
description: "启动客户端超过30分钟"
_noteDeletedWithin1min:
title: "无话可说"
description: "发帖后一分钟内就将其删除"
_postedAtLateNight:
title: "夜行者"
title: "夜猫子"
description: "深夜发布帖子"
flavor: "差不多该去睡了喔。"
_postedAt0min0sec:
title: "报时"
description: "在0点发布一篇帖子"
flavor: "嘣 嘣 嘣 Biu——"
_selfQuote:
title: "自我提及"
description: "引用了自己的帖子"
_htl20npm:
title: "流动的时间线"
description: "在首页时间线的流速超过20npm"
_viewInstanceChart:
title: "分析师"
description: "查看了实例信息中的图表"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
description: "在AiScript控制台中输出 hello world"
_open3windows:
title: "多窗口"
description: "打开了三个或更多的窗口"
_driveFolderCircularReference:
title: "循环引用"
description: "试图对网盘中的文件夹进行循环嵌套"
_reactWithoutRead:
title: "有好好读过吗?"
description: "在含有100字以上的帖子被发出三秒内做出回应"
_clickedClickHere:
title: "点这里"
description: "点了这里"
_justPlainLucky:
title: "超高校级的幸运"
description: "每10秒有0.01的概率自动获得"
_setNameToSyuilo:
title: "像神一样呐"
description: "将名称设定为syuilo"
_passedSinceAccountCreated1:
title: "一周年"
description: "账户创建时间超过1年"
_passedSinceAccountCreated2:
title: "二周年"
description: "账户创建时间超过2年"
_passedSinceAccountCreated3:
title: "三周年"
description: "账户创建时间超过3年"
_loggedInOnBirthday:
title: "生日快乐"
@@ -1048,6 +1159,15 @@ _achievements:
_loggedInOnNewYearsDay:
title: "恭贺新禧"
description: "在元旦登入"
flavor: "今年也请对本实例多多指教!"
_cookieClicked:
title: "点击饼干小游戏"
description: "点击了可疑的饼干"
flavor: "是不是软件有问题?"
_brainDiver:
title: "Brain Diver"
description: "发布了包含Brain Diver链接的帖子"
flavor: "Misskey-Misskey La-Tu-Ma"
_role:
new: "创建角色"
edit: "编辑角色"
@@ -1566,7 +1686,7 @@ _profile:
name: "昵称"
username: "用户名"
description: "个人简介"
youCanIncludeHashtags: "可以包含一个哈希标签。"
youCanIncludeHashtags: "可以在个人简介中包含一个#标签。"
metadata: "附加信息"
metadataEdit: "附加信息编辑"
metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。"

View File

@@ -240,7 +240,7 @@ removeAreYouSure: "確定要刪掉「{x}」嗎?"
deleteAreYouSure: "確定要刪掉「{x}」嗎?"
resetAreYouSure: "確定要重設嗎?"
saved: "已儲存"
messaging: "傳送訊息"
messaging: "聊天"
upload: "上傳"
keepOriginalUploading: "保留原圖"
keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時瀏覽器會在上傳時生成一張用於web發布的圖片。"
@@ -331,10 +331,10 @@ registration: "註冊"
enableRegistration: "開啟新使用者註冊"
invite: "邀請"
driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小"
driveCapacityPerRemoteAccount: "每個非本地用戶的雲端容量"
driveCapacityPerRemoteAccount: "每個非本地用戶的雲端空間大小"
inMb: "以Mbps為單位"
iconUrl: "圖URL"
bannerUrl: "橫幅圖URL"
iconUrl: "圖URL"
bannerUrl: "橫幅圖URL"
backgroundImageUrl: "背景圖片的來源網址 "
basicInfo: "基本資訊"
pinnedUsers: "置頂用戶"
@@ -373,8 +373,8 @@ connectedTo: "您的帳戶已連接到以下社交帳戶"
notesAndReplies: "貼文與回覆"
withFiles: "附件"
silence: "禁言"
silenceConfirm: "確定要禁言此用戶嗎?"
unsilence: "解除禁言"
silenceConfirm: "確定要靜音此使用者嗎?"
unsilence: "解除靜音"
unsilenceConfirm: "確定要解除禁言嗎?"
popularUsers: "熱門使用者"
recentlyUpdatedUsers: "最近發文的使用者"
@@ -383,13 +383,13 @@ recentlyDiscoveredUsers: "最近發現的使用者"
exploreUsersCount: "有{count}個使用者"
exploreFediverse: "探索聯邦世界"
popularTags: "熱門標籤"
userList: "清單"
about: "資訊"
userList: "使用者清單"
about: "關於"
aboutMisskey: "關於 Misskey"
administrator: "管理員"
token: "權杖"
twoStepAuthentication: "兩階段驗證"
moderator: "監察員"
moderator: "審核員"
moderation: "監察"
nUsersMentioned: "提到了{n}"
securityKey: "安全金鑰"
@@ -421,7 +421,7 @@ invites: "邀請"
groupName: "群組名稱"
members: "成員"
transfer: "轉讓"
messagingWithUser: "傳送訊息給其他使用者"
messagingWithUser: "其他使用者聊天"
messagingWithGroup: "發送訊息至群組"
title: "標題"
text: "文字"
@@ -473,7 +473,7 @@ createAccount: "建立帳戶"
existingAccount: "現有帳戶"
regenerate: "再生"
fontSize: "字體大小"
noFollowRequests: "沒有要求跟隨您的請"
noFollowRequests: "沒有跟隨您的請"
openImageInNewTab: "於新分頁中開啟圖片"
dashboard: "儀表板"
local: "本地"
@@ -530,8 +530,8 @@ installedDate: "安裝時間"
lastUsedDate: "最後上線日期"
state: "狀態"
sort: "排序"
ascendingOrder: "昇冪"
descendingOrder: "降冪"
ascendingOrder: "遞增"
descendingOrder: "遞減"
scratchpad: "暫存記憶體"
scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。"
output: "輸出"
@@ -995,24 +995,24 @@ _achievements:
_login3:
title: "初學者Ⅰ"
description: "總登入天數為3天"
flavor: "從今天開始我就是Misskeyist"
flavor: "從今天開始我就是Misskist"
_login7:
title: "初學者ⅠⅠ"
description: "總登入天數為7天"
flavor: "您開始習慣了嗎?"
_login15:
title: "初學者III"
title: "初學者"
description: "總登入天數為15天"
_login30:
title: "Misskeyist "
title: "Misskist "
description: "總登入天數為30天"
_login60:
title: "Misskeyist "
title: "Misskist "
description: "總登入天數為60天"
_login100:
title: "Misskeyist "
title: "Misskist "
description: "總登入天數為100天"
flavor: "辣個 Misskeyist 用戶"
flavor: "辣個 Misskist 用戶"
_login200:
title: "普通Ⅰ"
description: "總登入天數為200天"
@@ -1089,7 +1089,7 @@ _achievements:
title: "請排成一排"
description: "跟隨者超過300人了"
_followers500:
title: "基"
title: "基地台"
description: "超過500名追隨者了"
_followers1000:
title: "影響者"
@@ -1111,7 +1111,7 @@ _achievements:
title: "休息一下"
description: "用戶端啟動已超過30分鐘"
_noteDeletedWithin1min:
title: "現在沒有"
title: "現在沒有"
description: "發文後1分鐘內刪文"
_postedAtLateNight:
title: "夜行性"

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "13.2.4",
"version": "13.3.2",
"codename": "nasubi",
"repository": {
"type": "git",
@@ -38,8 +38,8 @@
"cleanall": "pnpm clean-all"
},
"resolutions": {
"chokidar": "^3.5.3",
"lodash": "^4.17.21"
"chokidar": "3.5.3",
"lodash": "4.17.21"
},
"dependencies": {
"execa": "5.1.1",
@@ -49,19 +49,19 @@
"gulp-replace": "1.1.4",
"gulp-terser": "2.1.0",
"js-yaml": "4.1.0",
"typescript": "4.9.4"
"typescript": "4.9.5"
},
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.49.0",
"@typescript-eslint/parser": "5.49.0",
"@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.50.0",
"cross-env": "7.0.3",
"cypress": "12.4.0",
"eslint": "^8.32.0",
"cypress": "12.5.1",
"eslint": "8.33.0",
"start-server-and-test": "1.15.3"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "^4.2.0"
"@tensorflow/tfjs-core": "4.2.0"
}
}

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

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

View File

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

View File

@@ -10,10 +10,9 @@ import { isUserRelated } from '@/misc/is-user-related.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
import * as Acct from '@/misc/acct.js';
import { Cache } from '@/misc/cache.js';
import type { Packed } from '@/misc/schema.js';
import { DI } from '@/di-symbols.js';
import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
@@ -23,7 +22,6 @@ import type { OnApplicationShutdown } from '@nestjs/common';
export class AntennaService implements OnApplicationShutdown {
private antennasFetched: boolean;
private antennas: Antenna[];
private blockingCache: Cache<User['id'][]>;
constructor(
@Inject(DI.redisSubscriber)
@@ -32,9 +30,6 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -52,14 +47,13 @@ export class AntennaService implements OnApplicationShutdown {
private utilityService: UtilityService,
private idService: IdService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService,
private noteEntityService: NoteEntityService,
private antennaEntityService: AntennaEntityService,
) {
this.antennasFetched = false;
this.antennas = [];
this.blockingCache = new Cache<User['id'][]>(1000 * 60 * 5);
this.redisSubscriber.on('message', this.onRedisMessage);
}
@@ -109,7 +103,7 @@ export class AntennaService implements OnApplicationShutdown {
read: read,
});
this.globalEventServie.publishAntennaStream(antenna.id, 'note', note);
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
if (!read) {
const mutings = await this.mutingsRepository.find({
@@ -139,7 +133,7 @@ export class AntennaService implements OnApplicationShutdown {
setTimeout(async () => {
const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false });
if (unread) {
this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
antenna: { id: antenna.id, name: antenna.name },
note: await this.noteEntityService.pack(note),
@@ -155,10 +149,6 @@ export class AntennaService implements OnApplicationShutdown {
public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> {
if (note.visibility === 'specified') return false;
if (note.visibility === 'followers') return false;
// アンテナ作成者がノート作成者にブロックされていたらスキップ
const blockings = await this.blockingCache.fetch(noteUser.id, () => this.blockingsRepository.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId)));
if (blockings.some(blocking => blocking === antenna.userId)) return false;
if (!antenna.withReplies && note.replyId != null) return false;

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -95,7 +95,7 @@ export class HttpRequestService {
}
@bindThis
public async getJson(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<unknown> {
public async getJson<T = unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<T> {
const res = await this.send(url, {
method: 'GET',
headers: Object.assign({
@@ -106,7 +106,7 @@ export class HttpRequestService {
size: 1024 * 256,
});
return await res.json();
return await res.json() as T;
}
@bindThis

View File

@@ -1,5 +1,4 @@
import { Inject, Injectable } from '@nestjs/common';
import * as SyslogPro from 'syslog-pro';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import Logger from '@/logger.js';
@@ -8,29 +7,14 @@ import type { KEYWORD } from 'color-convert/conversions';
@Injectable()
export class LoggerService {
private syslogClient;
constructor(
@Inject(DI.config)
private config: Config,
) {
if (this.config.syslog) {
this.syslogClient = new SyslogPro.RFC5424({
applicationName: 'Misskey',
timestamp: true,
includeStructuredData: true,
color: true,
extendedColor: true,
server: {
target: config.syslog.host,
port: config.syslog.port,
},
});
}
}
@bindThis
public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
return new Logger(domain, color, store, this.syslogClient);
return new Logger(domain, color, store);
}
}

View File

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

View File

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

View File

@@ -9,9 +9,9 @@ import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { NotificationService } from './NotificationService.js';
import { AntennaService } from './AntennaService.js';
import { bindThis } from '@/decorators.js';
import { PushNotificationService } from './PushNotificationService.js';
@Injectable()
@@ -40,7 +40,7 @@ export class NoteReadService {
private userEntityService: UserEntityService,
private idService: IdService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
private notificationService: NotificationService,
private antennaService: AntennaService,
private pushNotificationService: PushNotificationService,
@@ -87,13 +87,13 @@ export class NoteReadService {
if (exist == null) return;
if (params.isMentioned) {
this.globalEventServie.publishMainStream(userId, 'unreadMention', note.id);
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
}
if (params.isSpecified) {
this.globalEventServie.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
}
if (note.channelId) {
this.globalEventServie.publishMainStream(userId, 'unreadChannel', note.id);
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
}
}, 2000);
}
@@ -107,12 +107,6 @@ export class NoteReadService {
followingChannels: Set<Channel['id']>;
},
): Promise<void> {
const following = info?.following ? info.following : new Set<string>((await this.followingsRepository.find({
where: {
followerId: userId,
},
select: ['followeeId'],
})).map(x => x.followeeId));
const followingChannels = info?.followingChannels ? info.followingChannels : new Set<string>((await this.channelFollowingsRepository.find({
where: {
followerId: userId,
@@ -139,7 +133,7 @@ export class NoteReadService {
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
for (const antenna of myAntennas) {
if (await this.antennaService.checkHitAntenna(antenna, note, note.user, undefined, Array.from(following))) {
if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) {
readAntennaNotes.push(note);
}
}
@@ -161,7 +155,7 @@ export class NoteReadService {
}).then(mentionsCount => {
if (mentionsCount === 0) {
// 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllUnreadMentions');
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
}
});
@@ -171,7 +165,7 @@ export class NoteReadService {
}).then(specifiedCount => {
if (specifiedCount === 0) {
// 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
}
});
@@ -181,7 +175,7 @@ export class NoteReadService {
}).then(channelNoteCount => {
if (channelNoteCount === 0) {
// 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllChannels');
this.globalEventService.publishMainStream(userId, 'readAllChannels');
}
});
@@ -206,14 +200,14 @@ export class NoteReadService {
});
if (count === 0) {
this.globalEventServie.publishMainStream(userId, 'readAntenna', antenna);
this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
}
}
this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
if (!unread) {
this.globalEventServie.publishMainStream(userId, 'readAllAntennas');
this.globalEventService.publishMainStream(userId, 'readAllAntennas');
this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
}
});

View File

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

View File

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

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 type { CacheableUser, User } from '@/models/entities/User.js';
import type { Blocking } from '@/models/entities/Blocking.js';
@@ -7,7 +8,6 @@ import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { DI } from '@/di-symbols.js';
import logger from '@/logger.js';
import type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
import Logger from '@/logger.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -15,12 +15,20 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { bindThis } from '@/decorators.js';
import { Cache } from '@/misc/cache.js';
import { StreamMessages } from '@/server/api/stream/types.js';
@Injectable()
export class UserBlockingService {
export class UserBlockingService implements OnApplicationShutdown {
private logger: Logger;
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
private blockingsByUserIdCache: Cache<User['id'][]>;
constructor(
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -42,13 +50,44 @@ export class UserBlockingService {
private userEntityService: UserEntityService,
private idService: IdService,
private queueService: QueueService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
private webhookService: WebhookService,
private apRendererService: ApRendererService,
private perUserFollowingChart: PerUserFollowingChart,
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('user-block');
this.blockingsByUserIdCache = new Cache<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
@@ -72,6 +111,11 @@ export class UserBlockingService {
await this.blockingsRepository.insert(blocking);
this.globalEventService.publishInternalEvent('blockingCreated', {
blockerId: blocker.id,
blockeeId: blockee.id,
});
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking));
this.queueService.deliver(blocker, content, blockee.inbox);
@@ -97,15 +141,15 @@ export class UserBlockingService {
if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(followee, followee, {
detail: true,
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
}
if (this.userEntityService.isLocalUser(follower)) {
this.userEntityService.pack(followee, follower, {
detail: true,
}).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed);
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
@@ -152,8 +196,8 @@ export class UserBlockingService {
this.userEntityService.pack(followee, follower, {
detail: true,
}).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed);
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
@@ -210,10 +254,31 @@ export class UserBlockingService {
await this.blockingsRepository.delete(blocking.id);
this.globalEventService.publishInternalEvent('blockingDeleted', {
blockerId: blocker.id,
blockeeId: blockee.id,
});
// deliver if remote bloking
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
this.queueService.deliver(blocker, content, blockee.inbox);
}
}
@bindThis
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<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 { CreateNotificationService } from '@/core/CreateNotificationService.js';
import { DI } from '@/di-symbols.js';
import type { BlockingsRepository, FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import Logger from '../logger.js';
const logger = new Logger('following/create');
@@ -48,21 +49,18 @@ export class UserFollowingService {
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
private userEntityService: UserEntityService,
private userBlockingService: UserBlockingService,
private idService: IdService,
private queueService: QueueService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
private createNotificationService: CreateNotificationService,
private federatedInstanceService: FederatedInstanceService,
private webhookService: WebhookService,
private apRendererService: ApRendererService,
private globalEventService: GlobalEventService,
private perUserFollowingChart: PerUserFollowingChart,
private instanceChart: InstanceChart,
) {
@@ -77,26 +75,20 @@ export class UserFollowingService {
// check blocking
const [blocking, blocked] = await Promise.all([
this.blockingsRepository.findOneBy({
blockerId: follower.id,
blockeeId: followee.id,
}),
this.blockingsRepository.findOneBy({
blockerId: followee.id,
blockeeId: follower.id,
}),
this.userBlockingService.checkBlocked(follower.id, followee.id),
this.userBlockingService.checkBlocked(followee.id, follower.id),
]);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) {
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee));
this.queueService.deliver(followee, content, follower.inbox);
return;
} else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) {
// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
await this.blockingsRepository.delete(blocking.id);
// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
await this.userBlockingService.unblock(follower, followee);
} else {
// それ以外は単純に例外
// それ以外は単純に例外
if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking');
if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
}
@@ -227,8 +219,8 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, follower, {
detail: true,
}).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
this.globalEventServie.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
for (const webhook of webhooks) {
@@ -242,7 +234,7 @@ export class UserFollowingService {
// Publish followed event
if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(follower.id, followee).then(async packed => {
this.globalEventServie.publishMainStream(followee.id, 'followed', packed);
this.globalEventService.publishMainStream(followee.id, 'followed', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
for (const webhook of webhooks) {
@@ -288,8 +280,8 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, follower, {
detail: true,
}).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed);
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
@@ -357,14 +349,8 @@ export class UserFollowingService {
// check blocking
const [blocking, blocked] = await Promise.all([
this.blockingsRepository.findOneBy({
blockerId: follower.id,
blockeeId: followee.id,
}),
this.blockingsRepository.findOneBy({
blockerId: followee.id,
blockeeId: follower.id,
}),
this.userBlockingService.checkBlocked(follower.id, followee.id),
this.userBlockingService.checkBlocked(followee.id, follower.id),
]);
if (blocking != null) throw new Error('blocking');
@@ -388,11 +374,11 @@ export class UserFollowingService {
// Publish receiveRequest event
if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventServie.publishMainStream(followee.id, 'receiveFollowRequest', packed));
this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed));
this.userEntityService.pack(followee.id, followee, {
detail: true,
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
// 通知を作成
this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', {
@@ -440,7 +426,7 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, followee, {
detail: true,
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
}
@bindThis
@@ -468,7 +454,7 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, followee, {
detail: true,
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
}
@bindThis
@@ -583,8 +569,8 @@ export class UserFollowingService {
detail: true,
});
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packedFollowee);
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packedFollowee);
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packedFollowee);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {

View File

@@ -25,7 +25,7 @@ export class UserListService {
private idService: IdService,
private userFollowingService: UserFollowingService,
private roleService: RoleService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
private proxyAccountService: ProxyAccountService,
) {
}
@@ -46,7 +46,7 @@ export class UserListService {
userListId: list.id,
} as UserListJoining);
this.globalEventServie.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
if (this.userEntityService.isRemoteUser(target)) {

View File

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

View File

@@ -44,16 +44,25 @@ export class WebhookService implements OnApplicationShutdown {
switch (type) {
case 'webhookCreated':
if (body.active) {
this.webhooks.push(body);
this.webhooks.push({
...body,
createdAt: new Date(body.createdAt),
});
}
break;
case 'webhookUpdated':
if (body.active) {
const i = this.webhooks.findIndex(a => a.id === body.id);
if (i > -1) {
this.webhooks[i] = body;
this.webhooks[i] = {
...body,
createdAt: new Date(body.createdAt),
};
} else {
this.webhooks.push(body);
this.webhooks.push({
...body,
createdAt: new Date(body.createdAt),
});
}
} else {
this.webhooks = this.webhooks.filter(a => a.id !== body.id);

View File

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

View File

@@ -1,5 +1,6 @@
import * as crypto from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import jsonld from 'jsonld';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
import { CONTEXTS } from './misc/contexts.js';
@@ -84,7 +85,9 @@ class LdSignature {
@bindThis
public async normalize(data: any) {
const customLoader = this.getLoader();
return 42;
return await jsonld.normalize(data, {
documentLoader: customLoader,
});
}
@bindThis

View File

@@ -29,6 +29,7 @@ import { UserNotePining } from '@/models/entities/UserNotePining.js';
import { StatusError } from '@/misc/status-error.js';
import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common';
@@ -43,37 +44,6 @@ import type { IActor, IObject, IApPropertyValue } from '../type.js';
const nameLength = 128;
const summaryLength = 2048;
const services: {
[x: string]: (id: string, username: string) => any
} = {
'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }),
'misskey:authentication:github': (id, login) => ({ id, login }),
'misskey:authentication:discord': (id, name) => $discord(id, name),
};
const $discord = (id: string, name: string) => {
if (typeof name !== 'string') {
name = 'unknown#0000';
}
const [username, discriminator] = name.split('#');
return { id, username, discriminator };
};
function addService(target: { [x: string]: any }, source: IApPropertyValue) {
const service = services[source.name];
if (typeof source.value !== 'string') {
source.value = 'unknown';
}
const [id, username] = source.value.split('@');
if (service) {
target[source.name.split(':')[2]] = service(id, username);
}
}
import { bindThis } from '@/decorators.js';
@Injectable()
export class ApPersonService implements OnModuleInit {
private utilityService: UtilityService;
@@ -540,22 +510,16 @@ export class ApPersonService implements OnModuleInit {
name: string,
value: string
}[] = [];
const services: { [x: string]: any } = {};
if (Array.isArray(attachments)) {
for (const attachment of attachments.filter(isPropertyValue)) {
if (isPropertyValue(attachment.identifier)) {
addService(services, attachment.identifier);
} else {
fields.push({
name: attachment.name,
value: this.mfmService.fromHtml(attachment.value),
});
}
fields.push({
name: attachment.name,
value: this.mfmService.fromHtml(attachment.value),
});
}
}
return { fields, services };
return { fields };
}
@bindThis

View File

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

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

@@ -11,9 +11,9 @@ import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import type { Repository, DataSource } from 'typeorm';
const columnPrefix = '___' as const;
const uniqueTempColumnPrefix = 'unique_temp___' as const;
const columnDot = '_' as const;
const COLUMN_PREFIX = '___' as const;
const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
const COLUMN_DELIMITER = '_' as const;
type Schema = Record<string, {
uniqueIncrement?: boolean;
@@ -26,14 +26,14 @@ type Schema = Record<string, {
accumulate?: boolean;
}>;
type KeyToColumnName<T extends string> = T extends `${infer R1}.${infer R2}` ? `${R1}${typeof columnDot}${KeyToColumnName<R2>}` : T;
type KeyToColumnName<T extends string> = T extends `${infer R1}.${infer R2}` ? `${R1}${typeof COLUMN_DELIMITER}${KeyToColumnName<R2>}` : T;
type Columns<S extends Schema> = {
[K in keyof S as `${typeof columnPrefix}${KeyToColumnName<string & K>}`]: number;
[K in keyof S as `${typeof COLUMN_PREFIX}${KeyToColumnName<string & K>}`]: number;
};
type TempColumnsForUnique<S extends Schema> = {
[K in keyof S as `${typeof uniqueTempColumnPrefix}${KeyToColumnName<string & K>}`]: S[K]['uniqueIncrement'] extends true ? string[] : never;
[K in keyof S as `${typeof UNIQUE_TEMP_COLUMN_PREFIX}${KeyToColumnName<string & K>}`]: S[K]['uniqueIncrement'] extends true ? string[] : never;
};
type RawRecord<S extends Schema> = {
@@ -138,20 +138,20 @@ export default abstract class Chart<T extends Schema> {
private static convertSchemaToColumnDefinitions(schema: Schema): Record<string, { type: string; array?: boolean; default?: any; }> {
const columns = {} as Record<string, { type: string; array?: boolean; default?: any; }>;
for (const [k, v] of Object.entries(schema)) {
const name = k.replaceAll('.', columnDot);
const name = k.replaceAll('.', COLUMN_DELIMITER);
const type = v.range === 'big' ? 'bigint' : v.range === 'small' ? 'smallint' : 'integer';
if (v.uniqueIncrement) {
columns[uniqueTempColumnPrefix + name] = {
columns[UNIQUE_TEMP_COLUMN_PREFIX + name] = {
type: 'varchar',
array: true,
default: '{}',
};
columns[columnPrefix + name] = {
columns[COLUMN_PREFIX + name] = {
type,
default: 0,
};
} else {
columns[columnPrefix + name] = {
columns[COLUMN_PREFIX + name] = {
type,
default: 0,
};
@@ -253,8 +253,8 @@ export default abstract class Chart<T extends Schema> {
@bindThis
private convertRawRecord(x: RawRecord<T>): KVs<T> {
const kvs = {} as Record<string, number>;
for (const k of Object.keys(x).filter((k) => k.startsWith(columnPrefix)) as (keyof Columns<T>)[]) {
kvs[(k as string).substr(columnPrefix.length).split(columnDot).join('.')] = x[k] as unknown as number;
for (const k of Object.keys(x).filter((k) => k.startsWith(COLUMN_PREFIX)) as (keyof Columns<T>)[]) {
kvs[(k as string).substr(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number;
}
return kvs as KVs<T>;
}
@@ -357,8 +357,8 @@ export default abstract class Chart<T extends Schema> {
const columns = {} as Record<string, number | unknown[]>;
for (const [k, v] of Object.entries(data)) {
const name = k.replaceAll('.', columnDot);
columns[columnPrefix + name] = v;
const name = k.replaceAll('.', COLUMN_DELIMITER);
columns[COLUMN_PREFIX + name] = v;
}
// 新規ログ挿入
@@ -419,13 +419,13 @@ export default abstract class Chart<T extends Schema> {
const queryForDay: Record<keyof RawRecord<T>, number | (() => string)> = {} as any;
for (const [k, v] of Object.entries(finalDiffs)) {
if (typeof v === 'number') {
const name = columnPrefix + k.replaceAll('.', columnDot) as string & keyof Columns<T>;
const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as string & keyof Columns<T>;
if (v > 0) queryForHour[name] = () => `"${name}" + ${v}`;
if (v < 0) queryForHour[name] = () => `"${name}" - ${Math.abs(v)}`;
if (v > 0) queryForDay[name] = () => `"${name}" + ${v}`;
if (v < 0) queryForDay[name] = () => `"${name}" - ${Math.abs(v)}`;
} else if (Array.isArray(v) && v.length > 0) { // ユニークインクリメント
const tempColumnName = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as string & keyof TempColumnsForUnique<T>;
const tempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as string & keyof TempColumnsForUnique<T>;
// TODO: item をSQLエスケープ
const itemsForHour = v.filter(item => !(logHour[tempColumnName] as unknown as string[]).includes(item)).map(item => `"${item}"`);
const itemsForDay = v.filter(item => !(logDay[tempColumnName] as unknown as string[]).includes(item)).map(item => `"${item}"`);
@@ -437,8 +437,8 @@ export default abstract class Chart<T extends Schema> {
// bake unique count
for (const [k, v] of Object.entries(finalDiffs)) {
if (this.schema[k].uniqueIncrement) {
const name = columnPrefix + k.replaceAll('.', columnDot) as keyof Columns<T>;
const tempColumnName = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as keyof TempColumnsForUnique<T>;
const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof Columns<T>;
const tempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
queryForHour[name] = new Set([...(v as string[]), ...(logHour[tempColumnName] as unknown as string[])]).size;
queryForDay[name] = new Set([...(v as string[]), ...(logDay[tempColumnName] as unknown as string[])]).size;
}
@@ -449,15 +449,15 @@ export default abstract class Chart<T extends Schema> {
for (const [k, v] of Object.entries(this.schema)) {
const intersection = v.intersection;
if (intersection) {
const name = columnPrefix + k.replaceAll('.', columnDot) as keyof Columns<T>;
const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof Columns<T>;
const firstKey = intersection[0];
const firstTempColumnName = uniqueTempColumnPrefix + firstKey.replaceAll('.', columnDot) as keyof TempColumnsForUnique<T>;
const firstTempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + firstKey.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
const firstValues = finalDiffs[firstKey] as string[] | undefined;
const currentValuesForHour = new Set([...(firstValues ?? []), ...(logHour[firstTempColumnName] as unknown as string[])]);
const currentValuesForDay = new Set([...(firstValues ?? []), ...(logDay[firstTempColumnName] as unknown as string[])]);
for (let i = 1; i < intersection.length; i++) {
const targetKey = intersection[i];
const targetTempColumnName = uniqueTempColumnPrefix + targetKey.replaceAll('.', columnDot) as keyof TempColumnsForUnique<T>;
const targetTempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + targetKey.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
const targetValues = finalDiffs[targetKey] as string[] | undefined;
const targetValuesForHour = new Set([...(targetValues ?? []), ...(logHour[targetTempColumnName] as unknown as string[])]);
const targetValuesForDay = new Set([...(targetValues ?? []), ...(logDay[targetTempColumnName] as unknown as string[])]);
@@ -510,7 +510,7 @@ export default abstract class Chart<T extends Schema> {
const columns = {} as Record<keyof Columns<T>, number>;
for (const [k, v] of Object.entries(data) as ([keyof typeof data, number])[]) {
const name = columnPrefix + (k as string).replaceAll('.', columnDot) as keyof Columns<T>;
const name = COLUMN_PREFIX + (k as string).replaceAll('.', COLUMN_DELIMITER) as keyof Columns<T>;
columns[name] = v;
}
@@ -556,7 +556,7 @@ export default abstract class Chart<T extends Schema> {
const columns = {} as Record<keyof TempColumnsForUnique<T>, []>;
for (const [k, v] of Object.entries(this.schema)) {
if (v.uniqueIncrement) {
const name = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as keyof TempColumnsForUnique<T>;
const name = UNIQUE_TEMP_COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
columns[name] = [];
}
}

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

View File

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

View File

@@ -71,27 +71,41 @@ export class DriveFileEntityService {
}
@bindThis
public getPublicUrl(file: DriveFile, thumbnail = false): string | null {
public getPublicUrl(file: DriveFile, mode? : 'static' | 'avatar'): string | null { // static = thumbnail
const proxiedUrl = (url: string) => appendQuery(
`${this.config.mediaProxy}/${mode ?? 'image'}.webp`,
query({
url,
...(mode ? { [mode]: '1' } : {}),
})
);
// リモートかつメディアプロキシ
if (file.uri != null && file.userHost != null && this.config.mediaProxy != null) {
return appendQuery(this.config.mediaProxy, query({
url: file.uri,
thumbnail: thumbnail ? '1' : undefined,
}));
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
return proxiedUrl(file.uri);
}
// リモートかつ期限切れはローカルプロキシを試みる
if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) {
const key = thumbnail ? file.thumbnailAccessKey : file.webpublicAccessKey;
const key = mode === 'static' ? file.thumbnailAccessKey : file.webpublicAccessKey;
if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
return `${this.config.url}/files/${key}`;
const url = `${this.config.url}/files/${key}`;
if (mode === 'avatar') return proxiedUrl(url);
return url;
}
}
const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/avif', 'image/svg+xml'].includes(file.type);
return thumbnail ? (file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null)) : (file.webpublicUrl ?? file.url);
if (mode === 'static') {
return file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null);
}
const url = file.webpublicUrl ?? file.url;
if (mode === 'avatar') return proxiedUrl(url);
return url;
}
@bindThis
@@ -166,8 +180,8 @@ export class DriveFileEntityService {
isSensitive: file.isSensitive,
blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false),
thumbnailUrl: this.getPublicUrl(file, true),
url: opts.self ? file.url : this.getPublicUrl(file),
thumbnailUrl: this.getPublicUrl(file, 'static'),
comment: file.comment,
folderId: file.folderId,
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
@@ -201,8 +215,8 @@ export class DriveFileEntityService {
isSensitive: file.isSensitive,
blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false),
thumbnailUrl: this.getPublicUrl(file, true),
url: opts.self ? file.url : this.getPublicUrl(file),
thumbnailUrl: this.getPublicUrl(file, 'static'),
comment: file.comment,
folderId: file.folderId,
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
import type { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository } from '@/models/index.js';
import type { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository, User } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Notification } from '@/models/entities/Notification.js';
import type { NoteReaction } from '@/models/entities/NoteReaction.js';

View File

@@ -314,10 +314,10 @@ export class UserEntityService implements OnModuleInit {
@bindThis
public async getAvatarUrl(user: User): Promise<string> {
if (user.avatar) {
return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id);
return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user.id);
} else if (user.avatarId) {
const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId });
return this.driveFileEntityService.getPublicUrl(avatar, true) ?? this.getIdenticonUrl(user.id);
return this.driveFileEntityService.getPublicUrl(avatar, 'avatar') ?? this.getIdenticonUrl(user.id);
} else {
return this.getIdenticonUrl(user.id);
}
@@ -326,7 +326,7 @@ export class UserEntityService implements OnModuleInit {
@bindThis
public getAvatarUrlSync(user: User): string {
if (user.avatar) {
return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id);
return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user.id);
} else {
return this.getIdenticonUrl(user.id);
}
@@ -422,7 +422,7 @@ export class UserEntityService implements OnModuleInit {
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner, false) : null,
bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner) : null,
bannerBlurhash: user.banner?.blurhash ?? null,
isLocked: user.isLocked,
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
@@ -489,7 +489,6 @@ export class UserEntityService implements OnModuleInit {
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
hasUnreadNotification: this.getHasUnreadNotification(user.id),
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
integrations: profile!.integrations,
mutedWords: profile!.mutedWords,
mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: profile!.mutingNotificationTypes,

View File

@@ -17,15 +17,13 @@ export default class Logger {
private context: Context;
private parentLogger: Logger | null = null;
private store: boolean;
private syslogClient: any | null = null;
constructor(context: string, color?: KEYWORD, store = true, syslogClient = null) {
constructor(context: string, color?: KEYWORD, store = true) {
this.context = {
name: context,
color: color,
};
this.store = store;
this.syslogClient = syslogClient;
}
@bindThis
@@ -69,20 +67,6 @@ export default class Logger {
console.log(important ? chalk.bold(log) : log);
if (level === 'error' && data) console.log(data);
if (store) {
if (this.syslogClient) {
const send =
level === 'error' ? this.syslogClient.error :
level === 'warning' ? this.syslogClient.warning :
level === 'success' ? this.syslogClient.info :
level === 'debug' ? this.syslogClient.info :
level === 'info' ? this.syslogClient.info :
null as never;
send.bind(this.syslogClient)(message).catch(() => {});
}
}
}
@bindThis

View File

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

View File

@@ -4,7 +4,7 @@ import { unique } from '@/misc/prelude/array.js';
export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] {
const emojiNodes = mfm.extract(nodes, (node) => {
return (node.type === 'emojiCode' && node.props.name.length <= 100);
});
}) as mfm.MfmEmojiCode[];
return unique(emojiNodes.map(x => x.props.name));
}

View File

@@ -2,7 +2,7 @@ import * as mfm from 'mfm-js';
import { unique } from '@/misc/prelude/array.js';
export function extractHashtags(nodes: mfm.MfmNode[]): string[] {
const hashtagNodes = mfm.extract(nodes, (node) => node.type === 'hashtag');
const hashtagNodes = mfm.extract(nodes, (node) => node.type === 'hashtag') as mfm.MfmHashtag[];
const hashtags = unique(hashtagNodes.map(x => x.props.hashtag));
return hashtags;

View File

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

View File

@@ -132,11 +132,27 @@ type NullOrUndefined<p extends Schema, T> =
// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
// Get intersection from union
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type PartialIntersection<T> = Partial<UnionToIntersection<T>>;
// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552
// To get union, we use `Foo extends any ? Hoge<Foo> : never`
type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? SchemaType<X> : never;
type ArrayUnion<T> = T extends any ? Array<T> : never;
type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
type ArrayUnion<T> = T extends any ? Array<T> : never;
type ObjectSchemaTypeDef<p extends Schema> =
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
p['properties'] extends NonNullable<Obj> ?
p['anyOf'] extends ReadonlyArray<Schema> ?
ObjType<p['properties'], NonNullable<p['required']>[number]> & UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>>
:
ObjType<p['properties'], NonNullable<p['required']>[number]>
:
p['anyOf'] extends ReadonlyArray<Schema> ? UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>> :
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
any
type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;
export type SchemaTypeDef<p extends Schema> =
p['type'] extends 'null' ? null :
@@ -149,13 +165,7 @@ export type SchemaTypeDef<p extends Schema> =
string
) :
p['type'] extends 'boolean' ? boolean :
p['type'] extends 'object' ? (
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
p['properties'] extends NonNullable<Obj> ? ObjType<p['properties'], NonNullable<p['required']>[number]> :
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & Partial<UnionToIntersection<UnionSchemaType<p['anyOf']>>> :
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
any
) :
p['type'] extends 'object' ? ObjectSchemaTypeDef<p> :
p['type'] extends 'array' ? (
p['items'] extends OfSchema ? (
p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] :
@@ -166,6 +176,7 @@ export type SchemaTypeDef<p extends Schema> =
p['items'] extends NonNullable<Schema> ? SchemaTypeDef<p['items']>[] :
any[]
) :
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
any;

View File

@@ -279,57 +279,6 @@ export class Meta {
})
public swPrivateKey: string | null;
@Column('boolean', {
default: false,
})
public enableTwitterIntegration: boolean;
@Column('varchar', {
length: 128,
nullable: true,
})
public twitterConsumerKey: string | null;
@Column('varchar', {
length: 128,
nullable: true,
})
public twitterConsumerSecret: string | null;
@Column('boolean', {
default: false,
})
public enableGithubIntegration: boolean;
@Column('varchar', {
length: 128,
nullable: true,
})
public githubClientId: string | null;
@Column('varchar', {
length: 128,
nullable: true,
})
public githubClientSecret: string | null;
@Column('boolean', {
default: false,
})
public enableDiscordIntegration: boolean;
@Column('varchar', {
length: 128,
nullable: true,
})
public discordClientId: string | null;
@Column('varchar', {
length: 128,
nullable: true,
})
public discordClientSecret: string | null;
@Column('varchar', {
length: 128,
nullable: true,

View File

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

View File

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

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

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

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

View File

@@ -137,38 +137,38 @@ export class FileServerService {
try {
if (file.state === 'remote') {
const convertFile = async () => {
if (file.fileRole === 'thumbnail') {
if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(file.mime)) {
return this.imageProcessingService.convertToWebpStream(
file.path,
498,
280
);
} else if (file.mime.startsWith('video/')) {
return await this.videoProcessingService.generateVideoThumbnail(file.path);
}
}
let image: IImageStreamable | null = null;
if (file.fileRole === 'webpublic') {
if (['image/svg+xml'].includes(file.mime)) {
return this.imageProcessingService.convertToWebpStream(
file.path,
2048,
2048,
{ ...webpDefault, lossless: true }
)
}
}
if (file.fileRole === 'thumbnail') {
if (isMimeImage(file.mime, 'sharp-convertible-image')) {
reply.header('Cache-Control', 'max-age=31536000, immutable');
return {
const url = new URL(`${this.config.mediaProxy}/static.webp`);
url.searchParams.set('url', file.url);
url.searchParams.set('static', '1');
return await reply.redirect(301, url.toString());
} else if (file.mime.startsWith('video/')) {
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
}
}
if (file.fileRole === 'webpublic') {
if (['image/svg+xml'].includes(file.mime)) {
reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/svg.webp`);
url.searchParams.set('url', file.url);
return await reply.redirect(301, url.toString());
}
}
if (!image) {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
};
const image = await convertFile();
}
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
// image.dataがstreamなら、stream終了後にcleanup
@@ -180,7 +180,6 @@ export class FileServerService {
}
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
return image.data;
}
@@ -217,6 +216,23 @@ export class FileServerService {
return;
}
if (this.config.externalMediaProxyEnabled) {
// 外部のメディアプロキシが有効なら、そちらにリダイレクト
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);
for (const [key, value] of Object.entries(request.query)) {
url.searchParams.append(key, value);
}
return await reply.redirect(
301,
url.toString(),
);
}
// Create temp file
const file = await this.getStreamAndTypeFromUrl(url);
if (file === '404') {
@@ -236,7 +252,7 @@ export class FileServerService {
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image');
let image: IImageStreamable | null = null;
if ('emoji' in request.query && isConvertibleImage) {
if (('emoji' in request.query || 'avatar' in request.query) && isConvertibleImage) {
if (!isAnimationConvertibleImage && !('static' in request.query)) {
image = {
data: fs.createReadStream(file.path),
@@ -246,7 +262,7 @@ export class FileServerService {
} else {
const data = sharp(file.path, { animated: !('static' in request.query) })
.resize({
height: 128,
height: 'emoji' in request.query ? 128 : 320,
withoutEnlargement: true,
})
.webp(webpDefault);
@@ -370,7 +386,7 @@ export class FileServerService {
@bindThis
private async getFileFromKey(key: string): Promise<
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; }
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; }
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; }
| '404'
| '204'
@@ -392,6 +408,7 @@ export class FileServerService {
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
return {
...result,
url: file.uri,
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
file,
}

View File

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

View File

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

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なので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('emoji', '1');

View File

@@ -12,9 +12,6 @@ import endpoints, { IEndpoint } from './endpoints.js';
import { ApiCallService } from './ApiCallService.js';
import { SignupApiService } from './SignupApiService.js';
import { SigninApiService } from './SigninApiService.js';
import { GithubServerService } from './integration/GithubServerService.js';
import { DiscordServerService } from './integration/DiscordServerService.js';
import { TwitterServerService } from './integration/TwitterServerService.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
@Injectable()
@@ -38,9 +35,6 @@ export class ApiServerService {
private apiCallService: ApiCallService,
private signupApiService: SignupApiService,
private signinApiService: SigninApiService,
private githubServerService: GithubServerService,
private discordServerService: DiscordServerService,
private twitterServerService: TwitterServerService,
) {
//this.createServer = this.createServer.bind(this);
}
@@ -133,10 +127,6 @@ export class ApiServerService {
fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply));
fastify.register(this.discordServerService.create);
fastify.register(this.githubServerService.create);
fastify.register(this.twitterServerService.create);
fastify.get('/v1/instance/peers', async (request, reply) => {
const instances = await this.instancesRepository.find({
select: ['host'],

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_drive from './endpoints/charts/drive.js';
import * as ep___charts_federation from './endpoints/charts/federation.js';
import * as ep___charts_hashtag from './endpoints/charts/hashtag.js';
import * as ep___charts_instance from './endpoints/charts/instance.js';
import * as ep___charts_notes from './endpoints/charts/notes.js';
import * as ep___charts_user_drive from './endpoints/charts/user/drive.js';
@@ -433,7 +432,6 @@ const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useCl
const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default };
const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default };
const $charts_federation: Provider = { provide: 'ep:charts/federation', useClass: ep___charts_federation.default };
const $charts_hashtag: Provider = { provide: 'ep:charts/hashtag', useClass: ep___charts_hashtag.default };
const $charts_instance: Provider = { provide: 'ep:charts/instance', useClass: ep___charts_instance.default };
const $charts_notes: Provider = { provide: 'ep:charts/notes', useClass: ep___charts_notes.default };
const $charts_user_drive: Provider = { provide: 'ep:charts/user/drive', useClass: ep___charts_user_drive.default };
@@ -773,7 +771,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$charts_apRequest,
$charts_drive,
$charts_federation,
$charts_hashtag,
$charts_instance,
$charts_notes,
$charts_user_drive,
@@ -1107,7 +1104,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$charts_apRequest,
$charts_drive,
$charts_federation,
$charts_hashtag,
$charts_instance,
$charts_notes,
$charts_user_drive,

View File

@@ -96,7 +96,6 @@ import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
import * as ep___charts_drive from './endpoints/charts/drive.js';
import * as ep___charts_federation from './endpoints/charts/federation.js';
import * as ep___charts_hashtag from './endpoints/charts/hashtag.js';
import * as ep___charts_instance from './endpoints/charts/instance.js';
import * as ep___charts_notes from './endpoints/charts/notes.js';
import * as ep___charts_user_drive from './endpoints/charts/user/drive.js';
@@ -430,7 +429,6 @@ const eps = [
['charts/ap-request', ep___charts_apRequest],
['charts/drive', ep___charts_drive],
['charts/federation', ep___charts_federation],
['charts/hashtag', ep___charts_hashtag],
['charts/instance', ep___charts_instance],
['charts/notes', ep___charts_notes],
['charts/user/drive', ep___charts_user_drive],

View File

@@ -138,18 +138,6 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
enableTwitterIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableGithubIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableDiscordIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableServiceWorker: {
type: 'boolean',
optional: false, nullable: false,
@@ -223,30 +211,6 @@ export const meta = {
optional: true, nullable: true,
format: 'id',
},
twitterConsumerKey: {
type: 'string',
optional: true, nullable: true,
},
twitterConsumerSecret: {
type: 'string',
optional: true, nullable: true,
},
githubClientId: {
type: 'string',
optional: true, nullable: true,
},
githubClientSecret: {
type: 'string',
optional: true, nullable: true,
},
discordClientId: {
type: 'string',
optional: true, nullable: true,
},
discordClientSecret: {
type: 'string',
optional: true, nullable: true,
},
summaryProxy: {
type: 'string',
optional: true, nullable: true,
@@ -389,9 +353,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
defaultLightTheme: instance.defaultLightTheme,
defaultDarkTheme: instance.defaultDarkTheme,
enableEmail: instance.enableEmail,
enableTwitterIntegration: instance.enableTwitterIntegration,
enableGithubIntegration: instance.enableGithubIntegration,
enableDiscordIntegration: instance.enableDiscordIntegration,
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null,
pinnedPages: instance.pinnedPages,
@@ -409,12 +370,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
proxyAccountId: instance.proxyAccountId,
twitterConsumerKey: instance.twitterConsumerKey,
twitterConsumerSecret: instance.twitterConsumerSecret,
githubClientId: instance.githubClientId,
githubClientSecret: instance.githubClientSecret,
discordClientId: instance.discordClientId,
discordClientSecret: instance.discordClientSecret,
summalyProxy: instance.summalyProxy,
email: instance.email,
smtpSecure: instance.smtpSecure,

View File

@@ -65,11 +65,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
};
}
const maskedKeys = ['accessToken', 'accessTokenSecret', 'refreshToken'];
Object.keys(profile.integrations).forEach(integration => {
maskedKeys.forEach(key => profile.integrations[integration][key] = '<MASKED>');
});
const signins = await this.signinsRepository.findBy({ userId: user.id });
const roles = await this.roleService.getUserRoles(user.id);
@@ -84,7 +79,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
carefulBot: profile.carefulBot,
injectFeaturedNote: profile.injectFeaturedNote,
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
integrations: profile.integrations,
mutedWords: profile.mutedWords,
mutedInstances: profile.mutedInstances,
mutingNotificationTypes: profile.mutingNotificationTypes,

View File

@@ -68,15 +68,6 @@ export const paramDef = {
summalyProxy: { type: 'string', nullable: true },
deeplAuthKey: { type: 'string', nullable: true },
deeplIsPro: { type: 'boolean' },
enableTwitterIntegration: { type: 'boolean' },
twitterConsumerKey: { type: 'string', nullable: true },
twitterConsumerSecret: { type: 'string', nullable: true },
enableGithubIntegration: { type: 'boolean' },
githubClientId: { type: 'string', nullable: true },
githubClientSecret: { type: 'string', nullable: true },
enableDiscordIntegration: { type: 'boolean' },
discordClientId: { type: 'string', nullable: true },
discordClientSecret: { type: 'string', nullable: true },
enableEmail: { type: 'boolean' },
email: { type: 'string', nullable: true },
smtpSecure: { type: 'boolean' },
@@ -270,42 +261,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.summalyProxy = ps.summalyProxy;
}
if (ps.enableTwitterIntegration !== undefined) {
set.enableTwitterIntegration = ps.enableTwitterIntegration;
}
if (ps.twitterConsumerKey !== undefined) {
set.twitterConsumerKey = ps.twitterConsumerKey;
}
if (ps.twitterConsumerSecret !== undefined) {
set.twitterConsumerSecret = ps.twitterConsumerSecret;
}
if (ps.enableGithubIntegration !== undefined) {
set.enableGithubIntegration = ps.enableGithubIntegration;
}
if (ps.githubClientId !== undefined) {
set.githubClientId = ps.githubClientId;
}
if (ps.githubClientSecret !== undefined) {
set.githubClientSecret = ps.githubClientSecret;
}
if (ps.enableDiscordIntegration !== undefined) {
set.enableDiscordIntegration = ps.enableDiscordIntegration;
}
if (ps.discordClientId !== undefined) {
set.discordClientId = ps.discordClientId;
}
if (ps.discordClientSecret !== undefined) {
set.discordClientSecret = ps.discordClientSecret;
}
if (ps.enableEmail !== undefined) {
set.enableEmail = ps.enableEmail;
}

View File

@@ -1,37 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { getJsonSchema } from '@/core/chart/core.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import HashtagChart from '@/core/chart/charts/hashtag.js';
import { schema } from '@/core/chart/charts/entities/hashtag.js';
export const meta = {
tags: ['charts', 'hashtags'],
res: getJsonSchema(schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {
type: 'object',
properties: {
span: { type: 'string', enum: ['day', 'hour'] },
limit: { type: 'integer', minimum: 1, maximum: 500, default: 30 },
offset: { type: 'integer', nullable: true, default: null },
tag: { type: 'string' },
},
required: ['span', 'tag'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
private hashtagChart: HashtagChart,
) {
super(meta, paramDef, async (ps, me) => {
return await this.hashtagChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.tag);
});
}
}

View File

@@ -169,18 +169,6 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
enableTwitterIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableGithubIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableDiscordIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableServiceWorker: {
type: 'boolean',
optional: false, nullable: false,
@@ -193,6 +181,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
mediaProxy: {
type: 'string',
optional: false, nullable: false,
},
features: {
type: 'object',
optional: true, nullable: false,
@@ -225,18 +217,6 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
twitter: {
type: 'boolean',
optional: false, nullable: false,
},
github: {
type: 'boolean',
optional: false, nullable: false,
},
discord: {
type: 'boolean',
optional: false, nullable: false,
},
serviceWorker: {
type: 'boolean',
optional: false, nullable: false,
@@ -325,17 +305,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
imageUrl: ad.imageUrl,
})),
enableEmail: instance.enableEmail,
enableTwitterIntegration: instance.enableTwitterIntegration,
enableGithubIntegration: instance.enableGithubIntegration,
enableDiscordIntegration: instance.enableDiscordIntegration,
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
mediaProxy: this.config.mediaProxy,
...(ps.detail ? {
pinnedPages: instance.pinnedPages,
pinnedClipId: instance.pinnedClipId,
@@ -358,9 +335,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
recaptcha: instance.enableRecaptcha,
turnstile: instance.enableTurnstile,
objectStorage: instance.useObjectStorage,
twitter: instance.enableTwitterIntegration,
github: instance.enableGithubIntegration,
discord: instance.enableDiscordIntegration,
serviceWorker: instance.enableServiceWorker,
miauth: true,
};

View File

@@ -90,48 +90,13 @@ export const paramDef = {
visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id',
} },
text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true },
cw: { type: 'string', nullable: true, maxLength: 100 },
localOnly: { type: 'boolean', default: false },
noExtractMentions: { type: 'boolean', default: false },
noExtractHashtags: { type: 'boolean', default: false },
noExtractEmojis: { type: 'boolean', default: false },
fileIds: {
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
mediaIds: {
deprecated: true,
description: 'Use `fileIds` instead. If both are specified, this property is discarded.',
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
replyId: { type: 'string', format: 'misskey:id', nullable: true },
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
channelId: { type: 'string', format: 'misskey:id', nullable: true },
poll: {
type: 'object',
nullable: true,
properties: {
choices: {
type: 'array',
uniqueItems: true,
minItems: 2,
maxItems: 10,
items: { type: 'string', minLength: 1, maxLength: 50 },
},
multiple: { type: 'boolean', default: false },
expiresAt: { type: 'integer', nullable: true },
expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
},
required: ['choices'],
},
},
anyOf: [
{
@@ -143,21 +108,60 @@ export const paramDef = {
},
{
// (re)note with files, text and poll are optional
properties: {
fileIds: {
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
},
required: ['fileIds'],
},
{
// (re)note with files, text and poll are optional
properties: {
mediaIds: {
deprecated: true,
description: 'Use `fileIds` instead. If both are specified, this property is discarded.',
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
},
required: ['mediaIds'],
},
{
// (re)note with poll, text and files are optional
properties: {
poll: { type: 'object', nullable: false },
poll: {
type: 'object',
nullable: true,
properties: {
choices: {
type: 'array',
uniqueItems: true,
minItems: 2,
maxItems: 10,
items: { type: 'string', minLength: 1, maxLength: 50 },
},
multiple: { type: 'boolean' },
expiresAt: { type: 'integer', nullable: true },
expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
},
required: ['choices'],
},
},
required: ['poll'],
},
{
// pure renote
properties: {
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
},
required: ['renoteId'],
},
],

View File

@@ -1,6 +1,6 @@
import { Not } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, BlockingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js';
import type { UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js';
import type { IRemoteUser } from '@/models/entities/User.js';
import { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -11,6 +11,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
import { DI } from '@/di-symbols.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -77,9 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
@@ -93,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private apRendererService: ApRendererService,
private globalEventService: GlobalEventService,
private createNotificationService: CreateNotificationService,
private userBlockingService: UserBlockingService,
) {
super(meta, paramDef, async (ps, me) => {
const createdAt = new Date();
@@ -109,11 +108,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Check blocking
if (note.userId !== me.id) {
const block = await this.blockingsRepository.findOneBy({
blockerId: note.userId,
blockeeId: me.id,
});
if (block) {
const blocked = await this.userBlockingService.checkBlocked(note.userId, me.id);
if (blocked) {
throw new ApiError(meta.errors.youHaveBeenBlocked);
}
}

View File

@@ -29,14 +29,22 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
username: { type: 'string', nullable: true },
host: { type: 'string', nullable: true },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
detail: { type: 'boolean', default: true },
},
anyOf: [
{ required: ['username'] },
{ required: ['host'] },
{
properties: {
username: { type: 'string', nullable: true },
},
required: ['username']
},
{
properties: {
host: { type: 'string', nullable: true },
},
required: ['host']
},
],
} as const;

View File

@@ -1,308 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { OAuth2 } from 'oauth';
import { v4 as uuid } from 'uuid';
import { IsNull } from 'typeorm';
import type { Config } from '@/config.js';
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import type { ILocalUser } from '@/models/entities/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { bindThis } from '@/decorators.js';
import { SigninService } from '../SigninService.js';
import type { FastifyInstance, FastifyRequest, FastifyPluginOptions } from 'fastify';
@Injectable()
export class DiscordServerService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private userEntityService: UserEntityService,
private httpRequestService: HttpRequestService,
private globalEventService: GlobalEventService,
private metaService: MetaService,
private signinService: SigninService,
) {
//this.create = this.create.bind(this);
}
@bindThis
public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.get('/disconnect/discord', async (request, reply) => {
if (!this.compareOrigin(request)) {
throw new FastifyReplyError(400, 'invalid origin');
}
const userToken = this.getUserToken(request);
if (!userToken) {
throw new FastifyReplyError(400, 'signin required');
}
const user = await this.usersRepository.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
delete profile.integrations.discord;
await this.userProfilesRepository.update(user.id, {
integrations: profile.integrations,
});
// Publish i updated event
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
detail: true,
includeSecrets: true,
}));
return 'Discordの連携を解除しました :v:';
});
const getOAuth2 = async () => {
const meta = await this.metaService.fetch(true);
if (meta.enableDiscordIntegration) {
return new OAuth2(
meta.discordClientId!,
meta.discordClientSecret!,
'https://discord.com/',
'api/oauth2/authorize',
'api/oauth2/token');
} else {
return null;
}
};
fastify.get('/connect/discord', async (request, reply) => {
if (!this.compareOrigin(request)) {
throw new FastifyReplyError(400, 'invalid origin');
}
const userToken = this.getUserToken(request);
if (!userToken) {
throw new FastifyReplyError(400, 'signin required');
}
const params = {
redirect_uri: `${this.config.url}/api/dc/cb`,
scope: ['identify'],
state: uuid(),
response_type: 'code',
};
this.redisClient.set(userToken, JSON.stringify(params));
const oauth2 = await getOAuth2();
reply.redirect(oauth2!.getAuthorizeUrl(params));
});
fastify.get('/signin/discord', async (request, reply) => {
const sessid = uuid();
const params = {
redirect_uri: `${this.config.url}/api/dc/cb`,
scope: ['identify'],
state: uuid(),
response_type: 'code',
};
reply.setCookie('signin_with_discord_sid', sessid, {
path: '/',
secure: this.config.url.startsWith('https'),
httpOnly: true,
});
this.redisClient.set(sessid, JSON.stringify(params));
const oauth2 = await getOAuth2();
reply.redirect(oauth2!.getAuthorizeUrl(params));
});
fastify.get('/dc/cb', async (request, reply) => {
const userToken = this.getUserToken(request);
const oauth2 = await getOAuth2();
if (!userToken) {
const sessid = request.cookies['signin_with_discord_sid'];
if (!sessid) {
throw new FastifyReplyError(400, 'invalid session');
}
const code = request.query.code;
if (!code || typeof code !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
this.redisClient.get(sessid, async (_, state) => {
if (state == null) throw new Error('empty state');
res(JSON.parse(state));
});
});
if (request.query.state !== state) {
throw new FastifyReplyError(400, 'invalid session');
}
const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
oauth2!.getOAuthAccessToken(code, {
grant_type: 'authorization_code',
redirect_uri,
}, (err, accessToken, refreshToken, result) => {
if (err) {
rej(err);
} else if (result.error) {
rej(result.error);
} else {
res({
accessToken,
refreshToken,
expiresDate: Date.now() + Number(result.expires_in) * 1000,
});
}
}));
const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', {
'Authorization': `Bearer ${accessToken}`,
})) as Record<string, unknown>;
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const profile = await this.userProfilesRepository.createQueryBuilder()
.where('"integrations"->\'discord\'->>\'id\' = :id', { id: id })
.andWhere('"userHost" IS NULL')
.getOne();
if (profile == null) {
throw new FastifyReplyError(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`);
}
await this.userProfilesRepository.update(profile.userId, {
integrations: {
...profile.integrations,
discord: {
id: id,
accessToken: accessToken,
refreshToken: refreshToken,
expiresDate: expiresDate,
username: username,
discriminator: discriminator,
},
},
});
return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: profile.userId }) as ILocalUser, true);
} else {
const code = request.query.code;
if (!code || typeof code !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
this.redisClient.get(userToken, async (_, state) => {
if (state == null) throw new Error('empty state');
res(JSON.parse(state));
});
});
if (request.query.state !== state) {
throw new FastifyReplyError(400, 'invalid session');
}
const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
oauth2!.getOAuthAccessToken(code, {
grant_type: 'authorization_code',
redirect_uri,
}, (err, accessToken, refreshToken, result) => {
if (err) {
rej(err);
} else if (result.error) {
rej(result.error);
} else {
res({
accessToken,
refreshToken,
expiresDate: Date.now() + Number(result.expires_in) * 1000,
});
}
}));
const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', {
'Authorization': `Bearer ${accessToken}`,
})) as Record<string, unknown>;
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const user = await this.usersRepository.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
await this.userProfilesRepository.update(user.id, {
integrations: {
...profile.integrations,
discord: {
accessToken: accessToken,
refreshToken: refreshToken,
expiresDate: expiresDate,
id: id,
username: username,
discriminator: discriminator,
},
},
});
// Publish i updated event
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
detail: true,
includeSecrets: true,
}));
return `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`;
}
});
done();
}
@bindThis
private getUserToken(request: FastifyRequest): string | null {
return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
}
@bindThis
private compareOrigin(request: FastifyRequest): boolean {
function normalizeUrl(url?: string): string {
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
}
const referer = request.headers['referer'];
return (normalizeUrl(referer) === normalizeUrl(this.config.url));
}
}

View File

@@ -1,280 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { OAuth2 } from 'oauth';
import { v4 as uuid } from 'uuid';
import { IsNull } from 'typeorm';
import type { Config } from '@/config.js';
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import type { ILocalUser } from '@/models/entities/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { bindThis } from '@/decorators.js';
import { SigninService } from '../SigninService.js';
import type { FastifyInstance, FastifyRequest, FastifyPluginOptions } from 'fastify';
@Injectable()
export class GithubServerService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private userEntityService: UserEntityService,
private httpRequestService: HttpRequestService,
private globalEventService: GlobalEventService,
private metaService: MetaService,
private signinService: SigninService,
) {
//this.create = this.create.bind(this);
}
@bindThis
public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.get('/disconnect/github', async (request, reply) => {
if (!this.compareOrigin(request)) {
throw new FastifyReplyError(400, 'invalid origin');
}
const userToken = this.getUserToken(request);
if (!userToken) {
throw new FastifyReplyError(400, 'signin required');
}
const user = await this.usersRepository.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
delete profile.integrations.github;
await this.userProfilesRepository.update(user.id, {
integrations: profile.integrations,
});
// Publish i updated event
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
detail: true,
includeSecrets: true,
}));
return 'GitHubの連携を解除しました :v:';
});
const getOath2 = async () => {
const meta = await this.metaService.fetch(true);
if (meta.enableGithubIntegration && meta.githubClientId && meta.githubClientSecret) {
return new OAuth2(
meta.githubClientId,
meta.githubClientSecret,
'https://github.com/',
'login/oauth/authorize',
'login/oauth/access_token');
} else {
return null;
}
};
fastify.get('/connect/github', async (request, reply) => {
if (!this.compareOrigin(request)) {
throw new FastifyReplyError(400, 'invalid origin');
}
const userToken = this.getUserToken(request);
if (!userToken) {
throw new FastifyReplyError(400, 'signin required');
}
const params = {
redirect_uri: `${this.config.url}/api/gh/cb`,
scope: ['read:user'],
state: uuid(),
};
this.redisClient.set(userToken, JSON.stringify(params));
const oauth2 = await getOath2();
reply.redirect(oauth2!.getAuthorizeUrl(params));
});
fastify.get('/signin/github', async (request, reply) => {
const sessid = uuid();
const params = {
redirect_uri: `${this.config.url}/api/gh/cb`,
scope: ['read:user'],
state: uuid(),
};
reply.setCookie('signin_with_github_sid', sessid, {
path: '/',
secure: this.config.url.startsWith('https'),
httpOnly: true,
});
this.redisClient.set(sessid, JSON.stringify(params));
const oauth2 = await getOath2();
reply.redirect(oauth2!.getAuthorizeUrl(params));
});
fastify.get('/gh/cb', async (request, reply) => {
const userToken = this.getUserToken(request);
const oauth2 = await getOath2();
if (!userToken) {
const sessid = request.cookies['signin_with_github_sid'];
if (!sessid) {
throw new FastifyReplyError(400, 'invalid session');
}
const code = request.query.code;
if (!code || typeof code !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
this.redisClient.get(sessid, async (_, state) => {
if (state == null) throw new Error('empty state');
res(JSON.parse(state));
});
});
if (request.query.state !== state) {
throw new FastifyReplyError(400, 'invalid session');
}
const { accessToken } = await new Promise<{ accessToken: string }>((res, rej) =>
oauth2!.getOAuthAccessToken(code, {
redirect_uri,
}, (err, accessToken, refresh, result) => {
if (err) {
rej(err);
} else if (result.error) {
rej(result.error);
} else {
res({ accessToken });
}
}));
const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', {
'Authorization': `bearer ${accessToken}`,
})) as Record<string, unknown>;
if (typeof login !== 'string' || typeof id !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const link = await this.userProfilesRepository.createQueryBuilder()
.where('"integrations"->\'github\'->>\'id\' = :id', { id: id })
.andWhere('"userHost" IS NULL')
.getOne();
if (link == null) {
throw new FastifyReplyError(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
}
return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true);
} else {
const code = request.query.code;
if (!code || typeof code !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
this.redisClient.get(userToken, async (_, state) => {
if (state == null) throw new Error('empty state');
res(JSON.parse(state));
});
});
if (request.query.state !== state) {
throw new FastifyReplyError(400, 'invalid session');
}
const { accessToken } = await new Promise<{ accessToken: string }>((res, rej) =>
oauth2!.getOAuthAccessToken(
code,
{ redirect_uri },
(err, accessToken, refresh, result) => {
if (err) {
rej(err);
} else if (result.error) {
rej(result.error);
} else {
res({ accessToken });
}
}));
const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', {
'Authorization': `bearer ${accessToken}`,
})) as Record<string, unknown>;
if (typeof login !== 'string' || typeof id !== 'number') {
throw new FastifyReplyError(400, 'invalid session');
}
const user = await this.usersRepository.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
await this.userProfilesRepository.update(user.id, {
integrations: {
...profile.integrations,
github: {
accessToken: accessToken,
id: id,
login: login,
},
},
});
// Publish i updated event
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
detail: true,
includeSecrets: true,
}));
return `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
}
});
done();
}
@bindThis
private getUserToken(request: FastifyRequest): string | null {
return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
}
@bindThis
private compareOrigin(request: FastifyRequest): boolean {
function normalizeUrl(url?: string): string {
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
}
const referer = request.headers['referer'];
return (normalizeUrl(referer) === normalizeUrl(this.config.url));
}
}

View File

@@ -1,225 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { v4 as uuid } from 'uuid';
import { IsNull } from 'typeorm';
import * as autwh from 'autwh';
import type { Config } from '@/config.js';
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import type { ILocalUser } from '@/models/entities/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { bindThis } from '@/decorators.js';
import { SigninService } from '../SigninService.js';
import type { FastifyInstance, FastifyRequest, FastifyPluginOptions } from 'fastify';
@Injectable()
export class TwitterServerService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private userEntityService: UserEntityService,
private httpRequestService: HttpRequestService,
private globalEventService: GlobalEventService,
private metaService: MetaService,
private signinService: SigninService,
) {
//this.create = this.create.bind(this);
}
@bindThis
public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.get('/disconnect/twitter', async (request, reply) => {
if (!this.compareOrigin(request)) {
throw new FastifyReplyError(400, 'invalid origin');
}
const userToken = this.getUserToken(request);
if (userToken == null) {
throw new FastifyReplyError(400, 'signin required');
}
const user = await this.usersRepository.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
delete profile.integrations.twitter;
await this.userProfilesRepository.update(user.id, {
integrations: profile.integrations,
});
// Publish i updated event
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
detail: true,
includeSecrets: true,
}));
return 'Twitterの連携を解除しました :v:';
});
const getTwAuth = async () => {
const meta = await this.metaService.fetch(true);
if (meta.enableTwitterIntegration && meta.twitterConsumerKey && meta.twitterConsumerSecret) {
return autwh({
consumerKey: meta.twitterConsumerKey,
consumerSecret: meta.twitterConsumerSecret,
callbackUrl: `${this.config.url}/api/tw/cb`,
});
} else {
return null;
}
};
fastify.get('/connect/twitter', async (request, reply) => {
if (!this.compareOrigin(request)) {
throw new FastifyReplyError(400, 'invalid origin');
}
const userToken = this.getUserToken(request);
if (userToken == null) {
throw new FastifyReplyError(400, 'signin required');
}
const twAuth = await getTwAuth();
const twCtx = await twAuth!.begin();
this.redisClient.set(userToken, JSON.stringify(twCtx));
reply.redirect(twCtx.url);
});
fastify.get('/signin/twitter', async (request, reply) => {
const twAuth = await getTwAuth();
const twCtx = await twAuth!.begin();
const sessid = uuid();
this.redisClient.set(sessid, JSON.stringify(twCtx));
reply.setCookie('signin_with_twitter_sid', sessid, {
path: '/',
secure: this.config.url.startsWith('https'),
httpOnly: true,
});
reply.redirect(twCtx.url);
});
fastify.get('/tw/cb', async (request, reply) => {
const userToken = this.getUserToken(request);
const twAuth = await getTwAuth();
if (userToken == null) {
const sessid = request.cookies['signin_with_twitter_sid'];
if (sessid == null) {
throw new FastifyReplyError(400, 'invalid session');
}
const get = new Promise<any>((res, rej) => {
this.redisClient.get(sessid, async (_, twCtx) => {
res(twCtx);
});
});
const twCtx = await get;
const verifier = request.query.oauth_verifier;
if (!verifier || typeof verifier !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
const link = await this.userProfilesRepository.createQueryBuilder()
.where('"integrations"->\'twitter\'->>\'userId\' = :id', { id: result.userId })
.andWhere('"userHost" IS NULL')
.getOne();
if (link == null) {
throw new FastifyReplyError(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
}
return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true);
} else {
const verifier = request.query.oauth_verifier;
if (!verifier || typeof verifier !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const get = new Promise<any>((res, rej) => {
this.redisClient.get(userToken, async (_, twCtx) => {
res(twCtx);
});
});
const twCtx = await get;
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
const user = await this.usersRepository.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
await this.userProfilesRepository.update(user.id, {
integrations: {
...profile.integrations,
twitter: {
accessToken: result.accessToken,
accessTokenSecret: result.accessTokenSecret,
userId: result.userId,
screenName: result.screenName,
},
},
});
// Publish i updated event
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
detail: true,
includeSecrets: true,
}));
return `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
}
});
done();
}
@bindThis
private getUserToken(request: FastifyRequest): string | null {
return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
}
@bindThis
private compareOrigin(request: FastifyRequest): boolean {
function normalizeUrl(url?: string): string {
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
}
const referer = request.headers['referer'];
return (normalizeUrl(referer) === normalizeUrl(this.config.url));
}
}

View File

@@ -18,31 +18,28 @@ import { Following, Role, RoleAssignment } from '@/models';
import type Emitter from 'strict-event-emitter-types';
import type { EventEmitter } from 'events';
// redis通すとDateのインスタンスはstringに変換されるので
type Serialized<T> = {
[K in keyof T]: T[K] extends Date ? string : T[K];
};
//#region Stream type-body definitions
export interface InternalStreamTypes {
userChangeSuspendedState: Serialized<{ id: User['id']; isSuspended: User['isSuspended']; }>;
userTokenRegenerated: Serialized<{ id: User['id']; oldToken: User['token']; newToken: User['token']; }>;
remoteUserUpdated: Serialized<{ id: User['id']; }>;
follow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
unfollow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
policiesUpdated: Serialized<Role['policies']>;
roleCreated: Serialized<Role>;
roleDeleted: Serialized<Role>;
roleUpdated: Serialized<Role>;
userRoleAssigned: Serialized<RoleAssignment>;
userRoleUnassigned: Serialized<RoleAssignment>;
webhookCreated: Serialized<Webhook>;
webhookDeleted: Serialized<Webhook>;
webhookUpdated: Serialized<Webhook>;
antennaCreated: Serialized<Antenna>;
antennaDeleted: Serialized<Antenna>;
antennaUpdated: Serialized<Antenna>;
metaUpdated: Serialized<Meta>;
userChangeSuspendedState: { id: User['id']; isSuspended: User['isSuspended']; };
userTokenRegenerated: { id: User['id']; oldToken: User['token']; newToken: User['token']; };
remoteUserUpdated: { id: User['id']; };
follow: { followerId: User['id']; followeeId: User['id']; };
unfollow: { followerId: User['id']; followeeId: User['id']; };
blockingCreated: { blockerId: User['id']; blockeeId: User['id']; };
blockingDeleted: { blockerId: User['id']; blockeeId: User['id']; };
policiesUpdated: Role['policies'];
roleCreated: Role;
roleDeleted: Role;
roleUpdated: Role;
userRoleAssigned: RoleAssignment;
userRoleUnassigned: RoleAssignment;
webhookCreated: Webhook;
webhookDeleted: Webhook;
webhookUpdated: Webhook;
antennaCreated: Antenna;
antennaDeleted: Antenna;
antennaUpdated: Antenna;
metaUpdated: Meta;
}
export interface BroadcastTypes {
@@ -210,63 +207,72 @@ type EventUnionFromDictionary<
U = Events<T>
> = U[keyof U];
// redis通すとDateのインスタンスはstringに変換されるので
type Serialized<T> = {
[K in keyof T]: T[K] extends Date ? string : T[K] extends Record<string, any> ? Serialized<T[K]> : T[K];
};
type SerializedAll<T> = {
[K in keyof T]: Serialized<T[K]>;
};
// name/messages(spec) pairs dictionary
export type StreamMessages = {
internal: {
name: 'internal';
payload: EventUnionFromDictionary<InternalStreamTypes>;
payload: EventUnionFromDictionary<SerializedAll<InternalStreamTypes>>;
};
broadcast: {
name: 'broadcast';
payload: EventUnionFromDictionary<BroadcastTypes>;
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
};
user: {
name: `user:${User['id']}`;
payload: EventUnionFromDictionary<UserStreamTypes>;
payload: EventUnionFromDictionary<SerializedAll<UserStreamTypes>>;
};
main: {
name: `mainStream:${User['id']}`;
payload: EventUnionFromDictionary<MainStreamTypes>;
payload: EventUnionFromDictionary<SerializedAll<MainStreamTypes>>;
};
drive: {
name: `driveStream:${User['id']}`;
payload: EventUnionFromDictionary<DriveStreamTypes>;
payload: EventUnionFromDictionary<SerializedAll<DriveStreamTypes>>;
};
note: {
name: `noteStream:${Note['id']}`;
payload: EventUnionFromDictionary<NoteStreamEventTypes>;
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
};
channel: {
name: `channelStream:${Channel['id']}`;
payload: EventUnionFromDictionary<ChannelStreamTypes>;
payload: EventUnionFromDictionary<SerializedAll<ChannelStreamTypes>>;
};
userList: {
name: `userListStream:${UserList['id']}`;
payload: EventUnionFromDictionary<UserListStreamTypes>;
payload: EventUnionFromDictionary<SerializedAll<UserListStreamTypes>>;
};
antenna: {
name: `antennaStream:${Antenna['id']}`;
payload: EventUnionFromDictionary<AntennaStreamTypes>;
payload: EventUnionFromDictionary<SerializedAll<AntennaStreamTypes>>;
};
messaging: {
name: `messagingStream:${User['id']}-${User['id']}`;
payload: EventUnionFromDictionary<MessagingStreamTypes>;
payload: EventUnionFromDictionary<SerializedAll<MessagingStreamTypes>>;
};
groupMessaging: {
name: `messagingStream:${UserGroup['id']}`;
payload: EventUnionFromDictionary<GroupMessagingStreamTypes>;
payload: EventUnionFromDictionary<SerializedAll<GroupMessagingStreamTypes>>;
};
messagingIndex: {
name: `messagingIndexStream:${User['id']}`;
payload: EventUnionFromDictionary<MessagingIndexStreamTypes>;
payload: EventUnionFromDictionary<SerializedAll<MessagingIndexStreamTypes>>;
};
admin: {
name: `adminStream:${User['id']}`;
payload: EventUnionFromDictionary<AdminStreamTypes>;
payload: EventUnionFromDictionary<SerializedAll<AdminStreamTypes>>;
};
notes: {
name: 'notesStream';
payload: Packed<'Note'>;
payload: Serialized<Packed<'Note'>>;
};
};

View File

@@ -33,7 +33,7 @@ export class UrlPreviewService {
private wrap(url?: string): string | null {
return url != null
? url.match(/^https?:\/\//)
? `${this.config.url}/proxy/preview.webp?${query({
? `${this.config.mediaProxy}/preview.webp?${query({
url,
preview: '1',
})}`
@@ -73,6 +73,14 @@ export class UrlPreviewService {
});
this.logger.succ(`Got preview of ${url}: ${summary.title}`);
if (summary.url && !(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) {
throw new Error('unsupported schema included');
}
if (summary.player?.url && !(summary.player.url.startsWith('http://') || summary.player.url.startsWith('https://'))) {
throw new Error('unsupported schema included');
}
summary.icon = this.wrap(summary.icon);
summary.thumbnail = this.wrap(summary.thumbnail);

View File

@@ -100,90 +100,90 @@ describe('API visibility', () => {
//#region show post
// public
it('[show] public-postを自分が見れる', async () => {
test('[show] public-postを自分が見れる', async () => {
const res = await show(pub.id, alice);
assert.strictEqual(res.body.text, 'x');
});
it('[show] public-postをフォロワーが見れる', async () => {
test('[show] public-postをフォロワーが見れる', async () => {
const res = await show(pub.id, follower);
assert.strictEqual(res.body.text, 'x');
});
it('[show] public-postを非フォロワーが見れる', async () => {
test('[show] public-postを非フォロワーが見れる', async () => {
const res = await show(pub.id, other);
assert.strictEqual(res.body.text, 'x');
});
it('[show] public-postを未認証が見れる', async () => {
test('[show] public-postを未認証が見れる', async () => {
const res = await show(pub.id, null);
assert.strictEqual(res.body.text, 'x');
});
// home
it('[show] home-postを自分が見れる', async () => {
test('[show] home-postを自分が見れる', async () => {
const res = await show(home.id, alice);
assert.strictEqual(res.body.text, 'x');
});
it('[show] home-postをフォロワーが見れる', async () => {
test('[show] home-postをフォロワーが見れる', async () => {
const res = await show(home.id, follower);
assert.strictEqual(res.body.text, 'x');
});
it('[show] home-postを非フォロワーが見れる', async () => {
test('[show] home-postを非フォロワーが見れる', async () => {
const res = await show(home.id, other);
assert.strictEqual(res.body.text, 'x');
});
it('[show] home-postを未認証が見れる', async () => {
test('[show] home-postを未認証が見れる', async () => {
const res = await show(home.id, null);
assert.strictEqual(res.body.text, 'x');
});
// followers
it('[show] followers-postを自分が見れる', async () => {
test('[show] followers-postを自分が見れる', async () => {
const res = await show(fol.id, alice);
assert.strictEqual(res.body.text, 'x');
});
it('[show] followers-postをフォロワーが見れる', async () => {
test('[show] followers-postをフォロワーが見れる', async () => {
const res = await show(fol.id, follower);
assert.strictEqual(res.body.text, 'x');
});
it('[show] followers-postを非フォロワーが見れない', async () => {
test('[show] followers-postを非フォロワーが見れない', async () => {
const res = await show(fol.id, other);
assert.strictEqual(res.body.isHidden, true);
});
it('[show] followers-postを未認証が見れない', async () => {
test('[show] followers-postを未認証が見れない', async () => {
const res = await show(fol.id, null);
assert.strictEqual(res.body.isHidden, true);
});
// specified
it('[show] specified-postを自分が見れる', async () => {
test('[show] specified-postを自分が見れる', async () => {
const res = await show(spe.id, alice);
assert.strictEqual(res.body.text, 'x');
});
it('[show] specified-postを指定ユーザーが見れる', async () => {
test('[show] specified-postを指定ユーザーが見れる', async () => {
const res = await show(spe.id, target);
assert.strictEqual(res.body.text, 'x');
});
it('[show] specified-postをフォロワーが見れない', async () => {
test('[show] specified-postをフォロワーが見れない', async () => {
const res = await show(spe.id, follower);
assert.strictEqual(res.body.isHidden, true);
});
it('[show] specified-postを非フォロワーが見れない', async () => {
test('[show] specified-postを非フォロワーが見れない', async () => {
const res = await show(spe.id, other);
assert.strictEqual(res.body.isHidden, true);
});
it('[show] specified-postを未認証が見れない', async () => {
test('[show] specified-postを未認証が見れない', async () => {
const res = await show(spe.id, null);
assert.strictEqual(res.body.isHidden, true);
});
@@ -191,110 +191,110 @@ describe('API visibility', () => {
//#region show reply
// public
it('[show] public-replyを自分が見れる', async () => {
test('[show] public-replyを自分が見れる', async () => {
const res = await show(pubR.id, alice);
assert.strictEqual(res.body.text, 'x');
});
it('[show] public-replyをされた人が見れる', async () => {
test('[show] public-replyをされた人が見れる', async () => {
const res = await show(pubR.id, target);
assert.strictEqual(res.body.text, 'x');
});
it('[show] public-replyをフォロワーが見れる', async () => {
test('[show] public-replyをフォロワーが見れる', async () => {
const res = await show(pubR.id, follower);
assert.strictEqual(res.body.text, 'x');
});
it('[show] public-replyを非フォロワーが見れる', async () => {
test('[show] public-replyを非フォロワーが見れる', async () => {
const res = await show(pubR.id, other);
assert.strictEqual(res.body.text, 'x');
});
it('[show] public-replyを未認証が見れる', async () => {
test('[show] public-replyを未認証が見れる', async () => {
const res = await show(pubR.id, null);
assert.strictEqual(res.body.text, 'x');
});
// home
it('[show] home-replyを自分が見れる', async () => {
test('[show] home-replyを自分が見れる', async () => {
const res = await show(homeR.id, alice);
assert.strictEqual(res.body.text, 'x');
});
it('[show] home-replyをされた人が見れる', async () => {
test('[show] home-replyをされた人が見れる', async () => {
const res = await show(homeR.id, target);
assert.strictEqual(res.body.text, 'x');
});
it('[show] home-replyをフォロワーが見れる', async () => {
test('[show] home-replyをフォロワーが見れる', async () => {
const res = await show(homeR.id, follower);
assert.strictEqual(res.body.text, 'x');
});
it('[show] home-replyを非フォロワーが見れる', async () => {
test('[show] home-replyを非フォロワーが見れる', async () => {
const res = await show(homeR.id, other);
assert.strictEqual(res.body.text, 'x');
});
it('[show] home-replyを未認証が見れる', async () => {
test('[show] home-replyを未認証が見れる', async () => {
const res = await show(homeR.id, null);
assert.strictEqual(res.body.text, 'x');
});
// followers
it('[show] followers-replyを自分が見れる', async () => {
test('[show] followers-replyを自分が見れる', async () => {
const res = await show(folR.id, alice);
assert.strictEqual(res.body.text, 'x');
});
it('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async () => {
test('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async () => {
const res = await show(folR.id, target);
assert.strictEqual(res.body.text, 'x');
});
it('[show] followers-replyをフォロワーが見れる', async () => {
test('[show] followers-replyをフォロワーが見れる', async () => {
const res = await show(folR.id, follower);
assert.strictEqual(res.body.text, 'x');
});
it('[show] followers-replyを非フォロワーが見れない', async () => {
test('[show] followers-replyを非フォロワーが見れない', async () => {
const res = await show(folR.id, other);
assert.strictEqual(res.body.isHidden, true);
});
it('[show] followers-replyを未認証が見れない', async () => {
test('[show] followers-replyを未認証が見れない', async () => {
const res = await show(folR.id, null);
assert.strictEqual(res.body.isHidden, true);
});
// specified
it('[show] specified-replyを自分が見れる', async () => {
test('[show] specified-replyを自分が見れる', async () => {
const res = await show(speR.id, alice);
assert.strictEqual(res.body.text, 'x');
});
it('[show] specified-replyを指定ユーザーが見れる', async () => {
test('[show] specified-replyを指定ユーザーが見れる', async () => {
const res = await show(speR.id, target);
assert.strictEqual(res.body.text, 'x');
});
it('[show] specified-replyをされた人が指定されてなくても見れる', async () => {
test('[show] specified-replyをされた人が指定されてなくても見れる', async () => {
const res = await show(speR.id, target);
assert.strictEqual(res.body.text, 'x');
});
it('[show] specified-replyをフォロワーが見れない', async () => {
test('[show] specified-replyをフォロワーが見れない', async () => {
const res = await show(speR.id, follower);
assert.strictEqual(res.body.isHidden, true);
});
it('[show] specified-replyを非フォロワーが見れない', async () => {
test('[show] specified-replyを非フォロワーが見れない', async () => {
const res = await show(speR.id, other);
assert.strictEqual(res.body.isHidden, true);
});
it('[show] specified-replyを未認証が見れない', async () => {
test('[show] specified-replyを未認証が見れない', async () => {
const res = await show(speR.id, null);
assert.strictEqual(res.body.isHidden, true);
});
@@ -302,131 +302,131 @@ describe('API visibility', () => {
//#region show mention
// public
it('[show] public-mentionを自分が見れる', async () => {
test('[show] public-mentionを自分が見れる', async () => {
const res = await show(pubM.id, alice);
assert.strictEqual(res.body.text, '@target x');
});
it('[show] public-mentionをされた人が見れる', async () => {
test('[show] public-mentionをされた人が見れる', async () => {
const res = await show(pubM.id, target);
assert.strictEqual(res.body.text, '@target x');
});
it('[show] public-mentionをフォロワーが見れる', async () => {
test('[show] public-mentionをフォロワーが見れる', async () => {
const res = await show(pubM.id, follower);
assert.strictEqual(res.body.text, '@target x');
});
it('[show] public-mentionを非フォロワーが見れる', async () => {
test('[show] public-mentionを非フォロワーが見れる', async () => {
const res = await show(pubM.id, other);
assert.strictEqual(res.body.text, '@target x');
});
it('[show] public-mentionを未認証が見れる', async () => {
test('[show] public-mentionを未認証が見れる', async () => {
const res = await show(pubM.id, null);
assert.strictEqual(res.body.text, '@target x');
});
// home
it('[show] home-mentionを自分が見れる', async () => {
test('[show] home-mentionを自分が見れる', async () => {
const res = await show(homeM.id, alice);
assert.strictEqual(res.body.text, '@target x');
});
it('[show] home-mentionをされた人が見れる', async () => {
test('[show] home-mentionをされた人が見れる', async () => {
const res = await show(homeM.id, target);
assert.strictEqual(res.body.text, '@target x');
});
it('[show] home-mentionをフォロワーが見れる', async () => {
test('[show] home-mentionをフォロワーが見れる', async () => {
const res = await show(homeM.id, follower);
assert.strictEqual(res.body.text, '@target x');
});
it('[show] home-mentionを非フォロワーが見れる', async () => {
test('[show] home-mentionを非フォロワーが見れる', async () => {
const res = await show(homeM.id, other);
assert.strictEqual(res.body.text, '@target x');
});
it('[show] home-mentionを未認証が見れる', async () => {
test('[show] home-mentionを未認証が見れる', async () => {
const res = await show(homeM.id, null);
assert.strictEqual(res.body.text, '@target x');
});
// followers
it('[show] followers-mentionを自分が見れる', async () => {
test('[show] followers-mentionを自分が見れる', async () => {
const res = await show(folM.id, alice);
assert.strictEqual(res.body.text, '@target x');
});
it('[show] followers-mentionをメンションされていれば非フォロワーでも見れる', async () => {
test('[show] followers-mentionをメンションされていれば非フォロワーでも見れる', async () => {
const res = await show(folM.id, target);
assert.strictEqual(res.body.text, '@target x');
});
it('[show] followers-mentionをフォロワーが見れる', async () => {
test('[show] followers-mentionをフォロワーが見れる', async () => {
const res = await show(folM.id, follower);
assert.strictEqual(res.body.text, '@target x');
});
it('[show] followers-mentionを非フォロワーが見れない', async () => {
test('[show] followers-mentionを非フォロワーが見れない', async () => {
const res = await show(folM.id, other);
assert.strictEqual(res.body.isHidden, true);
});
it('[show] followers-mentionを未認証が見れない', async () => {
test('[show] followers-mentionを未認証が見れない', async () => {
const res = await show(folM.id, null);
assert.strictEqual(res.body.isHidden, true);
});
// specified
it('[show] specified-mentionを自分が見れる', async () => {
test('[show] specified-mentionを自分が見れる', async () => {
const res = await show(speM.id, alice);
assert.strictEqual(res.body.text, '@target2 x');
});
it('[show] specified-mentionを指定ユーザーが見れる', async () => {
test('[show] specified-mentionを指定ユーザーが見れる', async () => {
const res = await show(speM.id, target);
assert.strictEqual(res.body.text, '@target2 x');
});
it('[show] specified-mentionをされた人が指定されてなかったら見れない', async () => {
test('[show] specified-mentionをされた人が指定されてなかったら見れない', async () => {
const res = await show(speM.id, target2);
assert.strictEqual(res.body.isHidden, true);
});
it('[show] specified-mentionをフォロワーが見れない', async () => {
test('[show] specified-mentionをフォロワーが見れない', async () => {
const res = await show(speM.id, follower);
assert.strictEqual(res.body.isHidden, true);
});
it('[show] specified-mentionを非フォロワーが見れない', async () => {
test('[show] specified-mentionを非フォロワーが見れない', async () => {
const res = await show(speM.id, other);
assert.strictEqual(res.body.isHidden, true);
});
it('[show] specified-mentionを未認証が見れない', async () => {
test('[show] specified-mentionを未認証が見れない', async () => {
const res = await show(speM.id, null);
assert.strictEqual(res.body.isHidden, true);
});
//#endregion
//#region HTL
it('[HTL] public-post が 自分が見れる', async () => {
test('[HTL] public-post が 自分が見れる', async () => {
const res = await request('/notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes[0].text, 'x');
});
it('[HTL] public-post が 非フォロワーから見れない', async () => {
test('[HTL] public-post が 非フォロワーから見れない', async () => {
const res = await request('/notes/timeline', { limit: 100 }, other);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes.length, 0);
});
it('[HTL] followers-post が フォロワーから見れる', async () => {
test('[HTL] followers-post が フォロワーから見れる', async () => {
const res = await request('/notes/timeline', { limit: 100 }, follower);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === fol.id);
@@ -435,21 +435,21 @@ describe('API visibility', () => {
//#endregion
//#region RTL
it('[replies] followers-reply が フォロワーから見れる', async () => {
test('[replies] followers-reply が フォロワーから見れる', async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x');
});
it('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes.length, 0);
});
it('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id);
@@ -458,14 +458,14 @@ describe('API visibility', () => {
//#endregion
//#region MTL
it('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
const res = await request('/notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x');
});
it('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
const res = await request('/notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folM.id);

View File

@@ -22,7 +22,7 @@ describe('API', () => {
});
describe('General validation', () => {
it('wrong type', async(async () => {
test('wrong type', async(async () => {
const res = await request('/test', {
required: true,
string: 42,
@@ -30,14 +30,14 @@ describe('API', () => {
assert.strictEqual(res.status, 400);
}));
it('missing require param', async(async () => {
test('missing require param', async(async () => {
const res = await request('/test', {
string: 'a',
});
assert.strictEqual(res.status, 400);
}));
it('invalid misskey:id (empty string)', async(async () => {
test('invalid misskey:id (empty string)', async(async () => {
const res = await request('/test', {
required: true,
id: '',
@@ -45,7 +45,7 @@ describe('API', () => {
assert.strictEqual(res.status, 400);
}));
it('valid misskey:id', async(async () => {
test('valid misskey:id', async(async () => {
const res = await request('/test', {
required: true,
id: '8wvhjghbxu',
@@ -53,7 +53,7 @@ describe('API', () => {
assert.strictEqual(res.status, 200);
}));
it('default value', async(async () => {
test('default value', async(async () => {
const res = await request('/test', {
required: true,
string: 'a',
@@ -62,7 +62,7 @@ describe('API', () => {
assert.strictEqual(res.body.default, 'hello');
}));
it('can set null even if it has default value', async(async () => {
test('can set null even if it has default value', async(async () => {
const res = await request('/test', {
required: true,
nullableDefault: null,
@@ -71,7 +71,7 @@ describe('API', () => {
assert.strictEqual(res.body.nullableDefault, null);
}));
it('cannot set undefined if it has default value', async(async () => {
test('cannot set undefined if it has default value', async(async () => {
const res = await request('/test', {
required: true,
nullableDefault: undefined,

View File

@@ -23,7 +23,7 @@ describe('Block', () => {
await shutdownServer(p);
});
it('Block作成', async () => {
test('Block作成', async () => {
const res = await request('/blocking/create', {
userId: bob.id,
}, alice);
@@ -31,14 +31,14 @@ describe('Block', () => {
assert.strictEqual(res.status, 200);
});
it('ブロックされているユーザーをフォローできない', async () => {
test('ブロックされているユーザーをフォローできない', async () => {
const res = await request('/following/create', { userId: alice.id }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
});
it('ブロックされているユーザーにリアクションできない', async () => {
test('ブロックされているユーザーにリアクションできない', async () => {
const note = await post(alice, { text: 'hello' });
const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
@@ -47,7 +47,7 @@ describe('Block', () => {
assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
});
it('ブロックされているユーザーに返信できない', async () => {
test('ブロックされているユーザーに返信できない', async () => {
const note = await post(alice, { text: 'hello' });
const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob);
@@ -56,7 +56,7 @@ describe('Block', () => {
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
});
it('ブロックされているユーザーのートをRenoteできない', async () => {
test('ブロックされているユーザーのートをRenoteできない', async () => {
const note = await post(alice, { text: 'hello' });
const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob);
@@ -69,7 +69,7 @@ describe('Block', () => {
// TODO: ユーザーリストから除外されるテスト
it('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async () => {
test('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async () => {
const aliceNote = await post(alice);
const bobNote = await post(bob);
const carolNote = await post(carol);

View File

@@ -22,7 +22,7 @@ describe('Endpoints', () => {
});
describe('signup', () => {
it('不正なユーザー名でアカウントが作成できない', async () => {
test('不正なユーザー名でアカウントが作成できない', async () => {
const res = await request('api/signup', {
username: 'test.',
password: 'test',
@@ -30,7 +30,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400);
});
it('空のパスワードでアカウントが作成できない', async () => {
test('空のパスワードでアカウントが作成できない', async () => {
const res = await request('api/signup', {
username: 'test',
password: '',
@@ -38,7 +38,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400);
});
it('正しくアカウントが作成できる', async () => {
test('正しくアカウントが作成できる', async () => {
const me = {
username: 'test1',
password: 'test1',
@@ -51,7 +51,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.username, me.username);
});
it('同じユーザー名のアカウントは作成できない', async () => {
test('同じユーザー名のアカウントは作成できない', async () => {
const res = await request('api/signup', {
username: 'test1',
password: 'test1',
@@ -62,7 +62,7 @@ describe('Endpoints', () => {
});
describe('signin', () => {
it('間違ったパスワードでサインインできない', async () => {
test('間違ったパスワードでサインインできない', async () => {
const res = await request('api/signin', {
username: 'test1',
password: 'bar',
@@ -71,7 +71,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 403);
});
it('クエリをインジェクションできない', async () => {
test('クエリをインジェクションできない', async () => {
const res = await request('api/signin', {
username: 'test1',
password: {
@@ -82,7 +82,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400);
});
it('正しい情報でサインインできる', async () => {
test('正しい情報でサインインできる', async () => {
const res = await request('api/signin', {
username: 'test1',
password: 'test1',
@@ -93,7 +93,7 @@ describe('Endpoints', () => {
});
describe('i/update', () => {
it('アカウント設定を更新できる', async () => {
test('アカウント設定を更新できる', async () => {
const myName = '大室櫻子';
const myLocation = '七森中';
const myBirthday = '2000-09-07';
@@ -111,14 +111,14 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.birthday, myBirthday);
});
it('名前を空白にできない', async () => {
test('名前を空白にできない', async () => {
const res = await api('/i/update', {
name: ' ',
}, alice);
assert.strictEqual(res.status, 400);
});
it('誕生日の設定を削除できる', async () => {
test('誕生日の設定を削除できる', async () => {
await api('/i/update', {
birthday: '2000-09-07',
}, alice);
@@ -132,7 +132,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.birthday, null);
});
it('不正な誕生日の形式で怒られる', async () => {
test('不正な誕生日の形式で怒られる', async () => {
const res = await api('/i/update', {
birthday: '2000/09/07',
}, alice);
@@ -141,7 +141,7 @@ describe('Endpoints', () => {
});
describe('users/show', () => {
it('ユーザーが取得できる', async () => {
test('ユーザーが取得できる', async () => {
const res = await api('/users/show', {
userId: alice.id,
}, alice);
@@ -151,14 +151,14 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.id, alice.id);
});
it('ユーザーが存在しなかったら怒る', async () => {
test('ユーザーが存在しなかったら怒る', async () => {
const res = await api('/users/show', {
userId: '000000000000000000000000',
});
assert.strictEqual(res.status, 400);
});
it('間違ったIDで怒られる', async () => {
test('間違ったIDで怒られる', async () => {
const res = await api('/users/show', {
userId: 'kyoppie',
});
@@ -167,7 +167,7 @@ describe('Endpoints', () => {
});
describe('notes/show', () => {
it('投稿が取得できる', async () => {
test('投稿が取得できる', async () => {
const myPost = await post(alice, {
text: 'test',
});
@@ -182,14 +182,14 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.text, myPost.text);
});
it('投稿が存在しなかったら怒る', async () => {
test('投稿が存在しなかったら怒る', async () => {
const res = await api('/notes/show', {
noteId: '000000000000000000000000',
});
assert.strictEqual(res.status, 400);
});
it('間違ったIDで怒られる', async () => {
test('間違ったIDで怒られる', async () => {
const res = await api('/notes/show', {
noteId: 'kyoppie',
});
@@ -198,7 +198,7 @@ describe('Endpoints', () => {
});
describe('notes/reactions/create', () => {
it('リアクションできる', async () => {
test('リアクションできる', async () => {
const bobPost = await post(bob);
const alice = await signup({ username: 'alice' });
@@ -217,7 +217,7 @@ describe('Endpoints', () => {
assert.strictEqual(resNote.body.reactions['🚀'], [alice.id]);
});
it('自分の投稿にもリアクションできる', async () => {
test('自分の投稿にもリアクションできる', async () => {
const myPost = await post(alice);
const res = await api('/notes/reactions/create', {
@@ -228,7 +228,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 204);
});
it('二重にリアクションできない', async () => {
test('二重にリアクションできない', async () => {
const bobPost = await post(bob);
await api('/notes/reactions/create', {
@@ -244,7 +244,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400);
});
it('存在しない投稿にはリアクションできない', async () => {
test('存在しない投稿にはリアクションできない', async () => {
const res = await api('/notes/reactions/create', {
noteId: '000000000000000000000000',
reaction: '🚀',
@@ -253,13 +253,13 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400);
});
it('空のパラメータで怒られる', async () => {
test('空のパラメータで怒られる', async () => {
const res = await api('/notes/reactions/create', {}, alice);
assert.strictEqual(res.status, 400);
});
it('間違ったIDで怒られる', async () => {
test('間違ったIDで怒られる', async () => {
const res = await api('/notes/reactions/create', {
noteId: 'kyoppie',
reaction: '🚀',
@@ -270,7 +270,7 @@ describe('Endpoints', () => {
});
describe('following/create', () => {
it('フォローできる', async () => {
test('フォローできる', async () => {
const res = await api('/following/create', {
userId: alice.id,
}, bob);
@@ -278,7 +278,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200);
});
it('既にフォローしている場合は怒る', async () => {
test('既にフォローしている場合は怒る', async () => {
const res = await api('/following/create', {
userId: alice.id,
}, bob);
@@ -286,7 +286,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400);
});
it('存在しないユーザーはフォローできない', async () => {
test('存在しないユーザーはフォローできない', async () => {
const res = await api('/following/create', {
userId: '000000000000000000000000',
}, alice);
@@ -294,7 +294,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400);
});
it('自分自身はフォローできない', async () => {
test('自分自身はフォローできない', async () => {
const res = await api('/following/create', {
userId: alice.id,
}, alice);
@@ -302,13 +302,13 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400);
});
it('空のパラメータで怒られる', async () => {
test('空のパラメータで怒られる', async () => {
const res = await api('/following/create', {}, alice);
assert.strictEqual(res.status, 400);
});
it('間違ったIDで怒られる', async () => {
test('間違ったIDで怒られる', async () => {
const res = await api('/following/create', {
userId: 'foo',
}, alice);
@@ -318,7 +318,7 @@ describe('Endpoints', () => {
});
describe('following/delete', () => {
it('フォロー解除できる', async () => {
test('フォロー解除できる', async () => {
await api('/following/create', {
userId: alice.id,
}, bob);
@@ -330,7 +330,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200);
});
it('フォローしていない場合は怒る', async () => {
test('フォローしていない場合は怒る', async () => {
const res = await api('/following/delete', {
userId: alice.id,
}, bob);
@@ -338,7 +338,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400);
});
it('存在しないユーザーはフォロー解除できない', async () => {
test('存在しないユーザーはフォロー解除できない', async () => {
const res = await api('/following/delete', {
userId: '000000000000000000000000',
}, alice);
@@ -346,7 +346,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400);
});
it('自分自身はフォロー解除できない', async () => {
test('自分自身はフォロー解除できない', async () => {
const res = await api('/following/delete', {
userId: alice.id,
}, alice);
@@ -354,13 +354,13 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400);
});
it('空のパラメータで怒られる', async () => {
test('空のパラメータで怒られる', async () => {
const res = await api('/following/delete', {}, alice);
assert.strictEqual(res.status, 400);
});
it('間違ったIDで怒られる', async () => {
test('間違ったIDで怒られる', async () => {
const res = await api('/following/delete', {
userId: 'kyoppie',
}, alice);
@@ -371,7 +371,7 @@ describe('Endpoints', () => {
/*
describe('/i', () => {
it('', async () => {
test('', async () => {
});
});
*/
@@ -402,7 +402,7 @@ describe('API: Endpoints', () => {
});
describe('drive', () => {
it('ドライブ情報を取得できる', async () => {
test('ドライブ情報を取得できる', async () => {
await uploadFile({
userId: alice.id,
size: 256
@@ -423,7 +423,7 @@ describe('API: Endpoints', () => {
});
describe('drive/files/create', () => {
it('ファイルを作成できる', async () => {
test('ファイルを作成できる', async () => {
const res = await uploadFile(alice);
assert.strictEqual(res.status, 200);
@@ -431,7 +431,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.name, 'Lenna.png');
}));
it('ファイルに名前を付けられる', async () => {
test('ファイルに名前を付けられる', async () => {
const res = await assert.request(server)
.post('/drive/files/create')
.field('i', alice.token)
@@ -443,13 +443,13 @@ describe('API: Endpoints', () => {
expect(res.body).have.property('name').eql('Belmond.png');
}));
it('ファイル無しで怒られる', async () => {
test('ファイル無しで怒られる', async () => {
const res = await api('/drive/files/create', {}, alice);
assert.strictEqual(res.status, 400);
}));
it('SVGファイルを作成できる', async () => {
test('SVGファイルを作成できる', async () => {
const res = await uploadFile(alice, __dirname + '/resources/image.svg');
assert.strictEqual(res.status, 200);
@@ -460,7 +460,7 @@ describe('API: Endpoints', () => {
});
describe('drive/files/update', () => {
it('名前を更新できる', async () => {
test('名前を更新できる', async () => {
const file = await uploadFile(alice);
const newName = 'いちごパスタ.png';
@@ -474,7 +474,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.name, newName);
}));
it('他人のファイルは更新できない', async () => {
test('他人のファイルは更新できない', async () => {
const file = await uploadFile(bob);
const res = await api('/drive/files/update', {
@@ -485,7 +485,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('親フォルダを更新できる', async () => {
test('親フォルダを更新できる', async () => {
const file = await uploadFile(alice);
const folder = (await api('/drive/folders/create', {
name: 'test'
@@ -501,7 +501,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.folderId, folder.id);
}));
it('親フォルダを無しにできる', async () => {
test('親フォルダを無しにできる', async () => {
const file = await uploadFile(alice);
const folder = (await api('/drive/folders/create', {
@@ -523,7 +523,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.folderId, null);
}));
it('他人のフォルダには入れられない', async () => {
test('他人のフォルダには入れられない', async () => {
const file = await uploadFile(alice);
const folder = (await api('/drive/folders/create', {
name: 'test'
@@ -537,7 +537,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('存在しないフォルダで怒られる', async () => {
test('存在しないフォルダで怒られる', async () => {
const file = await uploadFile(alice);
const res = await api('/drive/files/update', {
@@ -548,7 +548,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('不正なフォルダIDで怒られる', async () => {
test('不正なフォルダIDで怒られる', async () => {
const file = await uploadFile(alice);
const res = await api('/drive/files/update', {
@@ -559,7 +559,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('ファイルが存在しなかったら怒る', async () => {
test('ファイルが存在しなかったら怒る', async () => {
const res = await api('/drive/files/update', {
fileId: '000000000000000000000000',
name: 'いちごパスタ.png'
@@ -568,7 +568,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async () => {
test('間違ったIDで怒られる', async () => {
const res = await api('/drive/files/update', {
fileId: 'kyoppie',
name: 'いちごパスタ.png'
@@ -579,7 +579,7 @@ describe('API: Endpoints', () => {
});
describe('drive/folders/create', () => {
it('フォルダを作成できる', async () => {
test('フォルダを作成できる', async () => {
const res = await api('/drive/folders/create', {
name: 'test'
}, alice);
@@ -591,7 +591,7 @@ describe('API: Endpoints', () => {
});
describe('drive/folders/update', () => {
it('名前を更新できる', async () => {
test('名前を更新できる', async () => {
const folder = (await api('/drive/folders/create', {
name: 'test'
}, alice)).body;
@@ -606,7 +606,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.name, 'new name');
}));
it('他人のフォルダを更新できない', async () => {
test('他人のフォルダを更新できない', async () => {
const folder = (await api('/drive/folders/create', {
name: 'test'
}, bob)).body;
@@ -619,7 +619,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('親フォルダを更新できる', async () => {
test('親フォルダを更新できる', async () => {
const folder = (await api('/drive/folders/create', {
name: 'test'
}, alice)).body;
@@ -637,7 +637,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.parentId, parentFolder.id);
}));
it('親フォルダを無しに更新できる', async () => {
test('親フォルダを無しに更新できる', async () => {
const folder = (await api('/drive/folders/create', {
name: 'test'
}, alice)).body;
@@ -659,7 +659,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.parentId, null);
}));
it('他人のフォルダを親フォルダに設定できない', async () => {
test('他人のフォルダを親フォルダに設定できない', async () => {
const folder = (await api('/drive/folders/create', {
name: 'test'
}, alice)).body;
@@ -675,7 +675,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない', async () => {
test('フォルダが循環するような構造にできない', async () => {
const folder = (await api('/drive/folders/create', {
name: 'test'
}, alice)).body;
@@ -695,7 +695,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない(再帰的)', async () => {
test('フォルダが循環するような構造にできない(再帰的)', async () => {
const folderA = (await api('/drive/folders/create', {
name: 'test'
}, alice)).body;
@@ -722,7 +722,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない(自身)', async () => {
test('フォルダが循環するような構造にできない(自身)', async () => {
const folderA = (await api('/drive/folders/create', {
name: 'test'
}, alice)).body;
@@ -735,7 +735,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('存在しない親フォルダを設定できない', async () => {
test('存在しない親フォルダを設定できない', async () => {
const folder = (await api('/drive/folders/create', {
name: 'test'
}, alice)).body;
@@ -748,7 +748,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('不正な親フォルダIDで怒られる', async () => {
test('不正な親フォルダIDで怒られる', async () => {
const folder = (await api('/drive/folders/create', {
name: 'test'
}, alice)).body;
@@ -761,7 +761,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('存在しないフォルダを更新できない', async () => {
test('存在しないフォルダを更新できない', async () => {
const res = await api('/drive/folders/update', {
folderId: '000000000000000000000000'
}, alice);
@@ -769,7 +769,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('不正なフォルダIDで怒られる', async () => {
test('不正なフォルダIDで怒られる', async () => {
const res = await api('/drive/folders/update', {
folderId: 'foo'
}, alice);
@@ -779,7 +779,7 @@ describe('API: Endpoints', () => {
});
describe('messaging/messages/create', () => {
it('メッセージを送信できる', async () => {
test('メッセージを送信できる', async () => {
const res = await api('/messaging/messages/create', {
userId: bob.id,
text: 'test'
@@ -790,7 +790,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.text, 'test');
}));
it('自分自身にはメッセージを送信できない', async () => {
test('自分自身にはメッセージを送信できない', async () => {
const res = await api('/messaging/messages/create', {
userId: alice.id,
text: 'Yo'
@@ -799,7 +799,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('存在しないユーザーにはメッセージを送信できない', async () => {
test('存在しないユーザーにはメッセージを送信できない', async () => {
const res = await api('/messaging/messages/create', {
userId: '000000000000000000000000',
text: 'test'
@@ -808,7 +808,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('不正なユーザーIDで怒られる', async () => {
test('不正なユーザーIDで怒られる', async () => {
const res = await api('/messaging/messages/create', {
userId: 'foo',
text: 'test'
@@ -817,7 +817,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('テキストが無くて怒られる', async () => {
test('テキストが無くて怒られる', async () => {
const res = await api('/messaging/messages/create', {
userId: bob.id
}, alice);
@@ -825,7 +825,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400);
}));
it('文字数オーバーで怒られる', async () => {
test('文字数オーバーで怒られる', async () => {
const res = await api('/messaging/messages/create', {
userId: bob.id,
text: '!'.repeat(1001)
@@ -836,7 +836,7 @@ describe('API: Endpoints', () => {
});
describe('notes/replies', () => {
it('自分に閲覧権限のない投稿は含まれない', async () => {
test('自分に閲覧権限のない投稿は含まれない', async () => {
const alicePost = await post(alice, {
text: 'foo'
});
@@ -859,7 +859,7 @@ describe('API: Endpoints', () => {
});
describe('notes/timeline', () => {
it('フォロワー限定投稿が含まれる', async () => {
test('フォロワー限定投稿が含まれる', async () => {
await api('/following/create', {
userId: alice.id
}, bob);

View File

@@ -35,38 +35,38 @@ describe('Fetch resource', () => {
});
describe('Common', () => {
it('meta', async () => {
test('meta', async () => {
const res = await request('/meta', {
});
assert.strictEqual(res.status, 200);
});
it('GET root', async () => {
test('GET root', async () => {
const res = await simpleGet('/');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
});
it('GET docs', async () => {
test('GET docs', async () => {
const res = await simpleGet('/docs/ja-JP/about');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
});
it('GET api-doc', async () => {
test('GET api-doc', async () => {
const res = await simpleGet('/api-doc');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
});
it('GET api.json', async () => {
test('GET api.json', async () => {
const res = await simpleGet('/api.json');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, JSON);
});
it('Validate api.json', async () => {
test('Validate api.json', async () => {
const config = await openapi.loadConfig();
const result = await openapi.bundle({
config,
@@ -80,25 +80,25 @@ describe('Fetch resource', () => {
assert.strictEqual(result.problems.length, 0);
});
it('GET favicon.ico', async () => {
test('GET favicon.ico', async () => {
const res = await simpleGet('/favicon.ico');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/x-icon');
});
it('GET apple-touch-icon.png', async () => {
test('GET apple-touch-icon.png', async () => {
const res = await simpleGet('/apple-touch-icon.png');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/png');
});
it('GET twemoji svg', async () => {
test('GET twemoji svg', async () => {
const res = await simpleGet('/twemoji/2764.svg');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/svg+xml');
});
it('GET twemoji svg with hyphen', async () => {
test('GET twemoji svg with hyphen', async () => {
const res = await simpleGet('/twemoji/2764-fe0f-200d-1f525.svg');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/svg+xml');
@@ -106,25 +106,25 @@ describe('Fetch resource', () => {
});
describe('/@:username', () => {
it('Only AP => AP', async () => {
test('Only AP => AP', async () => {
const res = await simpleGet(`/@${alice.username}`, ONLY_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
});
it('Prefer AP => AP', async () => {
test('Prefer AP => AP', async () => {
const res = await simpleGet(`/@${alice.username}`, PREFER_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
});
it('Prefer HTML => HTML', async () => {
test('Prefer HTML => HTML', async () => {
const res = await simpleGet(`/@${alice.username}`, PREFER_HTML);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
});
it('Unspecified => HTML', async () => {
test('Unspecified => HTML', async () => {
const res = await simpleGet(`/@${alice.username}`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
@@ -132,25 +132,25 @@ describe('Fetch resource', () => {
});
describe('/users/:id', () => {
it('Only AP => AP', async () => {
test('Only AP => AP', async () => {
const res = await simpleGet(`/users/${alice.id}`, ONLY_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
});
it('Prefer AP => AP', async () => {
test('Prefer AP => AP', async () => {
const res = await simpleGet(`/users/${alice.id}`, PREFER_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
});
it('Prefer HTML => Redirect to /@:username', async () => {
test('Prefer HTML => Redirect to /@:username', async () => {
const res = await simpleGet(`/users/${alice.id}`, PREFER_HTML);
assert.strictEqual(res.status, 302);
assert.strictEqual(res.location, `/@${alice.username}`);
});
it('Undecided => HTML', async () => {
test('Undecided => HTML', async () => {
const res = await simpleGet(`/users/${alice.id}`, UNSPECIFIED);
assert.strictEqual(res.status, 302);
assert.strictEqual(res.location, `/@${alice.username}`);
@@ -158,25 +158,25 @@ describe('Fetch resource', () => {
});
describe('/notes/:id', () => {
it('Only AP => AP', async () => {
test('Only AP => AP', async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, ONLY_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
});
it('Prefer AP => AP', async () => {
test('Prefer AP => AP', async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
});
it('Prefer HTML => HTML', async () => {
test('Prefer HTML => HTML', async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_HTML);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
});
it('Unspecified => HTML', async () => {
test('Unspecified => HTML', async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
@@ -184,19 +184,19 @@ describe('Fetch resource', () => {
});
describe('Feeds', () => {
it('RSS', async () => {
test('RSS', async () => {
const res = await simpleGet(`/@${alice.username}.rss`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'application/rss+xml; charset=utf-8');
});
it('ATOM', async () => {
test('ATOM', async () => {
const res = await simpleGet(`/@${alice.username}.atom`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'application/atom+xml; charset=utf-8');
});
it('JSON', async () => {
test('JSON', async () => {
const res = await simpleGet(`/@${alice.username}.json`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'application/json; charset=utf-8');

View File

@@ -22,7 +22,7 @@ describe('FF visibility', () => {
await shutdownServer(p);
});
it('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
test('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
await request('/i/update', {
ffVisibility: 'public',
}, alice);
@@ -40,7 +40,7 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true);
});
it('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => {
test('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => {
await request('/i/update', {
ffVisibility: 'followers',
}, alice);
@@ -58,7 +58,7 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true);
});
it('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => {
test('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => {
await request('/i/update', {
ffVisibility: 'followers',
}, alice);
@@ -74,7 +74,7 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 400);
});
it('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => {
test('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => {
await request('/i/update', {
ffVisibility: 'followers',
}, alice);
@@ -96,7 +96,7 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true);
});
it('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => {
test('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => {
await request('/i/update', {
ffVisibility: 'private',
}, alice);
@@ -114,7 +114,7 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true);
});
it('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => {
test('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => {
await request('/i/update', {
ffVisibility: 'private',
}, alice);
@@ -131,7 +131,7 @@ describe('FF visibility', () => {
});
describe('AP', () => {
it('ffVisibility が public 以外ならばAPからは取得できない', async () => {
test('ffVisibility が public 以外ならばAPからは取得できない', async () => {
{
await request('/i/update', {
ffVisibility: 'public',

View File

@@ -23,7 +23,7 @@ describe('Mute', () => {
await shutdownServer(p);
});
it('ミュート作成', async () => {
test('ミュート作成', async () => {
const res = await request('/mute/create', {
userId: carol.id,
}, alice);
@@ -31,7 +31,7 @@ describe('Mute', () => {
assert.strictEqual(res.status, 204);
});
it('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => {
test('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => {
const bobNote = await post(bob, { text: '@alice hi' });
const carolNote = await post(carol, { text: '@alice hi' });
@@ -43,7 +43,7 @@ describe('Mute', () => {
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
});
it('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
@@ -55,7 +55,7 @@ describe('Mute', () => {
assert.strictEqual(res.body.hasUnreadMentions, false);
});
it('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => {
test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
@@ -64,7 +64,7 @@ describe('Mute', () => {
assert.strictEqual(fired, false);
});
it('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => {
test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
await request('/notifications/mark-all-as-read', {}, alice);
@@ -75,7 +75,7 @@ describe('Mute', () => {
});
describe('Timeline', () => {
it('タイムラインにミュートしているユーザーの投稿が含まれない', async () => {
test('タイムラインにミュートしているユーザーの投稿が含まれない', async () => {
const aliceNote = await post(alice);
const bobNote = await post(bob);
const carolNote = await post(carol);
@@ -89,7 +89,7 @@ describe('Mute', () => {
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
});
it('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
const aliceNote = await post(alice);
const carolNote = await post(carol);
const bobNote = await post(bob, {
@@ -107,7 +107,7 @@ describe('Mute', () => {
});
describe('Notification', () => {
it('通知にミュートしているユーザーの通知が含まれない(リアクション)', async () => {
test('通知にミュートしているユーザーの通知が含まれない(リアクション)', async () => {
const aliceNote = await post(alice);
await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like');

View File

@@ -24,7 +24,7 @@ describe('Note', () => {
await shutdownServer(p);
});
it('投稿できる', async () => {
test('投稿できる', async () => {
const post = {
text: 'test',
};
@@ -36,7 +36,7 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.text, post.text);
});
it('ファイルを添付できる', async () => {
test('ファイルを添付できる', async () => {
const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const res = await request('/notes/create', {
@@ -48,7 +48,7 @@ describe('Note', () => {
assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]);
}, 1000 * 10);
it('他人のファイルは無視', async () => {
test('他人のファイルは無視', async () => {
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const res = await request('/notes/create', {
@@ -61,7 +61,7 @@ describe('Note', () => {
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
}, 1000 * 10);
it('存在しないファイルは無視', async () => {
test('存在しないファイルは無視', async () => {
const res = await request('/notes/create', {
text: 'test',
fileIds: ['000000000000000000000000'],
@@ -72,7 +72,7 @@ describe('Note', () => {
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
});
it('不正なファイルIDは無視', async () => {
test('不正なファイルIDは無視', async () => {
const res = await request('/notes/create', {
fileIds: ['kyoppie'],
}, alice);
@@ -81,7 +81,7 @@ describe('Note', () => {
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
});
it('返信できる', async () => {
test('返信できる', async () => {
const bobPost = await post(bob, {
text: 'foo',
});
@@ -100,7 +100,7 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
});
it('renoteできる', async () => {
test('renoteできる', async () => {
const bobPost = await post(bob, {
text: 'test',
});
@@ -117,7 +117,7 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
});
it('引用renoteできる', async () => {
test('引用renoteできる', async () => {
const bobPost = await post(bob, {
text: 'test',
});
@@ -136,7 +136,7 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
});
it('文字数ぎりぎりで怒られない', async () => {
test('文字数ぎりぎりで怒られない', async () => {
const post = {
text: '!'.repeat(3000),
};
@@ -144,7 +144,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 200);
});
it('文字数オーバーで怒られる', async () => {
test('文字数オーバーで怒られる', async () => {
const post = {
text: '!'.repeat(3001),
};
@@ -152,7 +152,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400);
});
it('存在しないリプライ先で怒られる', async () => {
test('存在しないリプライ先で怒られる', async () => {
const post = {
text: 'test',
replyId: '000000000000000000000000',
@@ -161,7 +161,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400);
});
it('存在しないrenote対象で怒られる', async () => {
test('存在しないrenote対象で怒られる', async () => {
const post = {
renoteId: '000000000000000000000000',
};
@@ -169,7 +169,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400);
});
it('不正なリプライ先IDで怒られる', async () => {
test('不正なリプライ先IDで怒られる', async () => {
const post = {
text: 'test',
replyId: 'foo',
@@ -178,7 +178,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400);
});
it('不正なrenote対象IDで怒られる', async () => {
test('不正なrenote対象IDで怒られる', async () => {
const post = {
renoteId: 'foo',
};
@@ -186,7 +186,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400);
});
it('存在しないユーザーにメンションできる', async () => {
test('存在しないユーザーにメンションできる', async () => {
const post = {
text: '@ghost yo',
};
@@ -198,7 +198,7 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.text, post.text);
});
it('同じユーザーに複数メンションしても内部的にまとめられる', async () => {
test('同じユーザーに複数メンションしても内部的にまとめられる', async () => {
const post = {
text: '@bob @bob @bob yo',
};
@@ -214,7 +214,7 @@ describe('Note', () => {
});
describe('notes/create', () => {
it('投票を添付できる', async () => {
test('投票を添付できる', async () => {
const res = await request('/notes/create', {
text: 'test',
poll: {
@@ -227,14 +227,14 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.poll != null, true);
});
it('投票の選択肢が無くて怒られる', async () => {
test('投票の選択肢が無くて怒られる', async () => {
const res = await request('/notes/create', {
poll: {},
}, alice);
assert.strictEqual(res.status, 400);
});
it('投票の選択肢が無くて怒られる (空の配列)', async () => {
test('投票の選択肢が無くて怒られる (空の配列)', async () => {
const res = await request('/notes/create', {
poll: {
choices: [],
@@ -243,7 +243,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400);
});
it('投票の選択肢が1つで怒られる', async () => {
test('投票の選択肢が1つで怒られる', async () => {
const res = await request('/notes/create', {
poll: {
choices: ['Strawberry Pasta'],
@@ -252,7 +252,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400);
});
it('投票できる', async () => {
test('投票できる', async () => {
const { body } = await request('/notes/create', {
text: 'test',
poll: {
@@ -268,7 +268,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 204);
});
it('複数投票できない', async () => {
test('複数投票できない', async () => {
const { body } = await request('/notes/create', {
text: 'test',
poll: {
@@ -289,7 +289,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400);
});
it('許可されている場合は複数投票できる', async () => {
test('許可されている場合は複数投票できる', async () => {
const { body } = await request('/notes/create', {
text: 'test',
poll: {
@@ -316,7 +316,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 204);
});
it('締め切られている場合は投票できない', async () => {
test('締め切られている場合は投票できない', async () => {
const { body } = await request('/notes/create', {
text: 'test',
poll: {
@@ -337,7 +337,7 @@ describe('Note', () => {
});
describe('notes/delete', () => {
it('delete a reply', async () => {
test('delete a reply', async () => {
const mainNoteRes = await api('notes/create', {
text: 'main post',
}, alice);

View File

@@ -78,7 +78,7 @@ describe('Streaming', () => {
});
describe('Events', () => {
it('mention event', async () => {
test('mention event', async () => {
const fired = await waitFire(
kyoko, 'main', // kyoko:main
() => post(ayano, { text: 'foo @kyoko bar' }), // ayano mention => kyoko
@@ -88,7 +88,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
it('renote event', async () => {
test('renote event', async () => {
const fired = await waitFire(
kyoko, 'main', // kyoko:main
() => post(ayano, { renoteId: kyokoNote.id }), // ayano renote
@@ -100,7 +100,7 @@ describe('Streaming', () => {
});
describe('Home Timeline', () => {
it('自分の投稿が流れる', async () => {
test('自分の投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:Home
() => api('notes/create', { text: 'foo' }, ayano), // ayano posts
@@ -110,7 +110,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
it('フォローしているユーザーの投稿が流れる', async () => {
test('フォローしているユーザーの投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home
() => api('notes/create', { text: 'foo' }, kyoko), // kyoko posts
@@ -120,7 +120,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
it('フォローしていないユーザーの投稿は流れない', async () => {
test('フォローしていないユーザーの投稿は流れない', async () => {
const fired = await waitFire(
kyoko, 'homeTimeline', // kyoko:home
() => api('notes/create', { text: 'foo' }, ayano), // ayano posts
@@ -130,7 +130,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, false);
});
it('フォローしているユーザーのダイレクト投稿が流れる', async () => {
test('フォローしているユーザーのダイレクト投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko dm => ayano
@@ -140,7 +140,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
it('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', async () => {
test('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, kyoko), // kyoko dm => chitose
@@ -152,7 +152,7 @@ describe('Streaming', () => {
}); // Home
describe('Local Timeline', () => {
it('自分の投稿が流れる', async () => {
test('自分の投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo' }, ayano), // ayano posts
@@ -162,7 +162,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
it('フォローしていないローカルユーザーの投稿が流れる', async () => {
test('フォローしていないローカルユーザーの投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo' }, chitose), // chitose posts
@@ -172,7 +172,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
it('リモートユーザーの投稿は流れない', async () => {
test('リモートユーザーの投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts
@@ -182,7 +182,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, false);
});
it('フォローしてたとしてもリモートユーザーの投稿は流れない', async () => {
test('フォローしてたとしてもリモートユーザーの投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo' }, akari), // akari posts
@@ -192,7 +192,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, false);
});
it('ホーム指定の投稿は流れない', async () => {
test('ホーム指定の投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko home posts
@@ -202,7 +202,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, false);
});
it('フォローしているローカルユーザーのダイレクト投稿は流れない', async () => {
test('フォローしているローカルユーザーのダイレクト投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko DM => ayano
@@ -212,7 +212,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, false);
});
it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => {
test('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose),
@@ -224,7 +224,7 @@ describe('Streaming', () => {
});
describe('Hybrid Timeline', () => {
it('自分の投稿が流れる', async () => {
test('自分の投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo' }, ayano), // ayano posts
@@ -234,7 +234,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
it('フォローしていないローカルユーザーの投稿が流れる', async () => {
test('フォローしていないローカルユーザーの投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo' }, chitose), // chitose posts
@@ -244,7 +244,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
it('フォローしているリモートユーザーの投稿が流れる', async () => {
test('フォローしているリモートユーザーの投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo' }, akari), // akari posts
@@ -254,7 +254,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
it('フォローしていないリモートユーザーの投稿は流れない', async () => {
test('フォローしていないリモートユーザーの投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts
@@ -264,7 +264,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, false);
});
it('フォローしているユーザーのダイレクト投稿が流れる', async () => {
test('フォローしているユーザーのダイレクト投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko),
@@ -274,7 +274,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
it('フォローしているユーザーのホーム投稿が流れる', async () => {
test('フォローしているユーザーのホーム投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko),
@@ -284,7 +284,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
it('フォローしていないローカルユーザーのホーム投稿は流れない', async () => {
test('フォローしていないローカルユーザーのホーム投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo', visibility: 'home' }, chitose),
@@ -294,7 +294,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, false);
});
it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => {
test('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose),
@@ -306,7 +306,7 @@ describe('Streaming', () => {
});
describe('Global Timeline', () => {
it('フォローしていないローカルユーザーの投稿が流れる', async () => {
test('フォローしていないローカルユーザーの投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'globalTimeline', // ayano:Global
() => api('notes/create', { text: 'foo' }, chitose), // chitose posts
@@ -316,7 +316,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
it('フォローしていないリモートユーザーの投稿が流れる', async () => {
test('フォローしていないリモートユーザーの投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'globalTimeline', // ayano:Global
() => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts
@@ -326,7 +326,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
it('ホーム投稿は流れない', async () => {
test('ホーム投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'globalTimeline', // ayano:Global
() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko posts
@@ -338,7 +338,7 @@ describe('Streaming', () => {
});
describe('UserList Timeline', () => {
it('リストに入れているユーザーの投稿が流れる', async () => {
test('リストに入れているユーザーの投稿が流れる', async () => {
const fired = await waitFire(
chitose, 'userList',
() => api('notes/create', { text: 'foo' }, ayano),
@@ -349,7 +349,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
it('リストに入れていないユーザーの投稿は流れない', async () => {
test('リストに入れていないユーザーの投稿は流れない', async () => {
const fired = await waitFire(
chitose, 'userList',
() => api('notes/create', { text: 'foo' }, chinatsu),
@@ -361,7 +361,7 @@ describe('Streaming', () => {
});
// #4471
it('リストに入れているユーザーのダイレクト投稿が流れる', async () => {
test('リストに入れているユーザーのダイレクト投稿が流れる', async () => {
const fired = await waitFire(
chitose, 'userList',
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, ayano),
@@ -373,7 +373,7 @@ describe('Streaming', () => {
});
// #4335
it('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', async () => {
test('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', async () => {
const fired = await waitFire(
chitose, 'userList',
() => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko),
@@ -386,7 +386,7 @@ describe('Streaming', () => {
});
describe('Hashtag Timeline', () => {
it('指定したハッシュタグの投稿が流れる', () => new Promise<void>(async done => {
test('指定したハッシュタグの投稿が流れる', () => new Promise<void>(async done => {
const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
if (type === 'note') {
assert.deepStrictEqual(body.text, '#foo');
@@ -404,7 +404,7 @@ describe('Streaming', () => {
});
}));
it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise<void>(async done => {
test('指定したハッシュタグの投稿が流れる (AND)', () => new Promise<void>(async done => {
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;
@@ -442,7 +442,7 @@ describe('Streaming', () => {
}, 3000);
}));
it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise<void>(async done => {
test('指定したハッシュタグの投稿が流れる (OR)', () => new Promise<void>(async done => {
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;
@@ -488,7 +488,7 @@ describe('Streaming', () => {
}, 3000);
}));
it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise<void>(async done => {
test('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise<void>(async done => {
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;

View File

@@ -22,7 +22,7 @@ describe('Note thread mute', () => {
await shutdownServer(p);
});
it('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => {
test('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => {
const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
@@ -40,7 +40,7 @@ describe('Note thread mute', () => {
assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false);
});
it('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
@@ -56,7 +56,7 @@ describe('Note thread mute', () => {
assert.strictEqual(res.body.hasUnreadMentions, false);
});
it('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => {
test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
@@ -82,7 +82,7 @@ describe('Note thread mute', () => {
}, 5000);
}));
it('i/notifications にミュートしているスレッドの通知が含まれない', async () => {
test('i/notifications にミュートしているスレッドの通知が含まれない', async () => {
const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });

View File

@@ -32,7 +32,7 @@ describe('users/notes', () => {
await shutdownServer(p);
});
it('ファイルタイプ指定 (jpg)', async () => {
test('ファイルタイプ指定 (jpg)', async () => {
const res = await request('/users/notes', {
userId: alice.id,
fileType: ['image/jpeg'],
@@ -45,7 +45,7 @@ describe('users/notes', () => {
assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true);
});
it('ファイルタイプ指定 (jpg or png)', async () => {
test('ファイルタイプ指定 (jpg or png)', async () => {
const res = await request('/users/notes', {
userId: alice.id,
fileType: ['image/jpeg', 'image/png'],

View File

@@ -2,17 +2,17 @@ import * as assert from 'assert';
import { just, nothing } from '../../src/misc/prelude/maybe.js';
describe('just', () => {
it('has a value', () => {
test('has a value', () => {
assert.deepStrictEqual(just(3).isJust(), true);
});
it('has the inverse called get', () => {
test('has the inverse called get', () => {
assert.deepStrictEqual(just(3).get(), 3);
});
});
describe('nothing', () => {
it('has no value', () => {
test('has no value', () => {
assert.deepStrictEqual(nothing().isJust(), false);
});
});

View File

@@ -2,7 +2,7 @@ import * as assert from 'assert';
import { query } from '../../src/misc/prelude/url.js';
describe('url', () => {
it('query', () => {
test('query', () => {
const s = query({
foo: 'ふぅ',
bar: 'b a r',

View File

@@ -27,7 +27,7 @@ describe('ActivityPub', () => {
content: 'あ',
};
it('Minimum Actor', async () => {
test('Minimum Actor', async () => {
const { MockResolver } = await import('../misc/mock-resolver.js');
const { createPerson } = await import('../../src/activitypub/models/person.js');
@@ -41,7 +41,7 @@ describe('ActivityPub', () => {
assert.deepStrictEqual(user.inbox, actor.inbox);
});
it('Minimum Note', async () => {
test('Minimum Note', async () => {
const { MockResolver } = await import('../misc/mock-resolver.js');
const { createNote } = await import('../../src/activitypub/models/note.js');
@@ -74,7 +74,7 @@ describe('ActivityPub', () => {
outbox: `${actorId}/outbox`,
};
it('Actor', async () => {
test('Actor', async () => {
const { MockResolver } = await import('../misc/mock-resolver.js');
const { createPerson } = await import('../../src/activitypub/models/person.js');

View File

@@ -19,7 +19,7 @@ export const buildParsedSignature = (signingString: string, signature: string, a
};
describe('ap-request', () => {
it('createSignedPost with verify', async () => {
test('createSignedPost with verify', async () => {
const keypair = await genRsaKeyPair();
const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
const url = 'https://example.com/inbox';
@@ -37,7 +37,7 @@ describe('ap-request', () => {
assert.deepStrictEqual(result, true);
});
it('createSignedGet with verify', async () => {
test('createSignedGet with verify', async () => {
const keypair = await genRsaKeyPair();
const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
const url = 'https://example.com/outbox';

View File

@@ -4,7 +4,7 @@ import { parse } from 'mfm-js';
import { extractMentions } from '../../src/misc/extract-mentions.js';
describe('Extract mentions', () => {
it('simple', () => {
test('simple', () => {
const ast = parse('@foo @bar @baz')!;
const mentions = extractMentions(ast);
assert.deepStrictEqual(mentions, [{
@@ -22,7 +22,7 @@ describe('Extract mentions', () => {
}]);
});
it('nested', () => {
test('nested', () => {
const ast = parse('@foo **@bar** @baz')!;
const mentions = extractMentions(ast);
assert.deepStrictEqual(mentions, [{

View File

@@ -5,13 +5,13 @@ import { toHtml } from '../../src/mfm/to-html.js';
import { fromHtml } from '../../src/mfm/from-html.js';
describe('toHtml', () => {
it('br', () => {
test('br', () => {
const input = 'foo\nbar\nbaz';
const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(toHtml(mfm.parse(input)), output);
});
it('br alt', () => {
test('br alt', () => {
const input = 'foo\r\nbar\rbaz';
const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(toHtml(mfm.parse(input)), output);
@@ -19,71 +19,71 @@ describe('toHtml', () => {
});
describe('fromHtml', () => {
it('p', () => {
test('p', () => {
assert.deepStrictEqual(fromHtml('<p>a</p><p>b</p>'), 'a\n\nb');
});
it('block element', () => {
test('block element', () => {
assert.deepStrictEqual(fromHtml('<div>a</div><div>b</div>'), 'a\nb');
});
it('inline element', () => {
test('inline element', () => {
assert.deepStrictEqual(fromHtml('<ul><li>a</li><li>b</li></ul>'), 'a\nb');
});
it('block code', () => {
test('block code', () => {
assert.deepStrictEqual(fromHtml('<pre><code>a\nb</code></pre>'), '```\na\nb\n```');
});
it('inline code', () => {
test('inline code', () => {
assert.deepStrictEqual(fromHtml('<code>a</code>'), '`a`');
});
it('quote', () => {
test('quote', () => {
assert.deepStrictEqual(fromHtml('<blockquote>a\nb</blockquote>'), '> a\n> b');
});
it('br', () => {
test('br', () => {
assert.deepStrictEqual(fromHtml('<p>abc<br><br/>d</p>'), 'abc\n\nd');
});
it('link with different text', () => {
test('link with different text', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b">c</a> d</p>'), 'a [c](https://example.com/b) d');
});
it('link with different text, but not encoded', () => {
test('link with different text, but not encoded', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/ä">c</a> d</p>'), 'a [c](<https://example.com/ä>) d');
});
it('link with same text', () => {
test('link with same text', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b">https://example.com/b</a> d</p>'), 'a https://example.com/b d');
});
it('link with same text, but not encoded', () => {
test('link with same text, but not encoded', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/ä">https://example.com/ä</a> d</p>'), 'a <https://example.com/ä> d');
});
it('link with no url', () => {
test('link with no url', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="b">c</a> d</p>'), 'a [c](b) d');
});
it('link without href', () => {
test('link without href', () => {
assert.deepStrictEqual(fromHtml('<p>a <a>c</a> d</p>'), 'a c d');
});
it('link without text', () => {
test('link without text', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b"></a> d</p>'), 'a https://example.com/b d');
});
it('link without both', () => {
test('link without both', () => {
assert.deepStrictEqual(fromHtml('<p>a <a></a> d</p>'), 'a d');
});
it('mention', () => {
test('mention', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/@user" class="u-url mention">@user</a> d</p>'), 'a @user@example.com d');
});
it('hashtag', () => {
test('hashtag', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/tags/a">#a</a> d</p>', ['#a']), 'a #a d');
});
});

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