Merge remote-branch 'upstream/master'

This commit is contained in:
NoriDev 2023-02-04 00:50:37 +09:00
commit f6b5b0b5d9
101 changed files with 2680 additions and 3926 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

View file

@ -9,6 +9,15 @@
You should also include the user name that made the change.
-->
## 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としてコピーしてから使用してください。

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

@ -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,14 +918,320 @@ 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: "Configurando mis espacio"
description: "Publicar 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"
@ -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

@ -529,8 +529,8 @@ state: "Стан"
sort: "Сортування"
ascendingOrder: "За зростанням"
descendingOrder: "За спаданням"
scratchpad: "Чернетка"
scratchpadDescription: "Scratchpad надає середовище для експериментів з AiScript. Ви можете писати, виконувати його і тестувати взаємодію з CherryPick."
scratchpad: "Scratchpad"
scratchpadDescription: "Scratchpad надає середовище для експериментів з AiScript. Ви можете писати, виконувати його і тестувати взаємодію з Misskey."
output: "Вихід"
script: "Скрипт"
disablePagesScript: "Вимкнути AiScript на Сторінках"
@ -1084,22 +1084,32 @@ _achievements:
description: "Перевищити швидкість домашньої стрічки 20npm (нотаток на хвилину)"
_viewInstanceChart:
title: "Аналітик"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
description: "Вивести \"hello world\" у Скретчпаді"
_clickedClickHere:
title: "Натисніть тут"
description: "Натиснуто тут"
_justPlainLucky:
title: "Просто вдача"
description: "Можна отримати з ймовірністю 0,01% кожні 10 секунд"
_setNameToSyuilo:
title: "Комплекс бога"
description: "Встановлено ім'я \"syuilo\""
_passedSinceAccountCreated1:
title: "Перша річниця"
description: "Минув рік з моменту створення акаунта"
_passedSinceAccountCreated2:
title: "Друга річниця"
description: "Минуло 2 роки з моменту створення акаунта"
_passedSinceAccountCreated3:
title: "Третя річниця"
description: "Минуло 3 роки з моменту створення акаунта"
_loggedInOnBirthday:
title: "З Днем народження!"
description: "Увійти у свій день народження"
_loggedInOnNewYearsDay:
title: "З Новим роком!"
description: "Увійшли в перший день року"
_brainDiver:
title: "Brain Diver"

View file

@ -1083,7 +1083,7 @@ _achievements:
title: "排列成行"
description: "关注者超过300人"
_followers500:
title: "风向标"
title: "信号塔"
description: "关注者超过500人"
_collectAchievements30:
title: "成就收藏家"
@ -1104,7 +1104,7 @@ _achievements:
title: "无话可说"
description: "发帖后一分钟内就将其删除"
_postedAtLateNight:
title: "夜行者"
title: "夜猫子"
description: "深夜发布帖子"
flavor: "差不多该去睡了喔。"
_postedAt0min0sec:
@ -1114,6 +1114,12 @@ _achievements:
_selfQuote:
title: "自我提及"
description: "引用了自己的帖子"
_htl20npm:
title: "流动的时间线"
description: "在首页时间线的流速超过20npm"
_viewInstanceChart:
title: "分析师"
description: "查看了实例信息中的图表"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
_open3windows:
@ -1129,7 +1135,7 @@ _achievements:
description: "点了这里"
_justPlainLucky:
title: "超高校级的幸运"
description: "每10秒有0.01的概率获得"
description: "每10秒有0.01的概率自动获得"
_setNameToSyuilo:
title: "像神一样呐"
description: "将名称设定为syuilo"

View file

@ -1,6 +1,6 @@
{
"name": "cherrypick",
"version": "13.2.6",
"version": "13.3.0",
"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": {
"@vitalets/google-translate-api": "8.0.0",
@ -50,19 +50,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;
@ -113,7 +108,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' } };

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

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

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

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

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

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

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

@ -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: 32,
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

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

@ -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,
@ -227,30 +215,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,
@ -393,9 +357,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,
translatorAvailable: instance.translatorType != null,
@ -415,12 +376,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

@ -69,15 +69,6 @@ export const paramDef = {
translatorType: { 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' },
@ -271,42 +262,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,
@ -225,18 +213,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,11 +301,6 @@ 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,
@ -359,9 +330,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,49 +90,14 @@ 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 },
disableRightClick: { 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: [
{
@ -144,21 +109,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

@ -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}と連携しているCherryPickアカウントはありませんでした...`);
}
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} を、CherryPick: @${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}と連携しているCherryPickアカウントはありませんでした...`);
}
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} を、CherryPick: @${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}と連携しているCherryPickアカウントはありませんでした...`);
}
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} を、CherryPick: @${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,26 @@ 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']; };
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 +205,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

@ -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/kokonect-link/cherrypick/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/kokonect-link/cherrypick/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');
});
});

View file

@ -4,79 +4,79 @@ import * as assert from 'assert';
import { toDbReaction } from '../src/misc/reaction-lib.js';
describe('toDbReaction', async () => {
it('既存の文字列リアクションはそのまま', async () => {
test('既存の文字列リアクションはそのまま', async () => {
assert.strictEqual(await toDbReaction('like'), 'like');
});
it('Unicodeプリンは寿司化不能とするため文字列化しない', async () => {
test('Unicodeプリンは寿司化不能とするため文字列化しない', async () => {
assert.strictEqual(await toDbReaction('🍮'), '🍮');
});
it('プリン以外の既存のリアクションは文字列化する like', async () => {
test('プリン以外の既存のリアクションは文字列化する like', async () => {
assert.strictEqual(await toDbReaction('👍'), 'like');
});
it('プリン以外の既存のリアクションは文字列化する love', async () => {
test('プリン以外の既存のリアクションは文字列化する love', async () => {
assert.strictEqual(await toDbReaction('❤️'), 'love');
});
it('プリン以外の既存のリアクションは文字列化する love 異体字セレクタなし', async () => {
test('プリン以外の既存のリアクションは文字列化する love 異体字セレクタなし', async () => {
assert.strictEqual(await toDbReaction('❤'), 'love');
});
it('プリン以外の既存のリアクションは文字列化する laugh', async () => {
test('プリン以外の既存のリアクションは文字列化する laugh', async () => {
assert.strictEqual(await toDbReaction('😆'), 'laugh');
});
it('プリン以外の既存のリアクションは文字列化する hmm', async () => {
test('プリン以外の既存のリアクションは文字列化する hmm', async () => {
assert.strictEqual(await toDbReaction('🤔'), 'hmm');
});
it('プリン以外の既存のリアクションは文字列化する surprise', async () => {
test('プリン以外の既存のリアクションは文字列化する surprise', async () => {
assert.strictEqual(await toDbReaction('😮'), 'surprise');
});
it('プリン以外の既存のリアクションは文字列化する congrats', async () => {
test('プリン以外の既存のリアクションは文字列化する congrats', async () => {
assert.strictEqual(await toDbReaction('🎉'), 'congrats');
});
it('プリン以外の既存のリアクションは文字列化する angry', async () => {
test('プリン以外の既存のリアクションは文字列化する angry', async () => {
assert.strictEqual(await toDbReaction('💢'), 'angry');
});
it('プリン以外の既存のリアクションは文字列化する confused', async () => {
test('プリン以外の既存のリアクションは文字列化する confused', async () => {
assert.strictEqual(await toDbReaction('😥'), 'confused');
});
it('プリン以外の既存のリアクションは文字列化する rip', async () => {
test('プリン以外の既存のリアクションは文字列化する rip', async () => {
assert.strictEqual(await toDbReaction('😇'), 'rip');
});
it('それ以外はUnicodeのまま', async () => {
test('それ以外はUnicodeのまま', async () => {
assert.strictEqual(await toDbReaction('🍅'), '🍅');
});
it('異体字セレクタ除去', async () => {
test('異体字セレクタ除去', async () => {
assert.strictEqual(await toDbReaction('㊗️'), '㊗');
});
it('異体字セレクタ除去 必要なし', async () => {
test('異体字セレクタ除去 必要なし', async () => {
assert.strictEqual(await toDbReaction('㊗'), '㊗');
});
it('fallback - undefined', async () => {
test('fallback - undefined', async () => {
assert.strictEqual(await toDbReaction(undefined), 'like');
});
it('fallback - null', async () => {
test('fallback - null', async () => {
assert.strictEqual(await toDbReaction(null), 'like');
});
it('fallback - empty', async () => {
test('fallback - empty', async () => {
assert.strictEqual(await toDbReaction(''), 'like');
});
it('fallback - unknown', async () => {
test('fallback - unknown', async () => {
assert.strictEqual(await toDbReaction('unknown'), 'like');
});
});

View file

@ -54,7 +54,7 @@ describe('FileInfoService', () => {
await app.close();
});
it('Empty file', async () => {
test('Empty file', async () => {
const path = `${resources}/emptyfile`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
delete info.warnings;
@ -74,7 +74,7 @@ describe('FileInfoService', () => {
});
});
it('Generic JPEG', async () => {
test('Generic JPEG', async () => {
const path = `${resources}/Lenna.jpg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
delete info.warnings;
@ -94,7 +94,7 @@ describe('FileInfoService', () => {
});
});
it('Generic APNG', async () => {
test('Generic APNG', async () => {
const path = `${resources}/anime.png`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
delete info.warnings;
@ -114,7 +114,7 @@ describe('FileInfoService', () => {
});
});
it('Generic AGIF', async () => {
test('Generic AGIF', async () => {
const path = `${resources}/anime.gif`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
delete info.warnings;
@ -134,7 +134,7 @@ describe('FileInfoService', () => {
});
});
it('PNG with alpha', async () => {
test('PNG with alpha', async () => {
const path = `${resources}/with-alpha.png`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
delete info.warnings;
@ -154,7 +154,7 @@ describe('FileInfoService', () => {
});
});
it('Generic SVG', async () => {
test('Generic SVG', async () => {
const path = `${resources}/image.svg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
delete info.warnings;
@ -174,7 +174,7 @@ describe('FileInfoService', () => {
});
});
it('SVG with XML definition', async () => {
test('SVG with XML definition', async () => {
// https://github.com/misskey-dev/misskey/issues/4413
const path = `${resources}/with-xml-def.svg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
@ -195,7 +195,7 @@ describe('FileInfoService', () => {
});
});
it('Dimension limit', async () => {
test('Dimension limit', async () => {
const path = `${resources}/25000x25000.png`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
delete info.warnings;
@ -215,7 +215,7 @@ describe('FileInfoService', () => {
});
});
it('Rotate JPEG', async () => {
test('Rotate JPEG', async () => {
const path = `${resources}/rotate.jpg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
delete info.warnings;

View file

@ -35,7 +35,7 @@ describe('MetaService', () => {
await app.close();
});
it('fetch (cache)', async () => {
test('fetch (cache)', async () => {
const db = app.get<DataSource>(DI.db);
const spy = jest.spyOn(db, 'transaction');
@ -45,7 +45,7 @@ describe('MetaService', () => {
expect(spy).toHaveBeenCalledTimes(0);
});
it('fetch (force)', async () => {
test('fetch (force)', async () => {
const db = app.get<DataSource>(DI.db);
const spy = jest.spyOn(db, 'transaction');

View file

@ -57,7 +57,7 @@ describe('RelayService', () => {
await app.close();
});
it('addRelay', async () => {
test('addRelay', async () => {
const result = await relayService.addRelay('https://example.com');
expect(result.inbox).toBe('https://example.com');
@ -68,7 +68,7 @@ describe('RelayService', () => {
//expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor');
});
it('listRelay', async () => {
test('listRelay', async () => {
const result = await relayService.listRelay();
expect(result.length).toBe(1);
@ -76,7 +76,7 @@ describe('RelayService', () => {
expect(result[0].status).toBe('requesting');
});
it('removeRelay: succ', async () => {
test('removeRelay: succ', async () => {
await relayService.removeRelay('https://example.com');
expect(queueService.deliver).toHaveBeenCalled();
@ -89,7 +89,7 @@ describe('RelayService', () => {
expect(list.length).toBe(0);
});
it('removeRelay: fail', async () => {
test('removeRelay: fail', async () => {
await expect(relayService.removeRelay('https://x.example.com'))
.rejects.toThrow('relay not found');
});

View file

@ -0,0 +1,232 @@
process.env.NODE_ENV = 'test';
import { jest } from '@jest/globals';
import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing';
import { DataSource } from 'typeorm';
import rndstr from 'rndstr';
import { GlobalModule } from '@/GlobalModule.js';
import { RoleService } from '@/core/RoleService.js';
import type { Role, RolesRepository, RoleAssignmentsRepository, UsersRepository, User } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { CoreModule } from '@/core/CoreModule.js';
import { MetaService } from '@/core/MetaService.js';
import { genAid } from '@/misc/id/aid.js';
import { UserCacheService } from '@/core/UserCacheService.js';
import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock';
const moduleMocker = new ModuleMocker(global);
describe('RoleService', () => {
let app: TestingModule;
let roleService: RoleService;
let usersRepository: UsersRepository;
let rolesRepository: RolesRepository;
let roleAssignmentsRepository: RoleAssignmentsRepository;
let metaService: jest.Mocked<MetaService>;
function createUser(data: Partial<User> = {}) {
const un = rndstr('a-z0-9', 16);
return usersRepository.insert({
id: genAid(new Date()),
createdAt: new Date(),
username: un,
usernameLower: un,
...data,
})
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
}
function createRole(data: Partial<Role> = {}) {
return rolesRepository.insert({
id: genAid(new Date()),
createdAt: new Date(),
updatedAt: new Date(),
lastUsedAt: new Date(),
description: '',
...data,
})
.then(x => rolesRepository.findOneByOrFail(x.identifiers[0]));
}
async function assign(roleId: Role['id'], userId: User['id']) {
await roleAssignmentsRepository.insert({
id: genAid(new Date()),
createdAt: new Date(),
roleId,
userId,
});
}
beforeEach(async () => {
app = await Test.createTestingModule({
imports: [
GlobalModule,
],
providers: [
RoleService,
UserCacheService,
],
})
.useMocker((token) => {
if (token === MetaService) {
return { fetch: jest.fn() };
}
if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
})
.compile();
app.enableShutdownHooks();
roleService = app.get<RoleService>(RoleService);
usersRepository = app.get<UsersRepository>(DI.usersRepository);
rolesRepository = app.get<RolesRepository>(DI.rolesRepository);
roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>;
});
afterEach(async () => {
await Promise.all([
app.get(DI.metasRepository).delete({}),
usersRepository.delete({}),
rolesRepository.delete({}),
roleAssignmentsRepository.delete({}),
]);
await app.close();
});
describe('getUserPolicies', () => {
test('instance default policies', async () => {
const user = await createUser();
metaService.fetch.mockResolvedValue({
policies: {
canManageCustomEmojis: false,
},
} as any);
const result = await roleService.getUserPolicies(user.id);
expect(result.canManageCustomEmojis).toBe(false);
});
test('instance default policies 2', async () => {
const user = await createUser();
metaService.fetch.mockResolvedValue({
policies: {
canManageCustomEmojis: true,
},
} as any);
const result = await roleService.getUserPolicies(user.id);
expect(result.canManageCustomEmojis).toBe(true);
});
test('with role', async () => {
const user = await createUser();
const role = await createRole({
name: 'a',
policies: {
canManageCustomEmojis: {
useDefault: false,
priority: 0,
value: true,
},
},
});
await assign(role.id, user.id);
metaService.fetch.mockResolvedValue({
policies: {
canManageCustomEmojis: false,
},
} as any);
const result = await roleService.getUserPolicies(user.id);
expect(result.canManageCustomEmojis).toBe(true);
});
test('priority', async () => {
const user = await createUser();
const role1 = await createRole({
name: 'role1',
policies: {
driveCapacityMb: {
useDefault: false,
priority: 0,
value: 200,
},
},
});
const role2 = await createRole({
name: 'role2',
policies: {
driveCapacityMb: {
useDefault: false,
priority: 1,
value: 100,
},
},
});
await assign(role1.id, user.id);
await assign(role2.id, user.id);
metaService.fetch.mockResolvedValue({
policies: {
driveCapacityMb: 50,
},
} as any);
const result = await roleService.getUserPolicies(user.id);
expect(result.driveCapacityMb).toBe(100);
});
test('conditional role', async () => {
const user1 = await createUser({
createdAt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 365)),
});
const user2 = await createUser({
createdAt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 365)),
followersCount: 10,
});
const role = await createRole({
name: 'a',
policies: {
canManageCustomEmojis: {
useDefault: false,
priority: 0,
value: true,
},
},
target: 'conditional',
condFormula: {
type: 'and',
values: [{
type: 'followersMoreThanOrEq',
value: 10,
}, {
type: 'createdMoreThan',
sec: 60 * 60 * 24 * 7,
}],
},
});
metaService.fetch.mockResolvedValue({
policies: {
canManageCustomEmojis: false,
},
} as any);
const user1Policies = await roleService.getUserPolicies(user1.id);
const user2Policies = await roleService.getUserPolicies(user2.id);
expect(user1Policies.canManageCustomEmojis).toBe(false);
expect(user2Policies.canManageCustomEmojis).toBe(true);
});
});
});

View file

@ -78,7 +78,7 @@ describe('Chart', () => {
if (db) await db.destroy();
});
it('Can updates', async () => {
test('Can updates', async () => {
await testChart.increment();
await testChart.save();
@ -102,7 +102,7 @@ describe('Chart', () => {
});
});
it('Can updates (dec)', async () => {
test('Can updates (dec)', async () => {
await testChart.decrement();
await testChart.save();
@ -126,7 +126,7 @@ describe('Chart', () => {
});
});
it('Empty chart', async () => {
test('Empty chart', async () => {
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
@ -147,7 +147,7 @@ describe('Chart', () => {
});
});
it('Can updates at multiple times at same time', async () => {
test('Can updates at multiple times at same time', async () => {
await testChart.increment();
await testChart.increment();
await testChart.increment();
@ -173,7 +173,7 @@ describe('Chart', () => {
});
});
it('複数回saveされてもデータの更新は一度だけ', async () => {
test('複数回saveされてもデータの更新は一度だけ', async () => {
await testChart.increment();
await testChart.save();
await testChart.save();
@ -199,7 +199,7 @@ describe('Chart', () => {
});
});
it('Can updates at different times', async () => {
test('Can updates at different times', async () => {
await testChart.increment();
await testChart.save();
@ -230,7 +230,7 @@ describe('Chart', () => {
// 仕様上はこうなってほしいけど、実装は難しそうなのでskip
/*
it('Can updates at different times without save', async () => {
test('Can updates at different times without save', async () => {
await testChart.increment();
clock.tick('01:00:00');
@ -259,7 +259,7 @@ describe('Chart', () => {
});
*/
it('Can padding', async () => {
test('Can padding', async () => {
await testChart.increment();
await testChart.save();
@ -289,7 +289,7 @@ describe('Chart', () => {
});
// 要求された範囲にログがひとつもない場合でもパディングできる
it('Can padding from past range', async () => {
test('Can padding from past range', async () => {
await testChart.increment();
await testChart.save();
@ -317,7 +317,7 @@ describe('Chart', () => {
// 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる
// Issue #3190
it('Can padding from past range 2', async () => {
test('Can padding from past range 2', async () => {
await testChart.increment();
await testChart.save();
@ -346,7 +346,7 @@ describe('Chart', () => {
});
});
it('Can specify offset', async () => {
test('Can specify offset', async () => {
await testChart.increment();
await testChart.save();
@ -375,7 +375,7 @@ describe('Chart', () => {
});
});
it('Can specify offset (floor time)', async () => {
test('Can specify offset (floor time)', async () => {
clock.tick('00:30:00');
await testChart.increment();
@ -407,7 +407,7 @@ describe('Chart', () => {
});
describe('Grouped', () => {
it('Can updates', async () => {
test('Can updates', async () => {
await testGroupedChart.increment('alice');
await testGroupedChart.save();
@ -451,7 +451,7 @@ describe('Chart', () => {
});
describe('Unique increment', () => {
it('Can updates', async () => {
test('Can updates', async () => {
await testUniqueChart.uniqueIncrement('alice');
await testUniqueChart.uniqueIncrement('alice');
await testUniqueChart.uniqueIncrement('bob');
@ -470,7 +470,7 @@ describe('Chart', () => {
});
describe('Intersection', () => {
it('条件が満たされていない場合はカウントされない', async () => {
test('条件が満たされていない場合はカウントされない', async () => {
await testIntersectionChart.addA('alice');
await testIntersectionChart.addA('bob');
await testIntersectionChart.addB('carol');
@ -492,7 +492,7 @@ describe('Chart', () => {
});
});
it('条件が満たされている場合にカウントされる', async () => {
test('条件が満たされている場合にカウントされる', async () => {
await testIntersectionChart.addA('alice');
await testIntersectionChart.addA('bob');
await testIntersectionChart.addB('carol');
@ -518,7 +518,7 @@ describe('Chart', () => {
});
describe('Resync', () => {
it('Can resync', async () => {
test('Can resync', async () => {
testChart.total = 1;
await testChart.resync();
@ -543,7 +543,7 @@ describe('Chart', () => {
});
});
it('Can resync (2)', async () => {
test('Can resync (2)', async () => {
await testChart.increment();
await testChart.save();

View file

@ -12,18 +12,18 @@
"@rollup/plugin-json": "6.0.0",
"@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.12.4",
"@tabler/icons-webfont": "^2.1.2",
"@tabler/icons-webfont": "2.1.2",
"@vitejs/plugin-vue": "4.0.0",
"@vue/compiler-sfc": "3.2.45",
"@vue/compiler-sfc": "3.2.47",
"autobind-decorator": "2.4.0",
"autosize": "5.0.2",
"blurhash": "2.0.4",
"broadcast-channel": "4.20.2",
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"canvas-confetti": "^1.6.0",
"canvas-confetti": "1.6.0",
"chart.js": "4.2.0",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "^1.3.0",
"chartjs-chart-matrix": "1.3.0",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.0",
"compare-versions": "5.0.1",
@ -31,23 +31,23 @@
"date-fns": "2.29.3",
"escape-regexp": "0.0.1",
"eventemitter3": "5.0.0",
"gsap": "^3.11.4",
"gsap": "3.11.4",
"idb-keyval": "6.2.0",
"insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2",
"json5": "2.2.3",
"matter-js": "0.18.0",
"mfm-js": "0.23.3",
"misskey-js": "0.0.14",
"photoswipe": "5.3.4",
"misskey-js": "0.0.15",
"photoswipe": "5.3.5",
"prismjs": "1.29.0",
"punycode": "2.3.0",
"querystring": "0.2.1",
"rndstr": "1.0.0",
"rollup": "3.11.0",
"rollup": "3.12.1",
"s-age": "1.1.2",
"sanitize-html": "^2.8.1",
"sass": "1.57.1",
"sanitize-html": "2.9.0",
"sass": "1.58.0",
"seedrandom": "3.0.5",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
@ -59,11 +59,12 @@
"tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0",
"typescript": "4.9.4",
"typescript": "4.9.5",
"uuid": "9.0.0",
"vanilla-tilt": "1.8.0",
"vite": "4.0.4",
"vue": "3.2.45",
"vue-plyr": "7.0.0",
"vite": "4.1.1",
"vue": "3.2.47",
"vue-prism-editor": "2.0.0-alpha.2",
"vuedraggable": "next"
},
@ -73,25 +74,25 @@
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@types/matter-js": "0.18.2",
"@types/node": "^18.11.18",
"@types/node": "18.11.18",
"@types/punycode": "2.1.0",
"@types/sanitize-html": "^2.8.0",
"@types/sanitize-html": "2.8.0",
"@types/seedrandom": "3.0.4",
"@types/throttle-debounce": "5.0.0",
"@types/tinycolor2": "1.4.3",
"@types/uuid": "9.0.0",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.49.0",
"@typescript-eslint/parser": "5.49.0",
"@vue/runtime-core": "3.2.45",
"@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.50.0",
"@vue/runtime-core": "3.2.47",
"cross-env": "7.0.3",
"cypress": "12.4.0",
"eslint": "8.32.0",
"cypress": "12.5.1",
"eslint": "8.33.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-vue": "9.9.0",
"start-server-and-test": "1.15.3",
"vue-eslint-parser": "^9.1.0",
"vue-tsc": "^1.0.24"
"vue-eslint-parser": "9.1.0",
"vue-tsc": "1.0.24"
}
}

View file

@ -6,19 +6,20 @@
</div>
</div>
<div v-else class="kkjnbbplepmiyuadieoenjgutgcmtsvu">
<video
:poster="video.thumbnailUrl"
:title="video.comment"
:alt="video.comment"
preload="none"
controls
@contextmenu.stop
>
<source
:src="video.url"
:type="video.type"
<vue-plyr>
<video
controls
crossorigin
playsinline
:data-poster="video.thumbnailUrl"
>
</video>
<source
size="720"
:src="video.url"
:type="video.type"
/>
</video>
</vue-plyr>
<i class="ti ti-eye-off" @click="hide = true"></i>
</div>
</template>
@ -26,7 +27,9 @@
<script lang="ts" setup>
import { ref } from 'vue';
import * as misskey from 'misskey-js';
import VuePlyr from 'vue-plyr';
import { defaultStore } from '@/store';
import 'vue-plyr/dist/vue-plyr.css';
const props = defineProps<{
video: misskey.entities.DriveFile;
@ -39,6 +42,8 @@ const hide = ref((defaultStore.state.nsfw === 'force') ? true : props.video.isSe
.kkjnbbplepmiyuadieoenjgutgcmtsvu {
position: relative;
--plyr-color-main: var(--accent);
> i {
display: block;
position: absolute;

View file

@ -789,7 +789,7 @@ defineExpose({
&.modal {
width: 100%;
max-width: 520px;
max-width: 800px;
}
}

View file

@ -40,11 +40,6 @@
</div>
</div>
</div>
<div class="social">
<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/twitter`"><i class="ti ti-brand-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/github`"><i class="ti ti-brand-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/discord`"><i class="ti ti-brand-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a>
</div>
</form>
</template>

View file

@ -11,7 +11,6 @@
<div>Ads by {{ host }}</div>
<!--<MkButton class="button" primary>{{ $ts._ad.like }}</MkButton>-->
<MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ $ts._ad.reduceFrequencyOfThisAd }}</MkButton>
<MkButton v-if="$i && $i.policies.canHideAds" :class="$style.menuButton" @click="hide">{{ $ts._ad.hide }}</MkButton>
<button class="_textButton" @click="toggleMenu">{{ $ts._ad.back }}</button>
</div>
</div>
@ -83,7 +82,7 @@ const choseAd = (): Ad | null => {
};
const chosen = ref(choseAd());
let shouldHide = $ref(chosen.value && $i && $i.policies.canHideAds && defaultStore.state.hiddenAds.includes(chosen.value.id));
const shouldHide = $ref($i && $i.policies.canHideAds);
function reduceFrequency(): void {
if (chosen.value == null) return;
@ -93,13 +92,6 @@ function reduceFrequency(): void {
chosen.value = choseAd();
showMenu.value = false;
}
function hide() {
if (chosen.value == null) return;
defaultStore.push('hiddenAds', chosen.value.id);
os.success();
shouldHide = true;
}
</script>
<style lang="scss" module>

View file

@ -43,505 +43,508 @@ import { reloadChannel } from '@/scripts/unison-reload';
import { reactionPicker } from '@/scripts/reaction-picker';
import { getUrlWithoutLoginId } from '@/scripts/login-id';
import { getAccountFromId } from '@/scripts/get-account-from-id';
import { deckStore } from './ui/deck/deck-store';
import { miLocalStorage } from './local-storage';
import { claimAchievement, claimedAchievements } from './scripts/achievements';
import { fetchCustomEmojis } from './custom-emojis';
(async () => {
console.info(`CherryPick v${version}`);
console.info(`CherryPick v${version}`);
if (_DEV_) {
console.warn('Development mode!!!');
if (_DEV_) {
console.warn('Development mode!!!');
console.info(`vue ${vueVersion}`);
console.info(`vue ${vueVersion}`);
(window as any).$i = $i;
(window as any).$store = defaultStore;
(window as any).$i = $i;
(window as any).$store = defaultStore;
window.addEventListener('error', event => {
console.error(event);
/*
alert({
type: 'error',
title: 'DEV: Unhandled error',
text: event.message
});
*/
window.addEventListener('error', event => {
console.error(event);
/*
alert({
type: 'error',
title: 'DEV: Unhandled error',
text: event.message
});
window.addEventListener('unhandledrejection', event => {
console.error(event);
/*
alert({
type: 'error',
title: 'DEV: Unhandled promise rejection',
text: event.reason
});
*/
});
}
//#region Detect language & fetch translations
const localeVersion = miLocalStorage.getItem('localeVersion');
const localeOutdated = (localeVersion == null || localeVersion !== version);
if (localeOutdated) {
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
if (res.status === 200) {
const newLocale = await res.text();
const parsedNewLocale = JSON.parse(newLocale);
miLocalStorage.setItem('locale', newLocale);
miLocalStorage.setItem('localeVersion', version);
updateLocale(parsedNewLocale);
updateI18n(parsedNewLocale);
}
}
//#endregion
// タッチデバイスでCSSの:hoverを機能させる
document.addEventListener('touchend', () => {}, { passive: true });
// 一斉リロード
reloadChannel.addEventListener('message', path => {
if (path !== null) location.href = path;
else location.reload();
*/
});
// If mobile, insert the viewport meta tag
if (['smartphone', 'tablet'].includes(deviceKind)) {
const viewport = document.getElementsByName('viewport').item(0);
viewport.setAttribute('content',
`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
window.addEventListener('unhandledrejection', event => {
console.error(event);
/*
alert({
type: 'error',
title: 'DEV: Unhandled promise rejection',
text: event.reason
});
*/
});
}
//#region Detect language & fetch translations
const localeVersion = miLocalStorage.getItem('localeVersion');
const localeOutdated = (localeVersion == null || localeVersion !== version);
if (localeOutdated) {
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
if (res.status === 200) {
const newLocale = await res.text();
const parsedNewLocale = JSON.parse(newLocale);
miLocalStorage.setItem('locale', newLocale);
miLocalStorage.setItem('localeVersion', version);
updateLocale(parsedNewLocale);
updateI18n(parsedNewLocale);
}
}
//#endregion
//#region Set lang attr
const html = document.documentElement;
html.setAttribute('lang', lang);
//#endregion
// タッチデバイスでCSSの:hoverを機能させる
document.addEventListener('touchend', () => {}, { passive: true });
//#region loginId
const params = new URLSearchParams(location.search);
const loginId = params.get('loginId');
// 一斉リロード
reloadChannel.addEventListener('message', path => {
if (path !== null) location.href = path;
else location.reload();
});
if (loginId) {
const target = getUrlWithoutLoginId(location.href);
// If mobile, insert the viewport meta tag
if (['smartphone', 'tablet'].includes(deviceKind)) {
const viewport = document.getElementsByName('viewport').item(0);
viewport.setAttribute('content',
`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
}
if (!$i || $i.id !== loginId) {
const account = await getAccountFromId(loginId);
if (account) {
await login(account.token, target);
}
//#region Set lang attr
const html = document.documentElement;
html.setAttribute('lang', lang);
//#endregion
//#region loginId
const params = new URLSearchParams(location.search);
const loginId = params.get('loginId');
if (loginId) {
const target = getUrlWithoutLoginId(location.href);
if (!$i || $i.id !== loginId) {
const account = await getAccountFromId(loginId);
if (account) {
await login(account.token, target);
}
history.replaceState({ misskey: 'loginId' }, '', target);
}
//#endregion
history.replaceState({ misskey: 'loginId' }, '', target);
}
//#region Fetch user
if ($i && $i.token) {
//#endregion
//#region Fetch user
if ($i && $i.token) {
if (_DEV_) {
console.log('account cache found. refreshing...');
}
refreshAccount();
} else {
if (_DEV_) {
console.log('no account cache found.');
}
// 連携ログインの場合用にCookieを参照する
const i = (document.cookie.match(/igi=(\w+)/) ?? [null, null])[1];
if (i != null && i !== 'null') {
if (_DEV_) {
console.log('account cache found. refreshing...');
console.log('signing...');
}
refreshAccount();
try {
document.body.innerHTML = '<div>Please wait...</div>';
await login(i);
} catch (err) {
// Render the error screen
// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
document.body.innerHTML = '<div id="err">Oops!</div>';
}
} else {
if (_DEV_) {
console.log('no account cache found.');
}
// 連携ログインの場合用にCookieを参照する
const i = (document.cookie.match(/igi=(\w+)/) ?? [null, null])[1];
if (i != null && i !== 'null') {
if (_DEV_) {
console.log('signing...');
}
try {
document.body.innerHTML = '<div>Please wait...</div>';
await login(i);
} catch (err) {
// Render the error screen
// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
document.body.innerHTML = '<div id="err">Oops!</div>';
}
} else {
if (_DEV_) {
console.log('not signed in');
}
console.log('not signed in');
}
}
//#endregion
}
//#endregion
const fetchInstanceMetaPromise = fetchInstance();
const fetchInstanceMetaPromise = fetchInstance();
fetchInstanceMetaPromise.then(() => {
miLocalStorage.setItem('v', instance.version);
fetchInstanceMetaPromise.then(() => {
miLocalStorage.setItem('v', instance.version);
// Init service worker
initializeSw();
});
// Init service worker
initializeSw();
});
try {
await fetchCustomEmojis();
} catch (err) {}
try {
await fetchCustomEmojis();
} catch (err) {}
const app = createApp(
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
ui === 'default' ? defineAsyncComponent(() => import('@/ui/universal.vue')) :
defineAsyncComponent(() => import('@/ui/friendly.vue')),
);
const app = createApp(
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
ui === 'default' ? defineAsyncComponent(() => import('@/ui/universal.vue')) :
defineAsyncComponent(() => import('@/ui/friendly.vue')),
);
if (_DEV_) {
app.config.performance = true;
if (_DEV_) {
app.config.performance = true;
}
// TODO: 廃止
app.config.globalProperties = {
$i,
$store: defaultStore,
$instance: instance,
$t: i18n.t,
$ts: i18n.ts,
};
widgets(app);
directives(app);
components(app);
const splash = document.getElementById('splash');
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
if (splash) splash.addEventListener('transitionend', () => {
splash.remove();
});
await deckStore.ready;
// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
// なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する
const rootEl = (() => {
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
const currentEl = document.getElementById(MISSKEY_MOUNT_DIV_ID);
if (currentEl) {
console.warn('multiple import detected');
return currentEl;
}
// TODO: 廃止
app.config.globalProperties = {
$i,
$store: defaultStore,
$instance: instance,
$t: i18n.t,
$ts: i18n.ts,
};
widgets(app);
directives(app);
components(app);
const splash = document.getElementById('splash');
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
if (splash) splash.addEventListener('transitionend', () => {
splash.remove();
});
// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
// なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する
const rootEl = (() => {
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
const currentEl = document.getElementById(MISSKEY_MOUNT_DIV_ID);
if (currentEl) {
console.warn('multiple import detected');
return currentEl;
}
const rootEl = document.createElement('div');
rootEl.id = MISSKEY_MOUNT_DIV_ID;
document.body.appendChild(rootEl);
return rootEl;
})();
app.mount(rootEl);
// boot.jsのやつを解除
window.onerror = null;
window.onunhandledrejection = null;
reactionPicker.init();
if (splash) {
splash.style.opacity = '0';
splash.style.pointerEvents = 'none';
}
// クライアントが更新されたか?
const lastVersion = miLocalStorage.getItem('lastVersion');
if (lastVersion !== version) {
miLocalStorage.setItem('lastVersion', version);
// テーマリビルドするため
miLocalStorage.removeItem('theme');
try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
// ログインしてる場合だけ
if ($i) {
popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed');
}
}
} catch (err) {
}
}
// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
watch(defaultStore.reactiveState.darkMode, (darkMode) => {
applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
}, { immediate: miLocalStorage.getItem('theme') == null });
const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
watch(darkTheme, (theme) => {
if (defaultStore.state.darkMode) {
applyTheme(theme);
}
});
watch(lightTheme, (theme) => {
if (!defaultStore.state.darkMode) {
applyTheme(theme);
}
});
//#region Sync dark mode
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
defaultStore.set('darkMode', isDeviceDarkmode());
}
window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
defaultStore.set('darkMode', mql.matches);
}
});
//#endregion
fetchInstanceMetaPromise.then(() => {
if (defaultStore.state.themeInitial) {
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme));
defaultStore.set('themeInitial', false);
}
});
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true });
watch(defaultStore.reactiveState.useBlurEffect, v => {
if (v) {
document.documentElement.style.removeProperty('--blur');
} else {
document.documentElement.style.setProperty('--blur', 'none');
}
}, { immediate: true });
let reloadDialogShowing = false;
stream.on('_disconnected_', async () => {
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
location.reload();
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
if (reloadDialogShowing) return;
reloadDialogShowing = true;
const { canceled } = await confirm({
type: 'warning',
title: i18n.ts.disconnectedFromServer,
text: i18n.ts.reloadConfirm,
});
reloadDialogShowing = false;
if (!canceled) {
location.reload();
}
} else if (defaultStore.state.serverDisconnectedBehavior === 'none') {
// none }
}
});
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
import('./plugin').then(({ install }) => {
install(plugin);
});
}
const hotkeys = {
'd': (): void => {
defaultStore.set('darkMode', !defaultStore.state.darkMode);
},
's': search,
};
if ($i) {
// only add post shortcuts if logged in
hotkeys['p|n'] = post;
if ($i.isDeleted) {
alert({
type: 'warning',
text: i18n.ts.accountDeletionInProgress,
});
}
const now = new Date();
const m = now.getMonth() + 1;
const d = now.getDate();
if ($i.birthday) {
const bm = parseInt($i.birthday.split('-')[1]);
const bd = parseInt($i.birthday.split('-')[2]);
if (m === bm && d === bd) {
claimAchievement('loggedInOnBirthday');
}
}
if (m === 1 && d === 1) {
claimAchievement('loggedInOnNewYearsDay');
}
if ($i.loggedInDays >= 3) claimAchievement('login3');
if ($i.loggedInDays >= 7) claimAchievement('login7');
if ($i.loggedInDays >= 15) claimAchievement('login15');
if ($i.loggedInDays >= 30) claimAchievement('login30');
if ($i.loggedInDays >= 60) claimAchievement('login60');
if ($i.loggedInDays >= 100) claimAchievement('login100');
if ($i.loggedInDays >= 200) claimAchievement('login200');
if ($i.loggedInDays >= 300) claimAchievement('login300');
if ($i.loggedInDays >= 400) claimAchievement('login400');
if ($i.loggedInDays >= 500) claimAchievement('login500');
if ($i.loggedInDays >= 600) claimAchievement('login600');
if ($i.loggedInDays >= 700) claimAchievement('login700');
if ($i.loggedInDays >= 800) claimAchievement('login800');
if ($i.loggedInDays >= 900) claimAchievement('login900');
if ($i.loggedInDays >= 1000) claimAchievement('login1000');
if ($i.notesCount > 0) claimAchievement('notes1');
if ($i.notesCount >= 10) claimAchievement('notes10');
if ($i.notesCount >= 100) claimAchievement('notes100');
if ($i.notesCount >= 500) claimAchievement('notes500');
if ($i.notesCount >= 1000) claimAchievement('notes1000');
if ($i.notesCount >= 5000) claimAchievement('notes5000');
if ($i.notesCount >= 10000) claimAchievement('notes10000');
if ($i.notesCount >= 20000) claimAchievement('notes20000');
if ($i.notesCount >= 30000) claimAchievement('notes30000');
if ($i.notesCount >= 40000) claimAchievement('notes40000');
if ($i.notesCount >= 50000) claimAchievement('notes50000');
if ($i.notesCount >= 60000) claimAchievement('notes60000');
if ($i.notesCount >= 70000) claimAchievement('notes70000');
if ($i.notesCount >= 80000) claimAchievement('notes80000');
if ($i.notesCount >= 90000) claimAchievement('notes90000');
if ($i.notesCount >= 100000) claimAchievement('notes100000');
if ($i.followersCount > 0) claimAchievement('followers1');
if ($i.followersCount >= 10) claimAchievement('followers10');
if ($i.followersCount >= 50) claimAchievement('followers50');
if ($i.followersCount >= 100) claimAchievement('followers100');
if ($i.followersCount >= 300) claimAchievement('followers300');
if ($i.followersCount >= 500) claimAchievement('followers500');
if ($i.followersCount >= 1000) claimAchievement('followers1000');
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) {
claimAchievement('passedSinceAccountCreated1');
}
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) {
claimAchievement('passedSinceAccountCreated2');
}
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) {
claimAchievement('passedSinceAccountCreated3');
}
if (claimedAchievements.length >= 30) {
claimAchievement('collectAchievements30');
}
window.setInterval(() => {
if (Math.floor(Math.random() * 10000) === 0) {
claimAchievement('justPlainLucky');
}
}, 1000 * 10);
window.setTimeout(() => {
claimAchievement('client30min');
}, 1000 * 60 * 30);
const lastUsed = miLocalStorage.getItem('lastUsed');
if (lastUsed) {
const lastUsedDate = parseInt(lastUsed, 10);
// 二時間以上前なら
if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
toast(i18n.t('welcomeBackWithName', {
name: $i.name || $i.username,
}));
}
}
miLocalStorage.setItem('lastUsed', Date.now().toString());
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3)))) {
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
}
}
if ('Notification' in window) {
// 許可を得ていなかったらリクエスト
if (Notification.permission === 'default') {
Notification.requestPermission();
}
}
const main = markRaw(stream.useChannel('main', null, 'System'));
// 自分の情報が更新されたとき
main.on('meUpdated', i => {
updateAccount(i);
});
main.on('readAllNotifications', () => {
updateAccount({ hasUnreadNotification: false });
});
main.on('unreadNotification', () => {
updateAccount({ hasUnreadNotification: true });
});
main.on('unreadMention', () => {
updateAccount({ hasUnreadMentions: true });
});
main.on('readAllUnreadMentions', () => {
updateAccount({ hasUnreadMentions: false });
});
main.on('unreadSpecifiedNote', () => {
updateAccount({ hasUnreadSpecifiedNotes: true });
});
main.on('readAllUnreadSpecifiedNotes', () => {
updateAccount({ hasUnreadSpecifiedNotes: false });
});
main.on('readAllMessagingMessages', () => {
updateAccount({ hasUnreadMessagingMessage: false });
});
main.on('unreadMessagingMessage', () => {
updateAccount({ hasUnreadMessagingMessage: true });
sound.play('chatBg');
});
main.on('readAllAntennas', () => {
updateAccount({ hasUnreadAntenna: false });
});
main.on('unreadAntenna', () => {
updateAccount({ hasUnreadAntenna: true });
sound.play('antenna');
});
main.on('readAllAnnouncements', () => {
updateAccount({ hasUnreadAnnouncement: false });
});
main.on('readAllChannels', () => {
updateAccount({ hasUnreadChannel: false });
});
main.on('unreadChannel', () => {
updateAccount({ hasUnreadChannel: true });
sound.play('channel');
});
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
main.on('myTokenRegenerated', () => {
signout();
});
}
// shortcut
document.addEventListener('keydown', makeHotkey(hotkeys));
const rootEl = document.createElement('div');
rootEl.id = MISSKEY_MOUNT_DIV_ID;
document.body.appendChild(rootEl);
return rootEl;
})();
app.mount(rootEl);
// boot.jsのやつを解除
window.onerror = null;
window.onunhandledrejection = null;
reactionPicker.init();
if (splash) {
splash.style.opacity = '0';
splash.style.pointerEvents = 'none';
}
// クライアントが更新されたか?
const lastVersion = miLocalStorage.getItem('lastVersion');
if (lastVersion !== version) {
miLocalStorage.setItem('lastVersion', version);
// テーマリビルドするため
miLocalStorage.removeItem('theme');
try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
// ログインしてる場合だけ
if ($i) {
popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed');
}
}
} catch (err) {
}
}
await defaultStore.ready;
// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
watch(defaultStore.reactiveState.darkMode, (darkMode) => {
applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
}, { immediate: miLocalStorage.getItem('theme') == null });
const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
watch(darkTheme, (theme) => {
if (defaultStore.state.darkMode) {
applyTheme(theme);
}
});
watch(lightTheme, (theme) => {
if (!defaultStore.state.darkMode) {
applyTheme(theme);
}
});
//#region Sync dark mode
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
defaultStore.set('darkMode', isDeviceDarkmode());
}
window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
defaultStore.set('darkMode', mql.matches);
}
});
//#endregion
fetchInstanceMetaPromise.then(() => {
if (defaultStore.state.themeInitial) {
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme));
defaultStore.set('themeInitial', false);
}
});
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true });
watch(defaultStore.reactiveState.useBlurEffect, v => {
if (v) {
document.documentElement.style.removeProperty('--blur');
} else {
document.documentElement.style.setProperty('--blur', 'none');
}
}, { immediate: true });
let reloadDialogShowing = false;
stream.on('_disconnected_', async () => {
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
location.reload();
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
if (reloadDialogShowing) return;
reloadDialogShowing = true;
const { canceled } = await confirm({
type: 'warning',
title: i18n.ts.disconnectedFromServer,
text: i18n.ts.reloadConfirm,
});
reloadDialogShowing = false;
if (!canceled) {
location.reload();
}
} else if (defaultStore.state.serverDisconnectedBehavior === 'none') {
// none }
}
});
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
import('./plugin').then(({ install }) => {
install(plugin);
});
}
const hotkeys = {
'd': (): void => {
defaultStore.set('darkMode', !defaultStore.state.darkMode);
},
's': search,
};
if ($i) {
// only add post shortcuts if logged in
hotkeys['p|n'] = post;
if ($i.isDeleted) {
alert({
type: 'warning',
text: i18n.ts.accountDeletionInProgress,
});
}
const now = new Date();
const m = now.getMonth() + 1;
const d = now.getDate();
if ($i.birthday) {
const bm = parseInt($i.birthday.split('-')[1]);
const bd = parseInt($i.birthday.split('-')[2]);
if (m === bm && d === bd) {
claimAchievement('loggedInOnBirthday');
}
}
if (m === 1 && d === 1) {
claimAchievement('loggedInOnNewYearsDay');
}
if ($i.loggedInDays >= 3) claimAchievement('login3');
if ($i.loggedInDays >= 7) claimAchievement('login7');
if ($i.loggedInDays >= 15) claimAchievement('login15');
if ($i.loggedInDays >= 30) claimAchievement('login30');
if ($i.loggedInDays >= 60) claimAchievement('login60');
if ($i.loggedInDays >= 100) claimAchievement('login100');
if ($i.loggedInDays >= 200) claimAchievement('login200');
if ($i.loggedInDays >= 300) claimAchievement('login300');
if ($i.loggedInDays >= 400) claimAchievement('login400');
if ($i.loggedInDays >= 500) claimAchievement('login500');
if ($i.loggedInDays >= 600) claimAchievement('login600');
if ($i.loggedInDays >= 700) claimAchievement('login700');
if ($i.loggedInDays >= 800) claimAchievement('login800');
if ($i.loggedInDays >= 900) claimAchievement('login900');
if ($i.loggedInDays >= 1000) claimAchievement('login1000');
if ($i.notesCount > 0) claimAchievement('notes1');
if ($i.notesCount >= 10) claimAchievement('notes10');
if ($i.notesCount >= 100) claimAchievement('notes100');
if ($i.notesCount >= 500) claimAchievement('notes500');
if ($i.notesCount >= 1000) claimAchievement('notes1000');
if ($i.notesCount >= 5000) claimAchievement('notes5000');
if ($i.notesCount >= 10000) claimAchievement('notes10000');
if ($i.notesCount >= 20000) claimAchievement('notes20000');
if ($i.notesCount >= 30000) claimAchievement('notes30000');
if ($i.notesCount >= 40000) claimAchievement('notes40000');
if ($i.notesCount >= 50000) claimAchievement('notes50000');
if ($i.notesCount >= 60000) claimAchievement('notes60000');
if ($i.notesCount >= 70000) claimAchievement('notes70000');
if ($i.notesCount >= 80000) claimAchievement('notes80000');
if ($i.notesCount >= 90000) claimAchievement('notes90000');
if ($i.notesCount >= 100000) claimAchievement('notes100000');
if ($i.followersCount > 0) claimAchievement('followers1');
if ($i.followersCount >= 10) claimAchievement('followers10');
if ($i.followersCount >= 50) claimAchievement('followers50');
if ($i.followersCount >= 100) claimAchievement('followers100');
if ($i.followersCount >= 300) claimAchievement('followers300');
if ($i.followersCount >= 500) claimAchievement('followers500');
if ($i.followersCount >= 1000) claimAchievement('followers1000');
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) {
claimAchievement('passedSinceAccountCreated1');
}
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) {
claimAchievement('passedSinceAccountCreated2');
}
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) {
claimAchievement('passedSinceAccountCreated3');
}
if (claimedAchievements.length >= 30) {
claimAchievement('collectAchievements30');
}
window.setInterval(() => {
if (Math.floor(Math.random() * 10000) === 0) {
claimAchievement('justPlainLucky');
}
}, 1000 * 10);
window.setTimeout(() => {
claimAchievement('client30min');
}, 1000 * 60 * 30);
const lastUsed = miLocalStorage.getItem('lastUsed');
if (lastUsed) {
const lastUsedDate = parseInt(lastUsed, 10);
// 二時間以上前なら
if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
toast(i18n.t('welcomeBackWithName', {
name: $i.name || $i.username,
}));
}
}
miLocalStorage.setItem('lastUsed', Date.now().toString());
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3)))) {
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
}
}
if ('Notification' in window) {
// 許可を得ていなかったらリクエスト
if (Notification.permission === 'default') {
Notification.requestPermission();
}
}
const main = markRaw(stream.useChannel('main', null, 'System'));
// 自分の情報が更新されたとき
main.on('meUpdated', i => {
updateAccount(i);
});
main.on('readAllNotifications', () => {
updateAccount({ hasUnreadNotification: false });
});
main.on('unreadNotification', () => {
updateAccount({ hasUnreadNotification: true });
});
main.on('unreadMention', () => {
updateAccount({ hasUnreadMentions: true });
});
main.on('readAllUnreadMentions', () => {
updateAccount({ hasUnreadMentions: false });
});
main.on('unreadSpecifiedNote', () => {
updateAccount({ hasUnreadSpecifiedNotes: true });
});
main.on('readAllUnreadSpecifiedNotes', () => {
updateAccount({ hasUnreadSpecifiedNotes: false });
});
main.on('readAllMessagingMessages', () => {
updateAccount({ hasUnreadMessagingMessage: false });
});
main.on('unreadMessagingMessage', () => {
updateAccount({ hasUnreadMessagingMessage: true });
sound.play('chatBg');
});
main.on('readAllAntennas', () => {
updateAccount({ hasUnreadAntenna: false });
});
main.on('unreadAntenna', () => {
updateAccount({ hasUnreadAntenna: true });
sound.play('antenna');
});
main.on('readAllAnnouncements', () => {
updateAccount({ hasUnreadAnnouncement: false });
});
main.on('readAllChannels', () => {
updateAccount({ hasUnreadChannel: false });
});
main.on('unreadChannel', () => {
updateAccount({ hasUnreadChannel: true });
sound.play('channel');
});
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
main.on('myTokenRegenerated', () => {
signout();
});
}
// shortcut
document.addEventListener('keydown', makeHotkey(hotkeys));

View file

@ -164,11 +164,6 @@ const menuDef = $computed(() => [{
text: i18n.ts.relays,
to: '/admin/relays',
active: currentPage?.route.name === 'relays',
}, {
icon: 'ti ti-share',
text: i18n.ts.integration,
to: '/admin/integrations',
active: currentPage?.route.name === 'integrations',
}, {
icon: 'ti ti-ban',
text: i18n.ts.instanceBlocking,

View file

@ -1,60 +0,0 @@
<template>
<FormSuspense :p="init">
<div class="_gaps_m">
<MkSwitch v-model="enableDiscordIntegration">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<template v-if="enableDiscordIntegration">
<FormInfo>Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo>
<MkInput v-model="discordClientId">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Client ID</template>
</MkInput>
<MkInput v-model="discordClientSecret">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Client Secret</template>
</MkInput>
</template>
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</FormSuspense>
</template>
<script lang="ts" setup>
import { } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import FormInfo from '@/components/MkInfo.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
let uri: string = $ref('');
let enableDiscordIntegration: boolean = $ref(false);
let discordClientId: string | null = $ref(null);
let discordClientSecret: string | null = $ref(null);
async function init() {
const meta = await os.api('admin/meta');
uri = meta.uri;
enableDiscordIntegration = meta.enableDiscordIntegration;
discordClientId = meta.discordClientId;
discordClientSecret = meta.discordClientSecret;
}
function save() {
os.apiWithDialog('admin/update-meta', {
enableDiscordIntegration,
discordClientId,
discordClientSecret,
}).then(() => {
fetchInstance();
});
}
</script>

View file

@ -1,60 +0,0 @@
<template>
<FormSuspense :p="init">
<div class="_gaps_m">
<MkSwitch v-model="enableGithubIntegration">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<template v-if="enableGithubIntegration">
<FormInfo>Callback URL: {{ `${uri}/api/gh/cb` }}</FormInfo>
<MkInput v-model="githubClientId">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Client ID</template>
</MkInput>
<MkInput v-model="githubClientSecret">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Client Secret</template>
</MkInput>
</template>
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</FormSuspense>
</template>
<script lang="ts" setup>
import { } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import FormInfo from '@/components/MkInfo.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
let uri: string = $ref('');
let enableGithubIntegration: boolean = $ref(false);
let githubClientId: string | null = $ref(null);
let githubClientSecret: string | null = $ref(null);
async function init() {
const meta = await os.api('admin/meta');
uri = meta.uri;
enableGithubIntegration = meta.enableGithubIntegration;
githubClientId = meta.githubClientId;
githubClientSecret = meta.githubClientSecret;
}
function save() {
os.apiWithDialog('admin/update-meta', {
enableGithubIntegration,
githubClientId,
githubClientSecret,
}).then(() => {
fetchInstance();
});
}
</script>

View file

@ -1,60 +0,0 @@
<template>
<FormSuspense :p="init">
<div class="_gaps_m">
<MkSwitch v-model="enableTwitterIntegration">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<template v-if="enableTwitterIntegration">
<FormInfo>Callback URL: {{ `${uri}/api/tw/cb` }}</FormInfo>
<MkInput v-model="twitterConsumerKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Consumer Key</template>
</MkInput>
<MkInput v-model="twitterConsumerSecret">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Consumer Secret</template>
</MkInput>
</template>
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</FormSuspense>
</template>
<script lang="ts" setup>
import { defineComponent } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import FormInfo from '@/components/MkInfo.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
let uri: string = $ref('');
let enableTwitterIntegration: boolean = $ref(false);
let twitterConsumerKey: string | null = $ref(null);
let twitterConsumerSecret: string | null = $ref(null);
async function init() {
const meta = await os.api('admin/meta');
uri = meta.uri;
enableTwitterIntegration = meta.enableTwitterIntegration;
twitterConsumerKey = meta.twitterConsumerKey;
twitterConsumerSecret = meta.twitterConsumerSecret;
}
function save() {
os.apiWithDialog('admin/update-meta', {
enableTwitterIntegration,
twitterConsumerKey,
twitterConsumerSecret,
}).then(() => {
fetchInstance();
});
}
</script>

View file

@ -1,61 +0,0 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<div class="_gaps_m">
<MkFolder>
<template #icon><i class="ti ti-brand-twitter"></i></template>
<template #label>Twitter</template>
<template #suffix>{{ enableTwitterIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
<XTwitter/>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-brand-github"></i></template>
<template #label>GitHub</template>
<template #suffix>{{ enableGithubIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
<XGithub/>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-brand-discord"></i></template>
<template #label>Discord</template>
<template #suffix>{{ enableDiscordIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
<XDiscord/>
</MkFolder>
</div>
</FormSuspense>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
import XTwitter from './integrations.twitter.vue';
import XGithub from './integrations.github.vue';
import XDiscord from './integrations.discord.vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let enableTwitterIntegration: boolean = $ref(false);
let enableGithubIntegration: boolean = $ref(false);
let enableDiscordIntegration: boolean = $ref(false);
async function init() {
const meta = await os.api('admin/meta');
enableTwitterIntegration = meta.enableTwitterIntegration;
enableGithubIntegration = meta.enableGithubIntegration;
enableDiscordIntegration = meta.enableDiscordIntegration;
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.integration,
icon: 'ti ti-share',
});
</script>

View file

@ -89,11 +89,6 @@ const menuDef = computed(() => [{
text: i18n.ts.email,
to: '/settings/email',
active: currentPage?.route.name === 'email',
}, {
icon: 'ti ti-share',
text: i18n.ts.integration,
to: '/settings/integration',
active: currentPage?.route.name === 'integration',
}, {
icon: 'ti ti-lock',
text: i18n.ts.security,

View file

@ -1,99 +0,0 @@
<template>
<div class="_gaps_m">
<FormSection v-if="instance.enableTwitterIntegration">
<template #label><i class="ti ti-brand-twitter"></i> Twitter</template>
<p v-if="integrations.twitter">{{ i18n.ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
<MkButton v-if="integrations.twitter" danger @click="disconnectTwitter">{{ i18n.ts.disconnectService }}</MkButton>
<MkButton v-else primary @click="connectTwitter">{{ i18n.ts.connectService }}</MkButton>
</FormSection>
<FormSection v-if="instance.enableDiscordIntegration">
<template #label><i class="ti ti-brand-discord"></i> Discord</template>
<p v-if="integrations.discord">{{ i18n.ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
<MkButton v-if="integrations.discord" danger @click="disconnectDiscord">{{ i18n.ts.disconnectService }}</MkButton>
<MkButton v-else primary @click="connectDiscord">{{ i18n.ts.connectService }}</MkButton>
</FormSection>
<FormSection v-if="instance.enableGithubIntegration">
<template #label><i class="ti ti-brand-github"></i> GitHub</template>
<p v-if="integrations.github">{{ i18n.ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
<MkButton v-if="integrations.github" danger @click="disconnectGithub">{{ i18n.ts.disconnectService }}</MkButton>
<MkButton v-else primary @click="connectGithub">{{ i18n.ts.connectService }}</MkButton>
</FormSection>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue';
import { apiUrl } from '@/config';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue';
import { $i } from '@/account';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const twitterForm = ref<Window | null>(null);
const discordForm = ref<Window | null>(null);
const githubForm = ref<Window | null>(null);
const integrations = computed(() => $i!.integrations);
function openWindow(service: string, type: string) {
return window.open(`${apiUrl}/${type}/${service}`,
`${service}_${type}_window`,
'height=570, width=520',
);
}
function connectTwitter() {
twitterForm.value = openWindow('twitter', 'connect');
}
function disconnectTwitter() {
openWindow('twitter', 'disconnect');
}
function connectDiscord() {
discordForm.value = openWindow('discord', 'connect');
}
function disconnectDiscord() {
openWindow('discord', 'disconnect');
}
function connectGithub() {
githubForm.value = openWindow('github', 'connect');
}
function disconnectGithub() {
openWindow('github', 'disconnect');
}
onMounted(() => {
document.cookie = `igi=${$i!.token}; path=/;` +
' max-age=31536000;' +
(document.location.protocol.startsWith('https') ? ' secure' : '');
watch(integrations, () => {
if (integrations.value.twitter) {
if (twitterForm.value) twitterForm.value.close();
}
if (integrations.value.discord) {
if (discordForm.value) discordForm.value.close();
}
if (integrations.value.github) {
if (githubForm.value) githubForm.value.close();
}
});
});
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.integration,
icon: 'ti ti-share',
});
</script>

View file

@ -1,136 +1,209 @@
// PIZZAX --- A lightweight store
import { onUnmounted, Ref, ref, watch } from 'vue';
import { BroadcastChannel } from 'broadcast-channel';
import { $i } from './account';
import { api } from './os';
import { get, set } from './scripts/idb-proxy';
import { defaultStore } from './store';
import { stream } from './stream';
import { deepClone } from './scripts/clone';
type StateDef = Record<string, {
where: 'account' | 'device' | 'deviceAccount';
default: any;
}>;
type State<T extends StateDef> = { [K in keyof T]: T[K]['default']; };
type ReactiveState<T extends StateDef> = { [K in keyof T]: Ref<T[K]['default']>; };
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
type PizzaxChannelMessage<T extends StateDef> = {
where: 'device' | 'deviceAccount';
key: keyof T;
value: T[keyof T]['default'];
userId?: string;
};
const connection = $i && stream.useChannel('main');
export class Storage<T extends StateDef> {
public readonly ready: Promise<void>;
public readonly loaded: Promise<void>;
public readonly key: string;
public readonly keyForLocalStorage: string;
public readonly deviceStateKeyName: `pizzax::${this['key']}`;
public readonly deviceAccountStateKeyName: `pizzax::${this['key']}::${string}` | '';
public readonly registryCacheKeyName: `pizzax::${this['key']}::cache::${string}` | '';
public readonly def: T;
// TODO: これが実装されたらreadonlyにしたい: https://github.com/microsoft/TypeScript/issues/37487
public readonly state: { [K in keyof T]: T[K]['default'] };
public readonly reactiveState: { [K in keyof T]: Ref<T[K]['default']> };
public readonly ready: Promise<void>;
private markAsReady: () => void = () => {};
public readonly state: State<T>;
public readonly reactiveState: ReactiveState<T>;
private pizzaxChannel: BroadcastChannel<PizzaxChannelMessage<T>>;
// 簡易的にキューイングして占有ロックとする
private currentIdbJob: Promise<any> = Promise.resolve();
private addIdbSetJob<T>(job: () => Promise<T>) {
const promise = this.currentIdbJob.then(job, e => {
console.error('Pizzax failed to save data to idb!', e);
return job();
});
this.currentIdbJob = promise;
return promise;
}
constructor(key: string, def: T) {
this.ready = new Promise((res) => {
this.markAsReady = res;
});
this.key = key;
this.keyForLocalStorage = 'pizzax::' + key;
this.deviceStateKeyName = `pizzax::${key}`;
this.deviceAccountStateKeyName = $i ? `pizzax::${key}::${$i.id}` : '';
this.registryCacheKeyName = $i ? `pizzax::${key}::cache::${$i.id}` : '';
this.def = def;
// TODO: indexedDBにする
const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}');
const deviceAccountState = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}') : {};
const registryCache = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}') : {};
this.pizzaxChannel = new BroadcastChannel(`pizzax::${key}`);
const state = {};
const reactiveState = {};
for (const [k, v] of Object.entries(def)) {
this.state = {} as State<T>;
this.reactiveState = {} as ReactiveState<T>;
for (const [k, v] of Object.entries(def) as [keyof T, T[keyof T]['default']][]) {
this.state[k] = v.default;
this.reactiveState[k] = ref(v.default);
}
this.ready = this.init();
this.loaded = this.ready.then(() => this.load());
}
private async init(): Promise<void> {
await this.migrate();
const deviceState: State<T> = await get(this.deviceStateKeyName) || {};
const deviceAccountState = $i ? await get(this.deviceAccountStateKeyName) || {} : {};
const registryCache = $i ? await get(this.registryCacheKeyName) || {} : {};
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
state[k] = deviceState[k];
this.reactiveState[k].value = this.state[k] = deviceState[k];
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
state[k] = registryCache[k];
this.reactiveState[k].value = this.state[k] = registryCache[k];
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
state[k] = deviceAccountState[k];
this.reactiveState[k].value = this.state[k] = deviceAccountState[k];
} else {
state[k] = v.default;
this.reactiveState[k].value = this.state[k] = v.default;
if (_DEV_) console.log('Use default value', k, v.default);
}
}
for (const [k, v] of Object.entries(state)) {
reactiveState[k] = ref(v);
}
this.state = state as any;
this.reactiveState = reactiveState as any;
this.pizzaxChannel.addEventListener('message', ({ where, key, value, userId }) => {
// アカウント変更すればunisonReloadが効くため、このreturnが発火することは
// まずないと思うけど一応弾いておく
if (where === 'deviceAccount' && !($i && userId !== $i.id)) return;
this.reactiveState[key].value = this.state[key] = value;
});
if ($i) {
// なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう)
window.setTimeout(() => {
api('i/registry/get-all', { scope: ['client', this.key] }).then(kvs => {
const cache = {};
for (const [k, v] of Object.entries(def)) {
if (v.where === 'account') {
if (Object.prototype.hasOwnProperty.call(kvs, k)) {
state[k] = kvs[k];
reactiveState[k].value = kvs[k];
cache[k] = kvs[k];
} else {
state[k] = v.default;
reactiveState[k].value = v.default;
}
}
}
localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
this.markAsReady();
});
}, 1);
// streamingのuser storage updateイベントを監視して更新
connection?.on('registryUpdated', ({ scope, key, value }: { scope: string[], key: keyof T, value: T[typeof key]['default'] }) => {
if (scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.state[key] === value) return;
connection?.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => {
if (!scope || scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.state[key] === value) return;
this.state[key] = value;
this.reactiveState[key].value = value;
const cache = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}');
if (cache[key] !== value) {
cache[key] = value;
localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
}
this.reactiveState[key].value = this.state[key] = value;
this.addIdbSetJob(async () => {
const cache = await get(this.registryCacheKeyName);
if (cache[key] !== value) {
cache[key] = value;
await set(this.registryCacheKeyName, cache);
}
});
});
} else {
this.markAsReady();
}
}
public set<K extends keyof T>(key: K, value: T[K]['default']): void {
if (_DEV_) console.log('set', key, value);
private load(): Promise<void> {
return new Promise((resolve, reject) => {
if ($i) {
// api関数と循環参照なので一応setTimeoutしておく
window.setTimeout(async () => {
await defaultStore.ready;
this.state[key] = value;
this.reactiveState[key].value = value;
api('i/registry/get-all', { scope: ['client', this.key] })
.then(kvs => {
const cache: Partial<T> = {};
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
if (v.where === 'account') {
if (Object.prototype.hasOwnProperty.call(kvs, k)) {
this.reactiveState[k].value = this.state[k] = (kvs as Partial<T>)[k];
cache[k] = (kvs as Partial<T>)[k];
} else {
this.reactiveState[k].value = this.state[k] = v.default;
}
}
}
return set(this.registryCacheKeyName, cache);
})
.then(() => resolve());
}, 1);
} else {
resolve();
}
});
}
switch (this.def[key].where) {
case 'device': {
const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}');
deviceState[key] = value;
localStorage.setItem(this.keyForLocalStorage, JSON.stringify(deviceState));
break;
public set<K extends keyof T>(key: K, value: T[K]['default']): Promise<void> {
// IndexedDBやBroadcastChannelで扱うために単純なオブジェクトにする
// (JSON.parse(JSON.stringify(value))の代わり)
const rawValue = deepClone(value);
if (_DEV_) console.log('set', key, rawValue, value);
this.reactiveState[key].value = this.state[key] = rawValue;
return this.addIdbSetJob(async () => {
if (_DEV_) console.log(`set ${key} start`);
switch (this.def[key].where) {
case 'device': {
this.pizzaxChannel.postMessage({
where: 'device',
key,
value: rawValue,
});
const deviceState = await get(this.deviceStateKeyName) || {};
deviceState[key] = rawValue;
await set(this.deviceStateKeyName, deviceState);
break;
}
case 'deviceAccount': {
if ($i == null) break;
this.pizzaxChannel.postMessage({
where: 'deviceAccount',
key,
value: rawValue,
userId: $i.id,
});
const deviceAccountState = await get(this.deviceAccountStateKeyName) || {};
deviceAccountState[key] = rawValue;
await set(this.deviceAccountStateKeyName, deviceAccountState);
break;
}
case 'account': {
if ($i == null) break;
const cache = await get(this.registryCacheKeyName) || {};
cache[key] = rawValue;
await set(this.registryCacheKeyName, cache);
await api('i/registry/set', {
scope: ['client', this.key],
key: key.toString(),
value: rawValue,
});
break;
}
}
case 'deviceAccount': {
if ($i == null) break;
const deviceAccountState = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}');
deviceAccountState[key] = value;
localStorage.setItem(this.keyForLocalStorage + '::' + $i.id, JSON.stringify(deviceAccountState));
break;
}
case 'account': {
if ($i == null) break;
const cache = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}');
cache[key] = value;
localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
api('i/registry/set', {
scope: ['client', this.key],
key: key,
value: value,
});
break;
}
}
if (_DEV_) console.log(`set ${key} complete`);
});
}
public push<K extends keyof T>(key: K, value: ArrayElement<T[K]['default']>): void {
@ -140,6 +213,7 @@ export class Storage<T extends StateDef> {
public reset(key: keyof T) {
this.set(key, this.def[key].default);
return this.def[key].default;
}
/**
@ -174,4 +248,25 @@ export class Storage<T extends StateDef> {
},
};
}
// localStorage => indexedDBのマイグレーション
private async migrate() {
const deviceState = localStorage.getItem(this.deviceStateKeyName);
if (deviceState) {
await set(this.deviceStateKeyName, JSON.parse(deviceState));
localStorage.removeItem(this.deviceStateKeyName);
}
const deviceAccountState = $i && localStorage.getItem(this.deviceAccountStateKeyName);
if ($i && deviceAccountState) {
await set(this.deviceAccountStateKeyName, JSON.parse(deviceAccountState));
localStorage.removeItem(this.deviceAccountStateKeyName);
}
const registryCache = $i && localStorage.getItem(this.registryCacheKeyName);
if ($i && registryCache) {
await set(this.registryCacheKeyName, JSON.parse(registryCache));
localStorage.removeItem(this.registryCacheKeyName);
}
}
}

View file

@ -70,10 +70,6 @@ export const routes = [{
path: '/email',
name: 'email',
component: page(() => import('./pages/settings/email.vue')),
}, {
path: '/integration',
name: 'integration',
component: page(() => import('./pages/settings/integration.vue')),
}, {
path: '/security',
name: 'security',
@ -400,10 +396,6 @@ export const routes = [{
path: '/relays',
name: 'relays',
component: page(() => import('./pages/admin/relays.vue')),
}, {
path: '/integrations',
name: 'integrations',
component: page(() => import('./pages/admin/integrations.vue')),
}, {
path: '/instance-block',
name: 'instance-block',

View file

@ -1,5 +1,7 @@
import { defaultStore } from '@/store';
await defaultStore.ready;
const ua = navigator.userAgent.toLowerCase();
const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700);
const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua);

View file

@ -86,10 +86,6 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'account',
default: [] as string[],
},
hiddenAds: {
where: 'account',
default: [] as string[],
},
menu: {
where: 'deviceAccount',
@ -342,6 +338,16 @@ export class ColdDeviceStorage {
}
}
public static getAll(): Partial<typeof this.default> {
return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce((acc, key) => {
const value = localStorage.getItem(PREFIX + key);
if (value != null) {
acc[key] = JSON.parse(value);
}
return acc;
}, {} as any);
}
public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
// 呼び出し側のバグ等で undefined が来ることがある
// undefined を文字列として miLocalStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視

View file

@ -1,7 +1,5 @@
import { inject } from 'vue';
import { post } from '@/os';
import { $i, login } from '@/account';
import { defaultStore } from '@/store';
import { getAccountFromId } from '@/scripts/get-account-from-id';
import { mainRouter } from '@/router';

View file

@ -132,7 +132,7 @@ if (window.innerWidth < 1024) {
document.documentElement.style.overflowY = 'scroll';
defaultStore.ready.then(() => {
defaultStore.loaded.then(() => {
if (defaultStore.state.widgets.length === 0) {
defaultStore.set('widgets', [{
name: 'calendar',

View file

@ -162,7 +162,7 @@ if (window.innerWidth > 1024) {
}
}
defaultStore.ready.then(() => {
defaultStore.loaded.then(() => {
if (defaultStore.state.widgets.length === 0) {
defaultStore.set('widgets', [{
name: 'calendar',

View file

@ -150,7 +150,7 @@ if (window.innerWidth > 1024) {
}
}
defaultStore.ready.then(() => {
defaultStore.loaded.then(() => {
if (defaultStore.state.widgets.length === 0) {
defaultStore.set('widgets', [{
name: 'calendar',

View file

@ -7,15 +7,15 @@
"lint": "tsc --noEmit && eslint --quiet src/**/*.ts"
},
"dependencies": {
"esbuild": "^0.14.42",
"idb-keyval": "^6.2.0",
"misskey-js": "0.0.14"
"esbuild": "0.14.42",
"idb-keyval": "6.2.0",
"misskey-js": "0.0.15"
},
"devDependencies": {
"@typescript-eslint/parser": "^5.49.0",
"@typescript-eslint/parser": "5.50.0",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.61",
"eslint": "^8.32.0",
"eslint-plugin-import": "^2.27.5",
"typescript": "4.9.4"
"eslint": "8.33.0",
"eslint-plugin-import": "2.27.5",
"typescript": "4.9.5"
}
}

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