diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml deleted file mode 100644 index 8b11c8413c..0000000000 --- a/.config/cypress-devcontainer.yml +++ /dev/null @@ -1,223 +0,0 @@ -#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -# Misskey configuration -#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -# ┌────────────────────────┐ -#───┘ Initial Setup Password └───────────────────────────────────────────────────── - -# Password to initiate setting up admin account. -# It will not be used after the initial setup is complete. -# -# Be sure to change this when you set up Misskey via the Internet. -# -# The provider of the service who sets up Misskey on behalf of the customer should -# set this value to something unique when generating the Misskey config file, -# and provide it to the customer. -setupPassword: example_password_please_change_this_or_you_will_get_hacked - -# ┌─────┐ -#───┘ URL └───────────────────────────────────────────────────── - -# Final accessible URL seen by a user. -url: 'http://misskey.local' - -# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE -# URL SETTINGS AFTER THAT! - -# ┌───────────────────────┐ -#───┘ Port and TLS settings └─────────────────────────────────── - -# -# Misskey requires a reverse proxy to support HTTPS connections. -# -# +----- https://example.tld/ ------------+ -# +------+ |+-------------+ +----------------+| -# | User | ---> || Proxy (443) | ---> | Misskey (3000) || -# +------+ |+-------------+ +----------------+| -# +---------------------------------------+ -# -# You need to set up a reverse proxy. (e.g. nginx) -# An encrypted connection with HTTPS is highly recommended -# because tokens may be transferred in GET requests. - -# The port that your Misskey server should listen on. -port: 61812 - -# ┌──────────────────────────┐ -#───┘ PostgreSQL configuration └──────────────────────────────── - -db: - host: db - port: 5432 - - # Database name - db: misskey - - # Auth - user: postgres - pass: postgres - - # Whether disable Caching queries - #disableCache: true - - # Extra Connection options - #extra: - # ssl: true - -dbReplications: false - -# You can configure any number of replicas here -#dbSlaves: -# - -# host: -# port: -# db: -# user: -# pass: -# - -# host: -# port: -# db: -# user: -# pass: - -# ┌─────────────────────┐ -#───┘ Redis configuration └───────────────────────────────────── - -redis: - host: redis - port: 6379 - #family: 0 # 0=Both, 4=IPv4, 6=IPv6 - #pass: example-pass - #prefix: example-prefix - #db: 1 - -#redisForPubsub: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -#redisForJobQueue: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -#redisForTimelines: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -#redisForReactions: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -# ┌───────────────────────────┐ -#───┘ MeiliSearch configuration └───────────────────────────── - -#meilisearch: -# host: meilisearch -# port: 7700 -# apiKey: '' -# ssl: true -# index: '' - -# ┌───────────────┐ -#───┘ ID generation └─────────────────────────────────────────── - -# You can select the ID generation method. -# You don't usually need to change this setting, but you can -# change it according to your preferences. - -# Available methods: -# aid ... Short, Millisecond accuracy -# aidx ... Millisecond accuracy -# meid ... Similar to ObjectID, Millisecond accuracy -# ulid ... Millisecond accuracy -# objectid ... This is left for backward compatibility - -# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE -# ID SETTINGS AFTER THAT! - -id: 'aidx' - -# ┌────────────────┐ -#───┘ Error tracking └────────────────────────────────────────── - -# Sentry is available for error tracking. -# See the Sentry documentation for more details on options. - -#sentryForBackend: -# enableNodeProfiling: true -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -#sentryForFrontend: -# vueIntegration: -# tracingOptions: -# trackComponents: true -# browserTracingIntegration: -# replayIntegration: -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -# ┌─────────────────────┐ -#───┘ Other configuration └───────────────────────────────────── - -# Whether disable HSTS -#disableHsts: true - -# Number of worker processes -#clusterLimit: 1 - -# Job concurrency per worker -# deliverJobConcurrency: 128 -# inboxJobConcurrency: 16 - -# Job rate limiter -# deliverJobPerSec: 128 -# inboxJobPerSec: 32 - -# Job attempts -# deliverJobMaxAttempts: 12 -# inboxJobMaxAttempts: 8 - -# IP address family used for outgoing request (ipv4, ipv6 or dual) -#outgoingAddressFamily: ipv4 - -# Proxy for HTTP/HTTPS -#proxy: http://127.0.0.1:3128 - -proxyBypassHosts: - - api.deepl.com - - api-free.deepl.com - - www.recaptcha.net - - hcaptcha.com - - challenges.cloudflare.com - -# Proxy for SMTP/SMTPS -#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT -#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 -#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 - -# Media Proxy -#mediaProxy: https://example.com/proxy - -allowedPrivateNetworks: [ - '127.0.0.1/32' -] - -# Upload or download file size limits (bytes) -#maxFileSize: 262144000 diff --git a/.config/docker_example.env b/.config/docker_example.env index c61248da2e..7a0261524b 100644 --- a/.config/docker_example.env +++ b/.config/docker_example.env @@ -1,11 +1,4 @@ -# misskey settings -# MISSKEY_URL=https://example.tld/ - # db settings POSTGRES_PASSWORD=example-misskey-pass -# DATABASE_PASSWORD=${POSTGRES_PASSWORD} POSTGRES_USER=example-misskey-user -# DATABASE_USER=${POSTGRES_USER} POSTGRES_DB=misskey -# DATABASE_DB=${POSTGRES_DB} -DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}" diff --git a/.config/docker_example.yml b/.config/docker_example.yml index dc354324dc..5cb17a44d1 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -6,7 +6,6 @@ #───┘ URL └───────────────────────────────────────────────────── # Final accessible URL seen by a user. -# You can set url from an environment variable instead. url: https://example.tld/ # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE @@ -39,11 +38,9 @@ db: port: 5432 # Database name - # You can set db from an environment variable instead. db: misskey # Auth - # You can set user and pass from environment variables instead. user: example-misskey-user pass: example-misskey-pass @@ -59,17 +56,17 @@ dbReplications: false # You can configure any number of replicas here #dbSlaves: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # ┌─────────────────────┐ #───┘ Redis configuration └───────────────────────────────────── @@ -98,45 +95,8 @@ redis: # #prefix: example-prefix # #db: 1 -#redisForTimelines: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -#redisForReactions: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -# ┌───────────────────────────────┐ -#───┘ Fulltext search configuration └───────────────────────────── - -# These are the setting items for the full-text search provider. -fulltextSearch: - # You can select the ID generation method. - # - sqlLike (default) - # Use SQL-like search. - # This is a standard feature of PostgreSQL, so no special extensions are required. - # - sqlPgroonga - # Use pgroonga. - # You need to install pgroonga and configure it as a PostgreSQL extension. - # In addition to the above, you need to create a pgroonga index on the text column of the note table. - # see: https://pgroonga.github.io/tutorial/ - # - meilisearch - # Use Meilisearch. - # You need to install Meilisearch and configure. - provider: sqlLike - -# For Meilisearch settings. -# If you select "meilisearch" for "fulltextSearch.provider", it must be set. -# You can set scope to local (default value) or global -# (include notes from remote). +# ┌───────────────────────────┐ +#───┘ MeiliSearch configuration └───────────────────────────── #meilisearch: # host: meilisearch @@ -144,7 +104,6 @@ fulltextSearch: # apiKey: '' # ssl: true # index: '' -# scope: local # ┌───────────────┐ #───┘ ID generation └─────────────────────────────────────────── @@ -155,7 +114,6 @@ fulltextSearch: # Available methods: # aid ... Short, Millisecond accuracy -# aidx ... Millisecond accuracy # meid ... Similar to ObjectID, Millisecond accuracy # ulid ... Millisecond accuracy # objectid ... This is left for backward compatibility @@ -163,27 +121,7 @@ fulltextSearch: # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # ID SETTINGS AFTER THAT! -id: 'aidx' - -# ┌────────────────┐ -#───┘ Error tracking └────────────────────────────────────────── - -# Sentry is available for error tracking. -# See the Sentry documentation for more details on options. - -#sentryForBackend: -# enableNodeProfiling: true -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -#sentryForFrontend: -# vueIntegration: -# tracingOptions: -# trackComponents: true -# browserTracingIntegration: -# replayIntegration: -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' +id: 'aid' # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── @@ -200,7 +138,7 @@ id: 'aidx' # Job rate limiter # deliverJobPerSec: 128 -# inboxJobPerSec: 32 +# inboxJobPerSec: 16 # Job attempts # deliverJobMaxAttempts: 12 @@ -227,22 +165,15 @@ proxyBypassHosts: # Media Proxy #mediaProxy: https://example.com/proxy -# For security reasons, uploading attachments from the intranet is prohibited, -# but exceptions can be made from the following settings. Default value is "undefined". -# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). +# Proxy remote files (default: false) +#proxyRemoteFiles: true + +# Sign to ActivityPub GET request (default: true) +signToActivityPubGet: true + #allowedPrivateNetworks: [ # '127.0.0.1/32' #] # Upload or download file size limits (bytes) #maxFileSize: 262144000 - -# Log settings -# logging: -# sql: -# # Outputs query parameters during SQL execution to the log. -# # default: false -# enableQueryParamLogging: false -# # Disable query truncation. If set to true, the full text of the query will be output to the log. -# # default: false -# disableQueryTruncation: false diff --git a/.config/example.yml b/.config/example.yml index c127eaae22..c179395966 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -2,77 +2,6 @@ # Misskey configuration #━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -# ┌──────────────────────────────┐ -#───┘ a boring but important thing └──────────────────────────── - -# -# First of all, let me tell you a story that may possibly be -# boring to you and possibly important to you. -# -# Misskey is licensed under the AGPLv3 license. This license is -# known to be often misunderstood. Please read the following -# instructions carefully and select the appropriate option so -# that you do not negligently cause a license violation. -# - -# -------- -# Option 1: If you host Misskey AS-IS (without any changes to -# the source code. forks are not included). -# -# Step 1: Congratulations! You don't need to do anything. - -# -------- -# Option 2: If you have made changes to the source code (forks -# are included) and publish a Git repository of source -# code. There should be no access restrictions on -# this repository. Strictly speaking, it doesn't have -# to be a Git repository, but you'll probably use Git! -# -# Step 1: Build and run the Misskey server first. -# Step 2: Open in -# your browser with the administrator account. -# Step 3: Enter the URL of your Git repository in the -# "Repository URL" field. - -# -------- -# Option 3: If neither of the above applies to you. -# (In this case, the source code should be published -# on the Misskey interface. IT IS NOT ENOUGH TO -# DISCLOSE THE SOURCE CODE WHEN A USER REQUESTS IT BY -# E-MAIL OR OTHER MEANS. If you are not satisfied -# with this, it is recommended that you read the -# license again carefully. Anyway, enabling this -# option will automatically generate and publish a -# tarball at build time, protecting you from -# inadvertent license violations. (There is no legal -# guarantee, of course.) The tarball will generated -# from the root directory of your codebase. So it is -# also recommended to check directory -# once after building and before activating the server -# to avoid ACCIDENTAL LEAKING OF SENSITIVE INFORMATION. -# To prevent certain files from being included in the -# tarball, add a glob pattern after line 15 in -# . DO NOT FORGET TO BUILD AFTER -# ENABLING THIS OPTION!) -# -# Step 1: Uncomment the following line. -# -# publishTarballInsteadOfProvideRepositoryUrl: true - -# ┌────────────────────────┐ -#───┘ Initial Setup Password └───────────────────────────────────────────────────── - -# Password to initiate setting up admin account. -# It will not be used after the initial setup is complete. -# -# Be sure to change this when you set up Misskey via the Internet. -# -# The provider of the service who sets up Misskey on behalf of the customer should -# set this value to something unique when generating the Misskey config file, -# and provide it to the customer. -# -# setupPassword: example_password_please_change_this_or_you_will_get_hacked - # ┌─────┐ #───┘ URL └───────────────────────────────────────────────────── @@ -101,10 +30,6 @@ url: https://example.tld/ # The port that your Misskey server should listen on. port: 3000 -# You can also use UNIX domain socket. -# socket: /path/to/misskey.sock -# chmodSocket: '777' - # ┌──────────────────────────┐ #───┘ PostgreSQL configuration └──────────────────────────────── @@ -131,17 +56,17 @@ dbReplications: false # You can configure any number of replicas here #dbSlaves: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # ┌─────────────────────┐ #───┘ Redis configuration └───────────────────────────────────── @@ -153,8 +78,6 @@ redis: #pass: example-pass #prefix: example-prefix #db: 1 - # You can specify more ioredis options... - #username: example-username #redisForPubsub: # host: localhost @@ -163,8 +86,6 @@ redis: # #pass: example-pass # #prefix: example-prefix # #db: 1 -# # You can specify more ioredis options... -# #username: example-username #redisForJobQueue: # host: localhost @@ -173,52 +94,9 @@ redis: # #pass: example-pass # #prefix: example-prefix # #db: 1 -# # You can specify more ioredis options... -# #username: example-username -#redisForTimelines: -# host: localhost -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 -# # You can specify more ioredis options... -# #username: example-username - -#redisForReactions: -# host: localhost -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 -# # You can specify more ioredis options... -# #username: example-username - -# ┌───────────────────────────────┐ -#───┘ Fulltext search configuration └───────────────────────────── - -# These are the setting items for the full-text search provider. -fulltextSearch: - # You can select the ID generation method. - # - sqlLike (default) - # Use SQL-like search. - # This is a standard feature of PostgreSQL, so no special extensions are required. - # - sqlPgroonga - # Use pgroonga. - # You need to install pgroonga and configure it as a PostgreSQL extension. - # In addition to the above, you need to create a pgroonga index on the text column of the note table. - # see: https://pgroonga.github.io/tutorial/ - # - meilisearch - # Use Meilisearch. - # You need to install Meilisearch and configure. - provider: sqlLike - -# For Meilisearch settings. -# If you select "meilisearch" for "fulltextSearch.provider", it must be set. -# You can set scope to local (default value) or global -# (include notes from remote). +# ┌───────────────────────────┐ +#───┘ MeiliSearch configuration └───────────────────────────── #meilisearch: # host: localhost @@ -226,7 +104,6 @@ fulltextSearch: # apiKey: '' # ssl: true # index: '' -# scope: local # ┌───────────────┐ #───┘ ID generation └─────────────────────────────────────────── @@ -237,7 +114,6 @@ fulltextSearch: # Available methods: # aid ... Short, Millisecond accuracy -# aidx ... Millisecond accuracy # meid ... Similar to ObjectID, Millisecond accuracy # ulid ... Millisecond accuracy # objectid ... This is left for backward compatibility @@ -245,27 +121,7 @@ fulltextSearch: # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # ID SETTINGS AFTER THAT! -id: 'aidx' - -# ┌────────────────┐ -#───┘ Error tracking └────────────────────────────────────────── - -# Sentry is available for error tracking. -# See the Sentry documentation for more details on options. - -#sentryForBackend: -# enableNodeProfiling: true -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -#sentryForFrontend: -# vueIntegration: -# tracingOptions: -# trackComponents: true -# browserTracingIntegration: -# replayIntegration: -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' +id: 'aid' # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── @@ -279,22 +135,19 @@ id: 'aidx' # Job concurrency per worker #deliverJobConcurrency: 128 #inboxJobConcurrency: 16 -#relationshipJobConcurrency: 16 -# What's relationshipJob?: +#relashionshipJobConcurrency: 16 +# What's relashionshipJob?: # Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations. # Job rate limiter #deliverJobPerSec: 128 -#inboxJobPerSec: 32 -#relationshipJobPerSec: 64 +#inboxJobPerSec: 16 +#relashionshipJobPerSec: 64 # Job attempts #deliverJobMaxAttempts: 12 #inboxJobMaxAttempts: 8 -# Local address used for outgoing requests -#outgoingAddress: 127.0.0.1 - # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 @@ -319,31 +172,22 @@ proxyBypassHosts: # * Perform image compression (on a different server resource than the main process) #mediaProxy: https://example.com/proxy +# Proxy remote files (default: false) +# Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains. +#proxyRemoteFiles: true + # Movie Thumbnail Generation URL # There is no reference implementation. # For example, Misskey will point to the following URL: # https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 #videoThumbnailGenerator: https://example.com -# For security reasons, uploading attachments from the intranet is prohibited, -# but exceptions can be made from the following settings. Default value is "undefined". -# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). +# Sign to ActivityPub GET request (default: true) +signToActivityPubGet: true + #allowedPrivateNetworks: [ # '127.0.0.1/32' #] # Upload or download file size limits (bytes) #maxFileSize: 262144000 - -# PID File of master process -#pidFile: /tmp/misskey.pid - -# Log settings -# logging: -# sql: -# # Outputs query parameters during SQL execution to the log. -# # default: false -# enableQueryParamLogging: false -# # Disable query truncation. If set to true, the full text of the query will be output to the log. -# # default: false -# disableQueryTruncation: false diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 514abdfb20..a47804ab07 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,24 +1,23 @@ { "name": "Misskey", - "dockerComposeFile": "compose.yml", + "dockerComposeFile": "docker-compose.yml", "service": "app", "workspaceFolder": "/workspace", "features": { + "ghcr.io/devcontainers-contrib/features/pnpm:2": {}, "ghcr.io/devcontainers/features/node:1": { - "version": "22.15.0" - }, - "ghcr.io/devcontainers-extra/features/pnpm:2": { - "version": "10.10.0" + "version": "18.16.0" } }, "forwardPorts": [3000], - "postCreateCommand": "/bin/bash .devcontainer/init.sh", + "postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh", "customizations": { "vscode": { "extensions": [ "editorconfig.editorconfig", "dbaeumer.vscode-eslint", "Vue.volar", + "Vue.vscode-typescript-vue-plugin", "Orta.vscode-jest", "dbaeumer.vscode-eslint", "mrmlnc.vscode-json5" diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index fb0d25c214..824a046dc0 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -56,17 +56,17 @@ dbReplications: false # You can configure any number of replicas here #dbSlaves: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # ┌─────────────────────┐ #───┘ Redis configuration └───────────────────────────────────── @@ -95,22 +95,6 @@ redis: # #prefix: example-prefix # #db: 1 -#redisForTimelines: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -#redisForReactions: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── @@ -130,7 +114,6 @@ redis: # Available methods: # aid ... Short, Millisecond accuracy -# aidx ... Millisecond accuracy # meid ... Similar to ObjectID, Millisecond accuracy # ulid ... Millisecond accuracy # objectid ... This is left for backward compatibility @@ -138,27 +121,7 @@ redis: # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # ID SETTINGS AFTER THAT! -id: 'aidx' - -# ┌────────────────┐ -#───┘ Error tracking └────────────────────────────────────────── - -# Sentry is available for error tracking. -# See the Sentry documentation for more details on options. - -#sentryForBackend: -# enableNodeProfiling: true -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -#sentryForFrontend: -# vueIntegration: -# tracingOptions: -# trackComponents: true -# browserTracingIntegration: -# replayIntegration: -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' +id: 'aid' # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── @@ -175,7 +138,7 @@ id: 'aidx' # Job rate limiter # deliverJobPerSec: 128 -# inboxJobPerSec: 32 +# inboxJobPerSec: 16 # Job attempts # deliverJobMaxAttempts: 12 @@ -202,6 +165,12 @@ proxyBypassHosts: # Media Proxy #mediaProxy: https://example.com/proxy +# Proxy remote files (default: false) +#proxyRemoteFiles: true + +# Sign to ActivityPub GET request (default: true) +signToActivityPubGet: true + allowedPrivateNetworks: [ '127.0.0.1/32' ] diff --git a/.devcontainer/compose.yml b/.devcontainer/docker-compose.yml similarity index 92% rename from .devcontainer/compose.yml rename to .devcontainer/docker-compose.yml index d02d2a8f4a..8f8c5a13ab 100644 --- a/.devcontainer/compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,12 +1,13 @@ +version: '3.8' + services: app: - build: + build: context: . dockerfile: Dockerfile volumes: - ../:/workspace:cached - - node_modules:/workspace/node_modules command: sleep infinity @@ -45,7 +46,6 @@ services: volumes: postgres-data: redis-data: - node_modules: networks: internal_network: diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh index 216292b082..bcad3e6d85 100755 --- a/.devcontainer/init.sh +++ b/.devcontainer/init.sh @@ -2,14 +2,10 @@ set -xe -sudo chown node node_modules -sudo apt-get update -sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb -git config --global --add safe.directory /workspace +sudo chown -R node /workspace git submodule update --init pnpm config set store-dir /home/node/.local/share/pnpm/store pnpm install --frozen-lockfile cp .devcontainer/devcontainer.yml .config/default.yml pnpm build pnpm migrate -pnpm exec cypress install diff --git a/.dockerignore b/.dockerignore index f204349160..1de0c7982b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,11 +7,12 @@ Dockerfile build/ built/ db/ -.devcontainer/compose.yml +docker-compose.yml node_modules/ packages/*/node_modules redis/ files/ +misskey-assets/ fluent-emojis/ .pnp.* @@ -27,4 +28,4 @@ fluent-emojis/ .idea/ packages/*/.vscode/ -packages/backend/test/compose.yml +packages/backend/test/docker-compose.yml diff --git a/.editorconfig b/.editorconfig index def7baa1a8..a6f988f8d7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,10 +6,6 @@ indent_size = 2 charset = utf-8 insert_final_newline = true end_of_line = lf -trim_trailing_whitespace = true - -[*.md] -trim_trailing_whitespace = false [*.{yml,yaml}] indent_style = space diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..c6b2a1611c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +patreon: syuilo diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.md b/.github/ISSUE_TEMPLATE/01_bug-report.md new file mode 100644 index 0000000000..25e7fc8b20 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_bug-report.md @@ -0,0 +1,60 @@ +--- +name: 🐛 Bug Report +about: Create a report to help us improve +title: '' +labels: ⚠️bug? +assignees: '' + +--- + + + +## 💡 Summary + + + +## 🥰 Expected Behavior + + + +## 🤬 Actual Behavior + + + +## 📝 Steps to Reproduce + +1. +2. +3. + +## 📌 Environment + + + + +### 💻 Frontend +* Model and OS of the device(s): + +* Browser: + +* Server URL: + +* Misskey: + 13.x.x + +### 🛰 Backend (for server admin) + + +* Installation Method or Hosting Service: +* Misskey: 13.x.x +* Node: 18.x.x +* PostgreSQL: 15.x.x +* Redis: 7.x.x +* OS and Architecture: diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.yml b/.github/ISSUE_TEMPLATE/01_bug-report.yml deleted file mode 100644 index 077855b5bf..0000000000 --- a/.github/ISSUE_TEMPLATE/01_bug-report.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: 🐛 Bug Report -description: Create a report to help us improve -labels: ["⚠️bug?"] - -body: - - type: markdown - attributes: - value: | - Thanks for reporting! - First, in order to avoid duplicate Issues, please search to see if the problem you found has already been reported. - Also, If you are NOT owner/admin of server, PLEASE DONT REPORT SERVER SPECIFIC ISSUES TO HERE! (e.g. feature XXX is not working in misskey.example) Please try with another misskey servers, and if your issue is only reproducible with specific server, contact your server's owner/admin first. - - - type: textarea - attributes: - label: 💡 Summary - description: Tell us what the bug is - validations: - required: true - - - type: textarea - attributes: - label: 🥰 Expected Behavior - description: Tell us what should happen - validations: - required: true - - - type: textarea - attributes: - label: 🤬 Actual Behavior - description: | - Tell us what happens instead of the expected behavior. - Please include errors from the developer console and/or server log files if you have access to them. - validations: - required: true - - - type: textarea - attributes: - label: 📝 Steps to Reproduce - placeholder: | - 1. - 2. - 3. - validations: - required: false - - - type: textarea - attributes: - label: 💻 Frontend Environment - description: | - Tell us where on the platform it happens - DO NOT WRITE "latest". Please provide the specific version. - - Examples: - * Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4 - * Browser: Chrome 113.0.5672.126 - * Server URL: misskey.example.com - * Misskey: 2025.x.x - value: | - * Model and OS of the device(s): - * Browser: - * Server URL: - * Misskey: - render: markdown - validations: - required: false - - - type: textarea - attributes: - label: 🛰 Backend Environment (for server admin) - description: | - Tell us where on the platform it happens - DO NOT WRITE "latest". Please provide the specific version. - If you are using a managed service, put that after the version. - - Examples: - * Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment - * Misskey: 2025.x.x - * Node: 20.x.x - * PostgreSQL: 15.x.x - * Redis: 7.x.x - * OS and Architecture: Ubuntu 24.04.2 LTS aarch64 - value: | - * Installation Method or Hosting Service: - * Misskey: - * Node: - * PostgreSQL: - * Redis: - * OS and Architecture: - render: markdown - validations: - required: false - - - type: checkboxes - attributes: - label: Do you want to address this bug yourself? - options: - - label: Yes, I will patch the bug myself and send a pull request diff --git a/.github/ISSUE_TEMPLATE/02_feature-request.md b/.github/ISSUE_TEMPLATE/02_feature-request.md new file mode 100644 index 0000000000..5045b17712 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_feature-request.md @@ -0,0 +1,12 @@ +--- +name: ✨ Feature Request +about: Suggest an idea for this project +title: '' +labels: ✨Feature +assignees: '' + +--- + +## Summary + + diff --git a/.github/ISSUE_TEMPLATE/02_feature-request.yml b/.github/ISSUE_TEMPLATE/02_feature-request.yml deleted file mode 100644 index 8d7b0b2539..0000000000 --- a/.github/ISSUE_TEMPLATE/02_feature-request.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: ✨ Feature Request -description: Suggest an idea for this project -labels: ["✨Feature"] - -body: - - type: textarea - attributes: - label: Summary - description: Tell us what the suggestion is - validations: - required: true - - type: textarea - attributes: - label: Purpose - description: Describe the specific problem or need you think this feature will solve, and who it will help. - validations: - required: true - - type: checkboxes - attributes: - label: Do you want to implement this feature yourself? - options: - - label: Yes, I will implement this by myself and send a pull request diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 5acad83336..730647b086 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,7 @@ contact_links: + - name: 👪 Misskey Forum + url: https://forum.misskey.io/ + about: Ask questions and share knowledge - name: 💬 Misskey official Discord url: https://discord.gg/Wp8gVStHW3 about: Chat freely about Misskey - # 仮 - - name: 💬 Start discussion - url: https://github.com/misskey-dev/misskey/discussions - about: The official forum to join conversation and ask question diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b93080278d..e878e5836a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,36 +10,23 @@ updates: schedule: interval: daily open-pull-requests-limit: 0 - -# Add only the root, not each workspace item -# https://github.com/dependabot/dependabot-core/issues/4993#issuecomment-1289133027 - package-ecosystem: npm directory: "/" schedule: interval: daily open-pull-requests-limit: 0 - # List dependencies required to be updated together, sharing the same version numbers. - # Those who simply have the common owner (e.g. @fastify) don't need to be listed. - groups: - aws-sdk: - patterns: - - "@aws-sdk/*" - nestjs: - patterns: - - "@nestjs/*" - slacc: - patterns: - - "slacc-*" - storybook: - patterns: - - "storybook*" - - "@storybook/*" - swc-core: - patterns: - - "@swc/core*" - typescript-eslint: - patterns: - - "@typescript-eslint/*" - tensorflow: - patterns: - - "@tensorflow/*" +- package-ecosystem: npm + directory: "/packages/backend" + schedule: + interval: daily + open-pull-requests-limit: 0 +- package-ecosystem: npm + directory: "/packages/frontend" + schedule: + interval: daily + open-pull-requests-limit: 0 +- package-ecosystem: npm + directory: "/packages/sw" + schedule: + interval: daily + open-pull-requests-limit: 0 diff --git a/.github/labeler.yml b/.github/labeler.yml index b64d726d65..137be487c0 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,34 +1,21 @@ 'packages/backend': -- any: - - changed-files: - - any-glob-to-any-file: ['packages/backend/**/*'] +- packages/backend/**/* 'packages/backend:test': -- any: - - changed-files: - - any-glob-to-any-file: ['packages/backend/test/**/*', 'packages/backend/test-federation/**/*'] +- packages/backend/test/**/* 'packages/frontend': -- any: - - changed-files: - - any-glob-to-any-file: ['packages/frontend/**/*'] +- packages/frontend/**/* 'packages/frontend:test': -- any: - - changed-files: - - any-glob-to-any-file: ['cypress/**/*'] +- cypress/**/* 'packages/sw': -- any: - - changed-files: - - any-glob-to-any-file: ['packages/sw/**/*'] +- packages/sw/**/* 'packages/misskey-js': -- any: - - changed-files: - - any-glob-to-any-file: ['packages/misskey-js/**/*'] +- packages/misskey-js/**/* 'packages/misskey-js:test': -- any: - - changed-files: - - any-glob-to-any-file: ['packages/misskey-js/test/**/*', 'packages/misskey-js/test-d/**/*'] +- packages/misskey-js/test/**/* +- packages/misskey-js/test-d/**/* diff --git a/.github/min.node-version b/.github/min.node-version deleted file mode 100644 index d5a159609d..0000000000 --- a/.github/min.node-version +++ /dev/null @@ -1 +0,0 @@ -20.10.0 diff --git a/.github/misskey/test.yml b/.github/misskey/test.yml index 3c807e8b9e..f43f74be14 100644 --- a/.github/misskey/test.yml +++ b/.github/misskey/test.yml @@ -1,7 +1,5 @@ url: 'http://misskey.local' -setupPassword: example_password_please_change_this_or_you_will_get_hacked - # ローカルでテストするときにポートを被らないようにするためデフォルトのものとは変える(以下同じ) port: 61812 @@ -14,4 +12,4 @@ db: redis: host: 127.0.0.1 port: 56312 -id: aidx +id: aid diff --git a/.github/reviewer-lottery.yml b/.github/reviewer-lottery.yml new file mode 100644 index 0000000000..c88e1342de --- /dev/null +++ b/.github/reviewer-lottery.yml @@ -0,0 +1,9 @@ +groups: + - name: devs + reviewers: 2 + internal_reviewers: 1 + usernames: + - syuilo + - acid-chicken + - EbiseLutica + - tamaina diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml index 6117e69c03..ed004c78dc 100644 --- a/.github/workflows/api-misskey-js.yml +++ b/.github/workflows/api-misskey-js.yml @@ -1,14 +1,7 @@ name: API report (misskey.js) -on: - push: - paths: - - packages/misskey-js/** - - .github/workflows/api-misskey-js.yml - pull_request: - paths: - - packages/misskey-js/** - - .github/workflows/api-misskey-js.yml +on: [push, pull_request] + jobs: report: @@ -16,13 +9,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v3.3.0 - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 + - run: corepack enable - name: Setup Node.js - uses: actions/setup-node@v4.4.0 + uses: actions/setup-node@v3.6.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml deleted file mode 100644 index 5ca27749bb..0000000000 --- a/.github/workflows/changelog-check.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Check the description in CHANGELOG.md - -on: - pull_request: - branches: - - master - - develop - -jobs: - check-changelog: - runs-on: ubuntu-latest - - steps: - - name: Checkout head - uses: actions/checkout@v4.2.2 - - name: Setup Node.js - uses: actions/setup-node@v4.4.0 - with: - node-version-file: '.node-version' - - - name: Checkout base - run: | - mkdir _base - cp -r .git _base/.git - cd _base - git fetch --depth 1 origin ${{ github.base_ref }} - git checkout origin/${{ github.base_ref }} CHANGELOG.md - - - name: Copy to Checker directory for CHANGELOG-base.md - run: cp _base/CHANGELOG.md scripts/changelog-checker/CHANGELOG-base.md - - name: Copy to Checker directory for CHANGELOG-head.md - run: cp CHANGELOG.md scripts/changelog-checker/CHANGELOG-head.md - - name: diff - continue-on-error: true - run: diff -u CHANGELOG-base.md CHANGELOG-head.md - working-directory: scripts/changelog-checker - - - name: Setup Checker - run: npm install - working-directory: scripts/changelog-checker - - name: Run Checker - run: npm run run - working-directory: scripts/changelog-checker diff --git a/.github/workflows/check-misskey-js-autogen.yml b/.github/workflows/check-misskey-js-autogen.yml deleted file mode 100644 index 22d500c306..0000000000 --- a/.github/workflows/check-misskey-js-autogen.yml +++ /dev/null @@ -1,139 +0,0 @@ -name: Check Misskey JS autogen - -on: - pull_request_target: - branches: - - master - - develop - - improve-misskey-js-autogen-check - paths: - - packages/backend/** - -jobs: - # pull_request_target safety: permissions: read-all, and there are no secrets used in this job - generate-misskey-js: - runs-on: ubuntu-latest - permissions: - contents: read - if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }} - steps: - - name: checkout - uses: actions/checkout@v4.2.2 - with: - submodules: true - persist-credentials: false - ref: refs/pull/${{ github.event.pull_request.number }}/merge - - - name: setup pnpm - uses: pnpm/action-setup@v4 - - - name: setup node - id: setup-node - uses: actions/setup-node@v4.4.0 - with: - node-version-file: '.node-version' - cache: pnpm - - - name: install dependencies - run: pnpm i --frozen-lockfile - - # generate api.json - - name: Copy Config - run: cp .config/example.yml .config/default.yml - - name: Build - run: pnpm build - - name: Generate API JSON - run: pnpm --filter backend generate-api-json - - # build misskey js - - name: Build misskey-js - run: |- - cp packages/backend/built/api.json packages/misskey-js/generator/api.json - pnpm run --filter misskey-js-type-generator generate - - # packages/misskey-js/generator/built/autogen - - name: Upload Generated - uses: actions/upload-artifact@v4 - with: - name: generated-misskey-js - path: packages/misskey-js/generator/built/autogen - - # pull_request_target safety: permissions: read-all, and no user codes are executed - get-actual-misskey-js: - runs-on: ubuntu-latest - permissions: - contents: read - if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }} - steps: - - name: checkout - uses: actions/checkout@v4.2.2 - with: - submodules: true - persist-credentials: false - ref: refs/pull/${{ github.event.pull_request.number }}/merge - - - name: Upload From Merged - uses: actions/upload-artifact@v4 - with: - name: actual-misskey-js - path: packages/misskey-js/src/autogen - - # pull_request_target safety: nothing is cloned from repository - comment-misskey-js-autogen: - runs-on: ubuntu-latest - needs: [generate-misskey-js, get-actual-misskey-js] - permissions: - pull-requests: write - steps: - - name: download generated-misskey-js - uses: actions/download-artifact@v4 - with: - name: generated-misskey-js - path: misskey-js-generated - - - name: download actual-misskey-js - uses: actions/download-artifact@v4 - with: - name: actual-misskey-js - path: misskey-js-actual - - - name: check misskey-js changes - id: check-changes - run: | - diff -r -u --label=generated --label=on-tree ./misskey-js-generated ./misskey-js-actual > misskey-js.diff || true - - if [ -s misskey-js.diff ]; then - echo "changes=true" >> $GITHUB_OUTPUT - else - echo "changes=false" >> $GITHUB_OUTPUT - fi - - - name: Print full diff - run: cat ./misskey-js.diff - - - name: send message - if: steps.check-changes.outputs.changes == 'true' - uses: thollander/actions-comment-pull-request@v2 - with: - comment_tag: check-misskey-js-autogen - message: |- - Thank you for sending us a great Pull Request! 👍 - Please regenerate misskey-js type definitions! 🙏 - - example: - ```sh - pnpm run build-misskey-js-with-types - ``` - - - name: send message - if: steps.check-changes.outputs.changes == 'false' - uses: thollander/actions-comment-pull-request@v2 - with: - comment_tag: check-misskey-js-autogen - mode: delete - message: "Thank you!" - create_if_not_exists: false - - - name: Make failure if changes are detected - if: steps.check-changes.outputs.changes == 'true' - run: exit 1 diff --git a/.github/workflows/check-misskey-js-version.yml b/.github/workflows/check-misskey-js-version.yml deleted file mode 100644 index 2b15cbee53..0000000000 --- a/.github/workflows/check-misskey-js-version.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Check Misskey JS version - -on: - push: - branches: [ develop ] - paths: - - packages/misskey-js/package.json - - package.json - - .github/workflows/check-misskey-js-version.yml - pull_request: - branches: [ develop ] - paths: - - packages/misskey-js/package.json - - package.json - - .github/workflows/check-misskey-js-version.yml -jobs: - check-version: - # ルートの package.json と packages/misskey-js/package.json のバージョンが一致しているかを確認する - name: Check version - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4.2.2 - - name: Check version - run: | - if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/misskey-js/package.json)" ]; then - echo "Version mismatch!" - exit 1 - fi diff --git a/.github/workflows/check-spdx-license-id.yml b/.github/workflows/check-spdx-license-id.yml deleted file mode 100644 index e40a4557df..0000000000 --- a/.github/workflows/check-spdx-license-id.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Check SPDX-License-Identifier - -on: - push: - branches: - - master - - develop - pull_request: - -jobs: - check-spdx-license-id: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4.2.2 - - name: Check - run: | - counter=0 - - search() { - local directory="$1" - find "$directory" -type f \ - '(' \ - -name "*.cjs" -and -not -name '*.config.cjs' -o \ - -name "*.html" -o \ - -name "*.js" -and -not -name '*.config.js' -o \ - -name "*.mjs" -and -not -name '*.config.mjs' -o \ - -name "*.scss" -o \ - -name "*.ts" -and -not -name '*.config.ts' -o \ - -name "*.vue" \ - ')' -and \ - -not -name '*eslint*' - } - - check() { - local file="$1" - if ! ( - grep -q "SPDX-FileCopyrightText: syuilo and misskey-project" "$file" || - grep -q "SPDX-License-Identifier: AGPL-3.0-only" "$file" - ); then - echo "Missing: $file" - ((counter++)) - fi - } - - directories=( - "cypress/e2e" - "packages/backend/migration" - "packages/backend/src" - "packages/backend/test" - "packages/frontend-shared/@types" - "packages/frontend-shared/js" - "packages/frontend/.storybook" - "packages/frontend/@types" - "packages/frontend/lib" - "packages/frontend/public" - "packages/frontend/src" - "packages/frontend/test" - "packages/frontend-embed/@types" - "packages/frontend-embed/src" - "packages/icons-subsetter/src" - "packages/misskey-bubble-game/src" - "packages/misskey-reversi/src" - "packages/sw/src" - "scripts" - ) - - for directory in "${directories[@]}"; do - for file in $(search $directory); do - check "$file" - done - done - - if [ $counter -gt 0 ]; then - echo "SPDX-License-Identifier is missing in $counter files." - exit 1 - else - echo "SPDX-License-Identifier is certainly described in all target files!" - exit 0 - fi diff --git a/.github/workflows/check_copyright_year.yml b/.github/workflows/check_copyright_year.yml index eaf922d4bc..8daea44a83 100644 --- a/.github/workflows/check_copyright_year.yml +++ b/.github/workflows/check_copyright_year.yml @@ -10,7 +10,7 @@ jobs: check_copyright_year: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v3.2.0 - run: | if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then echo "Please change copyright year!" diff --git a/.github/workflows/deploy-test-environment.yml b/.github/workflows/deploy-test-environment.yml deleted file mode 100644 index 46baf7421b..0000000000 --- a/.github/workflows/deploy-test-environment.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: deploy-test-environment - -on: - issue_comment: - types: [created] - workflow_dispatch: - inputs: - repository: - description: 'Repository to deploy (optional, use the repository where this workflow is stored by default)' - required: false - default: '' - branch_or_hash: - description: 'Branch or Commit hash to deploy (optional, use the branch where this workflow is stored by default)' - required: false - default: '' - wait_time: - description: 'Time to wait in seconds (optional, 1800 seconds by default)' - required: false - default: '' - -jobs: - get-pr-ref: - runs-on: ubuntu-latest - if: github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/preview') - outputs: - is-allowed-user: ${{ steps.check-allowed-users.outputs.is-allowed-user }} - pr-ref: ${{ steps.get-ref.outputs.pr-ref }} - wait_time: ${{ steps.get-wait-time.outputs.wait_time }} - steps: - - name: Checkout - uses: actions/checkout@v4.2.2 - - - name: Check allowed users - id: check-allowed-users - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ORG_ID: ${{ github.repository_owner_id }} - COMMENT_AUTHOR: ${{ github.event.comment.user.login }} - run: | - MEMBERSHIP_STATUS=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/organizations/$ORG_ID/public_members/$COMMENT_AUTHOR" \ - -o /dev/null -w '%{http_code}\n' -s) - if [ "$MEMBERSHIP_STATUS" -eq 204 ]; then - echo "is-allowed-user=true" > $GITHUB_OUTPUT - else - echo "is-allowed-user=false" > $GITHUB_OUTPUT - fi - - - name: Get PR ref - id: get-ref - run: | - PR_REF="refs/pull/${{ github.event.issue.number }}/head" - echo "pr-ref=$PR_REF" >> $GITHUB_OUTPUT - - - name: Extract wait time - id: get-wait-time - env: - COMMENT_BODY: ${{ github.event.comment.body }} - run: | - WAIT_TIME=$(echo "$COMMENT_BODY" | grep -oP '(?<=/preview\s)\d+' || echo "1800") - echo "wait_time=$WAIT_TIME" > $GITHUB_OUTPUT - - deploy-test-environment-pr-comment: - needs: get-pr-ref - if: needs.get-pr-ref.outputs.is-allowed-user == 'true' - uses: joinmisskey/misskey-tga/.github/workflows/deploy-test-environment.yml@main - with: - repository: ${{ github.repository }} - branch_or_hash: ${{ needs.get-pr-ref.outputs.pr-ref }} - wait_time: ${{ needs.get-pr-ref.outputs.wait_time }} - secrets: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - - deploy-test-environment-wd: - if: github.event_name == 'workflow_dispatch' - uses: joinmisskey/misskey-tga/.github/workflows/deploy-test-environment.yml@main - with: - repository: ${{ inputs.repository || github.repository }} - branch_or_hash: ${{ inputs.branch_or_hash || github.ref_name }} - wait_time: ${{ inputs.wait_time || '1800' }} - secrets: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml index 56dedf273d..09a2c33e0c 100644 --- a/.github/workflows/docker-develop.yml +++ b/.github/workflows/docker-develop.yml @@ -6,83 +6,38 @@ on: - develop workflow_dispatch: -env: - REGISTRY_IMAGE: misskey/misskey - jobs: - # see https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners - build: - name: Build + push_to_registry: + name: Push Docker image to Docker Hub runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - platform: - - linux/amd64 - - linux/arm64 if: github.repository == 'misskey-dev/misskey' steps: - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Check out the repo - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v3.3.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + id: buildx + uses: docker/setup-buildx-action@v2.3.0 + with: + platforms: linux/amd64,linux/arm64 + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: misskey/misskey - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and push by digest - id: build - uses: docker/build-push-action@v6 + - name: Build and Push to Docker Hub + uses: docker/build-push-action@v4 with: + builder: ${{ steps.buildx.outputs.name }} context: . push: true - platforms: ${{ matrix.platform }} + platforms: ${{ steps.buildx.outputs.platforms }} provenance: false + tags: misskey/misskey:develop labels: develop cache-from: type=gha cache-to: type=gha,mode=max - outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - merge: - runs-on: ubuntu-latest - needs: - - build - steps: - - name: Download digests - uses: actions/download-artifact@v4 - with: - path: /tmp/digests - pattern: digests-* - merge-multiple: true - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create --tag ${{ env.REGISTRY_IMAGE }}:develop \ - $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:develop diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index eb98273ba0..a465d92eaf 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -5,101 +5,45 @@ on: types: [published] workflow_dispatch: -env: - REGISTRY_IMAGE: misskey/misskey - TAGS: | - type=edge - type=ref,event=pr - type=ref,event=branch - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - jobs: - # see https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners - build: - name: Build + push_to_registry: + name: Push Docker image to Docker Hub runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - platform: - - linux/amd64 - - linux/arm64 + steps: - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Check out the repo - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v3.3.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + id: buildx + uses: docker/setup-buildx-action@v2.3.0 + with: + platforms: linux/amd64,linux/arm64 - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v4 with: - images: ${{ env.REGISTRY_IMAGE }} - tags: ${{ env.TAGS }} + images: misskey/misskey + tags: | + type=edge + type=ref,event=pr + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and Push to Docker Hub - id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v4 with: + builder: ${{ steps.buildx.outputs.name }} context: . push: true - platforms: ${{ matrix.platform }} + platforms: ${{ steps.buildx.outputs.platforms }} provenance: false + tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - merge: - runs-on: ubuntu-latest - needs: - - build - steps: - - name: Download digests - uses: actions/download-artifact@v4 - with: - path: /tmp/digests - pattern: digests-* - merge-multiple: true - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY_IMAGE }} - tags: ${{ env.TAGS }} - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml index 3054607913..9b79ee54f0 100644 --- a/.github/workflows/dockle.yml +++ b/.github/workflows/dockle.yml @@ -13,16 +13,14 @@ jobs: runs-on: ubuntu-latest env: DOCKER_CONTENT_TRUST: 1 - DOCKLE_VERSION: 0.4.14 steps: - - uses: actions/checkout@v4.2.2 - - name: Download and install dockle v${{ env.DOCKLE_VERSION }} - run: | - curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb" + - uses: actions/checkout@v3.2.0 + - run: | + curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb" sudo dpkg -i dockle.deb - run: | cp .config/docker_example.env .config/docker.env - cp ./compose_example.yml ./compose.yml + cp ./docker-compose.yml.example ./docker-compose.yml - run: | docker compose up -d web docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml deleted file mode 100644 index 933404dfa5..0000000000 --- a/.github/workflows/get-api-diff.yml +++ /dev/null @@ -1,67 +0,0 @@ -# this name is used in report-api-diff.yml so be careful when change name -name: Get api.json from Misskey - -on: - pull_request: - branches: - - master - - develop - paths: - - packages/backend/** - - .github/workflows/get-api-diff.yml -jobs: - get-from-misskey: - runs-on: ubuntu-latest - permissions: - contents: read - - strategy: - matrix: - api-json-name: [api-base.json, api-head.json] - include: - - api-json-name: api-base.json - ref: ${{ github.base_ref }} - - api-json-name: api-head.json - ref: refs/pull/${{ github.event.number }}/merge - - steps: - - uses: actions/checkout@v4.2.2 - with: - ref: ${{ matrix.ref }} - submodules: true - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 - - name: Use Node.js - uses: actions/setup-node@v4.4.0 - with: - node-version-file: '.node-version' - cache: 'pnpm' - - run: pnpm i --frozen-lockfile - - name: Check pnpm-lock.yaml - run: git diff --exit-code pnpm-lock.yaml - - name: Copy Configure - run: cp .config/example.yml .config/default.yml - - name: Build - run: pnpm build - - name: Generate API JSON - run: pnpm --filter backend generate-api-json - - name: Copy API.json - run: cp packages/backend/built/api.json ${{ matrix.api-json-name }} - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: api-artifact-${{ matrix.api-json-name }} - path: ${{ matrix.api-json-name }} - - save-pr-number: - runs-on: ubuntu-latest - steps: - - name: Save PR number - env: - PR_NUMBER: ${{ github.event.number }} - run: | - echo "$PR_NUMBER" > ./pr_number - - uses: actions/upload-artifact@v4 - with: - name: api-artifact-pr-number - path: pr_number diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 88e2aceaed..fa4a58c3a9 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -11,6 +11,6 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v4 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 235faeb807..0f3702f958 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,45 +5,25 @@ on: branches: - master - develop - paths: - - packages/backend/** - - packages/frontend/** - - packages/frontend-shared/** - - packages/frontend-embed/** - - packages/icons-subsetter/** - - packages/sw/** - - packages/misskey-js/** - - packages/misskey-bubble-game/** - - packages/misskey-reversi/** - - packages/shared/eslint.config.js - - .github/workflows/lint.yml pull_request: - paths: - - packages/backend/** - - packages/frontend/** - - packages/frontend-shared/** - - packages/frontend-embed/** - - packages/icons-subsetter/** - - packages/sw/** - - packages/misskey-js/** - - packages/misskey-bubble-game/** - - packages/misskey-reversi/** - - packages/shared/eslint.config.js - - .github/workflows/lint.yml + jobs: pnpm_install: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v3.3.0 with: fetch-depth: 0 submodules: true - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 - - uses: actions/setup-node@v4.4.0 + - uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + - uses: actions/setup-node@v3.6.0 with: node-version-file: '.node-version' cache: 'pnpm' + - run: corepack enable - run: pnpm i --frozen-lockfile lint: @@ -55,35 +35,24 @@ jobs: workspace: - backend - frontend - - frontend-shared - - frontend-embed - - icons-subsetter - sw - misskey-js - - misskey-bubble-game - - misskey-reversi - env: - eslint-cache-version: v1 - eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }} steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v3.3.0 with: fetch-depth: 0 submodules: true - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 - - uses: actions/setup-node@v4.4.0 + - uses: pnpm/action-setup@v2 + with: + version: 7 + run_install: false + - uses: actions/setup-node@v3.6.0 with: node-version-file: '.node-version' cache: 'pnpm' + - run: corepack enable - run: pnpm i --frozen-lockfile - - name: Restore eslint cache - uses: actions/cache@v4.2.3 - with: - path: ${{ env.eslint-cache-path }} - key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }} - restore-keys: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}- - - run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location ${{ env.eslint-cache-path }} --cache-strategy content + - run: pnpm --filter ${{ matrix.workspace }} run eslint typecheck: needs: [pnpm_install] @@ -93,22 +62,20 @@ jobs: matrix: workspace: - backend - - sw - misskey-js steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v3.3.0 with: fetch-depth: 0 submodules: true - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 - - uses: actions/setup-node@v4.4.0 + - uses: pnpm/action-setup@v2 + with: + version: 7 + run_install: false + - uses: actions/setup-node@v3.6.0 with: node-version-file: '.node-version' cache: 'pnpm' + - run: corepack enable - run: pnpm i --frozen-lockfile - - run: pnpm --filter misskey-js run build - if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'sw' }} - - run: pnpm --filter misskey-reversi run build - if: ${{ matrix.workspace == 'backend' }} - run: pnpm --filter ${{ matrix.workspace }} run typecheck diff --git a/.github/workflows/locale.yml b/.github/workflows/locale.yml deleted file mode 100644 index 68e45fdf61..0000000000 --- a/.github/workflows/locale.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Lint - -on: - push: - paths: - - locales/** - - .github/workflows/locale.yml - pull_request: - paths: - - locales/** - - .github/workflows/locale.yml -jobs: - locale_verify: - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 0 - submodules: true - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 - - uses: actions/setup-node@v4.4.0 - with: - node-version-file: '.node-version' - cache: 'pnpm' - - run: pnpm i --frozen-lockfile - - run: cd locales && node verify.js diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml new file mode 100644 index 0000000000..87af3a6ba6 --- /dev/null +++ b/.github/workflows/ok-to-test.yml @@ -0,0 +1,36 @@ +# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event +name: Ok To Test + +on: + issue_comment: + types: [created] + +jobs: + ok-to-test: + runs-on: ubuntu-latest + # Only run for PRs, not issue comments + if: ${{ github.event.issue.pull_request }} + steps: + # Generate a GitHub App installation access token from an App ID and private key + # To create a new GitHub App: + # https://developer.github.com/apps/building-github-apps/creating-a-github-app/ + # See app.yml for an example app manifest + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@v1 + with: + app_id: ${{ secrets.DEPLOYBOT_APP_ID }} + private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }} + + - name: Slash Command Dispatch + uses: peter-evans/slash-command-dispatch@v1 + env: + TOKEN: ${{ steps.generate_token.outputs.token }} + with: + token: ${{ env.TOKEN }} # GitHub App installation access token + # token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work + reaction-token: ${{ secrets.GITHUB_TOKEN }} + issue-type: pull-request + commands: deploy + named-args: true + permission: write diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml deleted file mode 100644 index c156de1a8b..0000000000 --- a/.github/workflows/on-release-created.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: On Release Created (Publish misskey-js) - -on: - release: - types: [created] - - workflow_dispatch: - -jobs: - publish-misskey-js: - name: Publish misskey-js - runs-on: ubuntu-latest - - permissions: - contents: read - id-token: write - - steps: - - uses: actions/checkout@v4.2.2 - with: - submodules: true - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 - - name: Use Node.js - uses: actions/setup-node@v4.4.0 - with: - node-version-file: '.node-version' - cache: 'pnpm' - - name: Publish package - run: | - pnpm i --frozen-lockfile - pnpm build - pnpm --filter misskey-js publish --access public --no-git-checks --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} - NPM_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} diff --git a/.github/workflows/pr-preview-deploy.yml b/.github/workflows/pr-preview-deploy.yml new file mode 100644 index 0000000000..9b786d34aa --- /dev/null +++ b/.github/workflows/pr-preview-deploy.yml @@ -0,0 +1,92 @@ +# Run secret-dependent integration tests only after /deploy approval +on: + repository_dispatch: + types: [deploy-command] + +name: Deploy preview environment + +jobs: + # Repo owner has commented /deploy on a (fork-based) pull request + deploy-preview-environment: + runs-on: ubuntu-latest + if: + github.event.client_payload.slash_command.sha != '' && + contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha) + steps: + - uses: actions/github-script@v6.3.3 + id: check-id + env: + number: ${{ github.event.client_payload.pull_request.number }} + job: ${{ github.job }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + result-encoding: string + script: | + const { data: pull } = await github.rest.pulls.get({ + ...context.repo, + pull_number: process.env.number + }); + const ref = pull.head.sha; + + const { data: checks } = await github.rest.checks.listForRef({ + ...context.repo, + ref + }); + + const check = checks.check_runs.filter(c => c.name === process.env.job); + + return check[0].id; + + - uses: actions/github-script@v6.3.3 + env: + check_id: ${{ steps.check-id.outputs.result }} + details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.checks.update({ + ...context.repo, + check_run_id: process.env.check_id, + status: 'in_progress', + details_url: process.env.details_url + }); + + # Check out merge commit + - name: Fork based /deploy checkout + uses: actions/checkout@v3.3.0 + with: + ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' + + # + - name: Context + uses: okteto/context@latest + with: + token: ${{ secrets.OKTETO_TOKEN }} + + - name: Deploy preview environment + uses: ikuradon/deploy-preview@latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + name: pr-${{ github.event.client_payload.pull_request.number }}-syuilo + timeout: 15m + + # Update check run called "integration-fork" + - uses: actions/github-script@v6.3.3 + id: update-check-run + if: ${{ always() }} + env: + # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run + conclusion: ${{ job.status }} + check_id: ${{ steps.check-id.outputs.result }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: result } = await github.rest.checks.update({ + ...context.repo, + check_run_id: process.env.check_id, + status: 'completed', + conclusion: process.env.conclusion + }); + + return result; diff --git a/.github/workflows/pr-preview-destroy.yml b/.github/workflows/pr-preview-destroy.yml new file mode 100644 index 0000000000..8adfad9dab --- /dev/null +++ b/.github/workflows/pr-preview-destroy.yml @@ -0,0 +1,54 @@ +# file: .github/workflows/preview-closed.yaml +on: + pull_request: + types: + - closed + +name: Destroy preview environment + +jobs: + destroy-preview-environment: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v6.3.3 + id: check-conclusion + env: + number: ${{ github.event.number }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + result-encoding: string + script: | + const { data: pull } = await github.rest.pulls.get({ + ...context.repo, + pull_number: process.env.number + }); + const ref = pull.head.sha; + + const { data: checks } = await github.rest.checks.listForRef({ + ...context.repo, + ref + }); + + const check = checks.check_runs.filter(c => c.name === 'deploy-preview-environment'); + + if (check.length === 0) { + return; + } + + const { data: result } = await github.rest.checks.get({ + ...context.repo, + check_run_id: check[0].id, + }); + + return result.conclusion; + - name: Context + if: steps.check-conclusion.outputs.result == 'success' + uses: okteto/context@latest + with: + token: ${{ secrets.OKTETO_TOKEN }} + + - name: Destroy preview environment + if: steps.check-conclusion.outputs.result == 'success' + uses: okteto/destroy-preview@latest + with: + name: pr-${{ github.event.number }}-syuilo diff --git a/.github/workflows/release-edit-with-push.yml b/.github/workflows/release-edit-with-push.yml deleted file mode 100644 index 57657a4ba7..0000000000 --- a/.github/workflows/release-edit-with-push.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: "Release Manager: sync changelog with PR" - -on: - push: - branches: - - develop - paths: - - 'CHANGELOG.md' - -env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - edit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - # headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得 - - name: Get PR - run: | - echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT - id: get_pr - env: - STABLE_BRANCH: ${{ vars.STABLE_BRANCH }} - - name: Get target version - if: steps.get_pr.outputs.pr_number != '' - uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v2 - id: v - # CHANGELOG.mdの内容を取得 - - name: Get changelog - if: steps.get_pr.outputs.pr_number != '' - uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v2 - with: - version: ${{ steps.v.outputs.target_version }} - id: changelog - # PRのnotesを更新 - - name: Update PR - if: steps.get_pr.outputs.pr_number != '' - run: | - gh pr edit "$PR_NUMBER" --body "$CHANGELOG" - env: - PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }} - CHANGELOG: ${{ steps.changelog.outputs.changelog }} diff --git a/.github/workflows/release-with-dispatch.yml b/.github/workflows/release-with-dispatch.yml deleted file mode 100644 index d750001b71..0000000000 --- a/.github/workflows/release-with-dispatch.yml +++ /dev/null @@ -1,136 +0,0 @@ -name: "Release Manager [Dispatch]" - -on: - workflow_dispatch: - inputs: - ## Specify the type of the next release. - #version_increment_type: - # type: choice - # description: 'VERSION INCREMENT TYPE' - # default: 'patch' - # required: false - # options: - # - 'major' - # - 'minor' - # - 'patch' - merge: - type: boolean - description: 'MERGE RELEASE BRANCH TO MAIN' - default: false - start-rc: - type: boolean - description: 'Start Release Candidate' - default: false - -env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - get-pr: - runs-on: ubuntu-latest - outputs: - pr_number: ${{ steps.get_pr.outputs.pr_number }} - steps: - - uses: actions/checkout@v4 - # headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得 - - name: Get PRs - run: | - echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT - id: get_pr - env: - STABLE_BRANCH: ${{ vars.STABLE_BRANCH }} - - merge: - uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v2 - needs: get-pr - if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }} - with: - pr_number: ${{ needs.get-pr.outputs.pr_number }} - user: 'github-actions[bot]' - package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} - # Text to prepend to the changelog - # The first line must be `## Unreleased` - changes_template: | - ## Unreleased - - ### General - - - - ### Client - - - - ### Server - - - - use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} - indent: ${{ vars.INDENT }} - secrets: - RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} - RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - - create-prerelease: - uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2 - needs: get-pr - if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }} - with: - pr_number: ${{ needs.get-pr.outputs.pr_number }} - user: 'github-actions[bot]' - package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} - use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} - indent: ${{ vars.INDENT }} - draft_prerelease_channel: alpha - ready_start_prerelease_channel: beta - prerelease_channel: ${{ inputs.start-rc && 'rc' || '' }} - reset_number_on_channel_change: true - secrets: - RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} - RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - - create-target: - uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v2 - needs: get-pr - if: ${{ needs.get-pr.outputs.pr_number == '' }} - with: - user: 'github-actions[bot]' - # The script for version increment. - # process.env.CURRENT_VERSION: The current version. - # - # Misskey calender versioning (yyyy.MM.patch) example - version_increment_script: | - const now = new Date(); - const year = now.toLocaleDateString('en-US', { year: 'numeric', timeZone: 'Asia/Tokyo' }); - const month = now.toLocaleDateString('en-US', { month: 'numeric', timeZone: 'Asia/Tokyo' }); - const [major, minor, _patch] = process.env.CURRENT_VERSION.split('.'); - const patch = Number(_patch.split('-')[0]); - if (Number.isNaN(patch)) { - console.error('Invalid patch version', year, month, process.env.CURRENT_VERSION, major, minor, _patch); - throw new Error('Invalid patch version'); - } - if (year !== major || month !== minor) { - return `${year}.${month}.0`; - } else { - return `${major}.${minor}.${patch + 1}`; - } - ##Semver example - #version_increment_script: | - # const [major, minor, patch] = process.env.CURRENT_VERSION.split('.'); - # if ("${{ inputs.version_increment_type }}" === "major") { - # return `${Number(major) + 1}.0.0`; - # } else if ("${{ inputs.version_increment_type }}" === "minor") { - # return `${major}.${Number(minor) + 1}.0`; - # } else { - # return `${major}.${minor}.${Number(patch) + 1}`; - # } - package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} - use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} - indent: ${{ vars.INDENT }} - stable_branch: ${{ vars.STABLE_BRANCH }} - draft_prerelease_channel: alpha - secrets: - RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} - RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} diff --git a/.github/workflows/report-api-diff.yml b/.github/workflows/report-api-diff.yml deleted file mode 100644 index 1170f898ce..0000000000 --- a/.github/workflows/report-api-diff.yml +++ /dev/null @@ -1,104 +0,0 @@ -name: Report API Diff - -on: - workflow_run: - types: [completed] - workflows: - - Get api.json from Misskey # get-api-diff.yml - -jobs: - compare-diff: - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' }} - permissions: - pull-requests: write - -# api-artifact - steps: - - name: Download artifact - uses: actions/github-script@v7.0.1 - with: - script: | - const fs = require('fs'); - let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.payload.workflow_run.id, - }); - let matchArtifacts = allArtifacts.data.artifacts.filter((artifact) => { - return artifact.name.startsWith("api-artifact-") || artifact.name == "api-artifact" - }); - await Promise.all(matchArtifacts.map(async (artifact) => { - let download = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: artifact.id, - archive_format: 'zip', - }); - await fs.promises.writeFile(`${process.env.GITHUB_WORKSPACE}/${artifact.name}.zip`, Buffer.from(download.data)); - })); - - name: Extract all artifacts - run: | - find . -mindepth 1 -maxdepth 1 -type f -name '*.zip' -exec unzip {} -d artifacts ';' - ls -la - - name: Load PR Number - id: load-pr-num - run: echo "pr-number=$(cat artifacts/pr_number)" >> "$GITHUB_OUTPUT" - - - name: Output base - run: cat ./artifacts/api-base.json - - name: Output head - run: cat ./artifacts/api-head.json - - name: Arrange json files - run: | - jq '.' ./artifacts/api-base.json > ./api-base.json - jq '.' ./artifacts/api-head.json > ./api-head.json - - name: Get diff of 2 files - run: diff -u --label=base --label=head ./api-base.json ./api-head.json | cat > api.json.diff - - name: Get full diff - run: diff --label=base --label=head --new-line-format='+%L' --old-line-format='-%L' --unchanged-line-format=' %L' ./api-base.json ./api-head.json | cat > api-full.json.diff - - name: Echo full diff - run: cat ./api-full.json.diff - - name: Upload full diff to Artifact - uses: actions/upload-artifact@v4 - with: - name: api-artifact - path: | - api-full.json.diff - api-base.json - api-head.json - - id: out-diff - name: Build diff Comment - run: | - HEADER="このPRによるapi.jsonの差分" - FOOTER="[Get diff files from Workflow Page](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})" - DIFF_BYTES="$(stat ./api.json.diff -c '%s' | tr -d '\n')" - - echo "$HEADER" > ./output.md - - if (( "$DIFF_BYTES" <= 1 )); then - echo '差分はありません。' >> ./output.md - else - echo '
' >> ./output.md - echo '差分はこちら' >> ./output.md - echo >> ./output.md - echo '```diff' >> ./output.md - cat ./api.json.diff >> ./output.md - echo '```' >> ./output.md - echo '
' >> .output.md - fi - - echo "$FOOTER" >> ./output.md - - uses: thollander/actions-comment-pull-request@v2 - with: - pr_number: ${{ steps.load-pr-num.outputs.pr-number }} - comment_tag: show_diff - filePath: ./output.md - - name: Tell error to PR - uses: thollander/actions-comment-pull-request@v2 - if: failure() && steps.load-pr-num.outputs.pr-number - with: - pr_number: ${{ steps.load-pr-num.outputs.pr-number }} - comment_tag: show_diff_error - message: | - api.jsonの差分作成中にエラーが発生しました。詳細は[Workflowのログ](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})を確認してください。 diff --git a/.github/workflows/reviewer_lottery.yml b/.github/workflows/reviewer_lottery.yml new file mode 100644 index 0000000000..33228d7465 --- /dev/null +++ b/.github/workflows/reviewer_lottery.yml @@ -0,0 +1,13 @@ +name: "Reviewer lottery" +on: + pull_request_target: + types: [opened, ready_for_review, reopened] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: uesteibar/reviewer-lottery@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index b1d95c1b33..6cb1b34997 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -6,28 +6,21 @@ on: - master - develop pull_request_target: - branches-ignore: - # Since pull requests targets master mostly is the "develop" branch. - # Storybook CI is checked on the "push" event of "develop" branch so it would cause a duplicate build. - # This is a waste of chromatic build quota, so we don't run storybook CI on pull requests targets master. - - master jobs: build: - # Chromatic is not likely to be available for fork repositories, so we disable for fork repositories. - if: github.repository == 'misskey-dev/misskey' runs-on: ubuntu-latest env: NODE_OPTIONS: "--max_old_space_size=7168" steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v3.3.0 if: github.event_name != 'pull_request_target' with: fetch-depth: 0 submodules: true - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v3.3.0 if: github.event_name == 'pull_request_target' with: fetch-depth: 0 @@ -35,19 +28,26 @@ jobs: ref: "refs/pull/${{ github.event.number }}/merge" - name: Checkout actual HEAD if: github.event_name == 'pull_request_target' - run: git checkout "$(git rev-list --parents -n1 HEAD | cut -d" " -f3)" - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 - - name: Use Node.js - uses: actions/setup-node@v4.4.0 + id: rev + run: | + echo "base=$(git rev-list --parents -n1 HEAD | cut -d" " -f2)" >> $GITHUB_OUTPUT + git checkout $(git rev-list --parents -n1 HEAD | cut -d" " -f3) + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + - name: Use Node.js 18.x + uses: actions/setup-node@v3.6.0 with: node-version-file: '.node-version' cache: 'pnpm' + - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml - - name: Build dependent packages - run: pnpm -F misskey-js -F misskey-bubble-game -F misskey-reversi build + - name: Build misskey-js + run: pnpm --filter misskey-js build - name: Build storybook run: pnpm --filter frontend build-storybook - name: Publish to Chromatic @@ -78,19 +78,23 @@ jobs: if: github.event_name == 'pull_request_target' id: chromatic_pull_request run: | - CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff --name-only origin/${GITHUB_BASE_REF}...origin/${GITHUB_HEAD_REF} | xargs))" + DIFF="${{ steps.rev.outputs.base }} HEAD" + if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then + DIFF="HEAD" + fi + CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))" if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then echo "skip=true" >> $GITHUB_OUTPUT fi - BRANCH="${{ github.event.pull_request.head.user.login }}:$GITHUB_HEAD_REF" - if [ "$BRANCH" = "misskey-dev:$GITHUB_HEAD_REF" ]; then - BRANCH="$GITHUB_HEAD_REF" + BRANCH="${{ github.event.pull_request.head.user.login }}:${{ github.event.pull_request.head.ref }}" + if [ "$BRANCH" = "misskey-dev:${{ github.event.pull_request.head.ref }}" ]; then + BRANCH="${{ github.event.pull_request.head.ref }}" fi - pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name "$BRANCH" $(echo "$CHROMATIC_PARAMETER") + pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER") env: CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - name: Notify that Chromatic detects changes - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v6.4.0 if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false' with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -102,7 +106,7 @@ jobs: body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).' }) - name: Upload Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: storybook path: packages/frontend/storybook-static diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 9d611c9964..d7be15bd4f 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -5,32 +5,19 @@ on: branches: - master - develop - paths: - - packages/backend/** - # for permissions - - packages/misskey-js/** - - .github/workflows/test-backend.yml - - .github/misskey/test.yml pull_request: - paths: - - packages/backend/** - # for permissions - - packages/misskey-js/** - - .github/workflows/test-backend.yml - - .github/misskey/test.yml + jobs: - unit: - name: Unit tests (backend) + jest: runs-on: ubuntu-latest + strategy: matrix: - node-version-file: - - .node-version - - .github/min.node-version + node-version: [18.x] services: postgres: - image: postgres:15 + image: postgres:13 ports: - 54312:5432 env: @@ -42,31 +29,20 @@ jobs: - 56312:6379 steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v3.3.0 with: submodules: true - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 - - name: Install FFmpeg - run: | - for i in {1..3}; do - echo "Attempt $i: Installing FFmpeg..." - curl -s -L https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz -o ffmpeg.tar.xz && \ - tar -xf ffmpeg.tar.xz && \ - mv ffmpeg-*-static/ffmpeg /usr/local/bin/ && \ - mv ffmpeg-*-static/ffprobe /usr/local/bin/ && \ - rm -rf ffmpeg.tar.xz ffmpeg-*-static/ && \ - break || sleep 10 - if [ $i -eq 3 ]; then - echo "Failed to install FFmpeg after 3 attempts" - exit 1 - fi - done - - name: Use Node.js - uses: actions/setup-node@v4.4.0 + - name: Install pnpm + uses: pnpm/action-setup@v2 with: - node-version-file: ${{ matrix.node-version-file }} + version: 8 + run_install: false + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3.6.0 + with: + node-version: ${{ matrix.node-version }} cache: 'pnpm' + - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml @@ -75,57 +51,9 @@ jobs: - name: Build run: pnpm build - name: Test - run: pnpm --filter backend test-and-coverage - - name: Upload to Codecov - uses: codecov/codecov-action@v5 + run: pnpm jest-and-coverage + - name: Upload Coverage + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./packages/backend/coverage/coverage-final.json - - e2e: - name: E2E tests (backend) - runs-on: ubuntu-latest - strategy: - matrix: - node-version-file: - - .node-version - - .github/min.node-version - - services: - postgres: - image: postgres:15 - ports: - - 54312:5432 - env: - POSTGRES_DB: test-misskey - POSTGRES_HOST_AUTH_METHOD: trust - redis: - image: redis:7 - ports: - - 56312:6379 - - steps: - - uses: actions/checkout@v4.2.2 - with: - submodules: true - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 - - name: Use Node.js - uses: actions/setup-node@v4.4.0 - with: - node-version-file: ${{ matrix.node-version-file }} - cache: 'pnpm' - - run: pnpm i --frozen-lockfile - - name: Check pnpm-lock.yaml - run: git diff --exit-code pnpm-lock.yaml - - name: Copy Configure - run: cp .github/misskey/test.yml .config - - name: Build - run: pnpm build - - name: Test - run: pnpm --filter backend test-and-coverage:e2e - - name: Upload to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./packages/backend/coverage/coverage-final.json diff --git a/.github/workflows/test-federation.yml b/.github/workflows/test-federation.yml deleted file mode 100644 index 737b543a73..0000000000 --- a/.github/workflows/test-federation.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Test (federation) - -on: - push: - branches: - - master - - develop - paths: - - packages/backend/** - - packages/misskey-js/** - - .github/workflows/test-federation.yml - pull_request: - paths: - - packages/backend/** - - packages/misskey-js/** - - .github/workflows/test-federation.yml - -jobs: - test: - name: Federation test - runs-on: ubuntu-latest - strategy: - matrix: - node-version-file: - - .node-version - - .github/min.node-version - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 - - name: Install FFmpeg - run: | - for i in {1..3}; do - echo "Attempt $i: Installing FFmpeg..." - curl -s -L https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz -o ffmpeg.tar.xz && \ - tar -xf ffmpeg.tar.xz && \ - mv ffmpeg-*-static/ffmpeg /usr/local/bin/ && \ - mv ffmpeg-*-static/ffprobe /usr/local/bin/ && \ - rm -rf ffmpeg.tar.xz ffmpeg-*-static/ && \ - break || sleep 10 - if [ $i -eq 3 ]; then - echo "Failed to install FFmpeg after 3 attempts" - exit 1 - fi - done - - name: Use Node.js - uses: actions/setup-node@v4.4.0 - with: - node-version-file: ${{ matrix.node-version-file }} - cache: 'pnpm' - - name: Build Misskey - run: | - pnpm i --frozen-lockfile - pnpm build - - name: Setup - run: | - echo "NODE_VERSION=$(cat ${{ matrix.node-version-file }})" >> $GITHUB_ENV - cd packages/backend/test-federation - bash ./setup.sh - sudo chmod 644 ./certificates/*.test.key - - name: Start servers - id: start_servers - continue-on-error: true - # https://github.com/docker/compose/issues/1294#issuecomment-374847206 - run: | - cd packages/backend/test-federation - docker compose up -d --scale tester=0 - - name: Print start_servers error - if: ${{ steps.start_servers.outcome == 'failure' }} - run: | - cd packages/backend/test-federation - docker compose logs | tail -n 300 - exit 1 - - name: Test - run: | - cd packages/backend/test-federation - docker compose run --no-deps tester - - name: Log - if: always() - run: | - cd packages/backend/test-federation - docker compose logs - - name: Stop servers - if: always() - run: | - cd packages/backend/test-federation - docker compose down diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index 94e43cf91e..4ea4ba4628 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -5,39 +5,31 @@ on: branches: - master - develop - paths: - - packages/frontend/** - # for permissions - - packages/misskey-js/** - # for e2e - - packages/backend/** - - .github/workflows/test-frontend.yml - - .github/misskey/test.yml pull_request: - paths: - - packages/frontend/** - # for permissions - - packages/misskey-js/** - # for e2e - - packages/backend/** - - .github/workflows/test-frontend.yml - - .github/misskey/test.yml + jobs: vitest: - name: Unit tests (frontend) runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v3.3.0 with: submodules: true - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 - - name: Use Node.js - uses: actions/setup-node@v4.4.0 + - name: Install pnpm + uses: pnpm/action-setup@v2 with: - node-version-file: '.node-version' + version: 8 + run_install: false + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3.6.0 + with: + node-version: ${{ matrix.node-version }} cache: 'pnpm' + - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml @@ -48,23 +40,23 @@ jobs: - name: Test run: pnpm --filter frontend test-and-coverage - name: Upload Coverage - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./packages/frontend/coverage/coverage-final.json e2e: - name: E2E tests (frontend) runs-on: ubuntu-latest strategy: fail-fast: false matrix: + node-version: [18.x] browser: [chrome] services: postgres: - image: postgres:15 + image: postgres:13 ports: - 54312:5432 env: @@ -76,7 +68,7 @@ jobs: - 56312:6379 steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v3.3.0 with: submodules: true # https://github.com/cypress-io/cypress-docker-images/issues/150 @@ -85,13 +77,17 @@ jobs: # if: ${{ matrix.browser == 'firefox' }} #- uses: browser-actions/setup-firefox@latest # if: ${{ matrix.browser == 'firefox' }} - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 - - name: Use Node.js - uses: actions/setup-node@v4.4.0 + - name: Install pnpm + uses: pnpm/action-setup@v2 with: - node-version-file: '.node-version' + version: 7 + run_install: false + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3.6.0 + with: + node-version: ${{ matrix.node-version }} cache: 'pnpm' + - run: corepack enable - run: pnpm i --frozen-lockfile - name: Copy Configure run: cp .github/misskey/test.yml .config @@ -105,20 +101,19 @@ jobs: - name: Cypress install run: pnpm exec cypress install - name: Cypress run - uses: cypress-io/github-action@v6 - timeout-minutes: 15 + uses: cypress-io/github-action@v5 with: install: false start: pnpm start:test wait-on: 'http://localhost:61812' headed: true browser: ${{ matrix.browser }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v2 if: failure() with: name: ${{ matrix.browser }}-cypress-screenshots path: cypress/screenshots - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v2 if: always() with: name: ${{ matrix.browser }}-cypress-videos diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml index f6d16bbd76..b15e704c7f 100644 --- a/.github/workflows/test-misskey-js.yml +++ b/.github/workflows/test-misskey-js.yml @@ -6,31 +6,29 @@ name: Test (misskey.js) on: push: branches: [ develop ] - paths: - - packages/misskey-js/** - - .github/workflows/test-misskey-js.yml pull_request: branches: [ develop ] - paths: - - packages/misskey-js/** - - .github/workflows/test-misskey-js.yml + jobs: test: - name: Unit tests (misskey.js) runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v3.3.0 - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 + - run: corepack enable - - name: Setup Node.js - uses: actions/setup-node@v4.4.0 + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3.6.0 with: - node-version-file: '.node-version' + node-version: ${{ matrix.node-version }} cache: 'pnpm' - name: Install dependencies @@ -48,7 +46,7 @@ jobs: CI: true - name: Upload Coverage - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./packages/misskey-js/coverage/coverage-final.json diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml index 751c374608..5243a83777 100644 --- a/.github/workflows/test-production.yml +++ b/.github/workflows/test-production.yml @@ -12,20 +12,27 @@ env: jobs: production: - name: Production build runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v3.3.0 with: submodules: true - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 - - name: Use Node.js - uses: actions/setup-node@v4.4.0 + - name: Install pnpm + uses: pnpm/action-setup@v2 with: - node-version-file: '.node-version' + version: 8 + run_install: false + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3.6.0 + with: + node-version: ${{ matrix.node-version }} cache: 'pnpm' + - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml deleted file mode 100644 index edff7dbecb..0000000000 --- a/.github/workflows/validate-api-json.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: api.json validation - -on: - push: - branches: - - master - - develop - paths: - - packages/backend/** - - .github/workflows/validate-api-json.yml - pull_request: - paths: - - packages/backend/** - - .github/workflows/validate-api-json.yml -jobs: - validate-api-json: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4.2.2 - with: - submodules: true - - name: Setup pnpm - uses: pnpm/action-setup@v4.1.0 - - name: Use Node.js - uses: actions/setup-node@v4.4.0 - with: - node-version-file: '.node-version' - cache: 'pnpm' - - name: Install Redocly CLI - run: npm i -g @redocly/cli - - run: pnpm i --frozen-lockfile - - name: Check pnpm-lock.yaml - run: git diff --exit-code pnpm-lock.yaml - - name: Copy Configure - run: cp .config/example.yml .config/default.yml - - name: Build and generate - run: pnpm build && pnpm --filter backend generate-api-json - - name: Validation - run: npx @redocly/cli lint --extends=minimal ./packages/backend/built/api.json diff --git a/.gitignore b/.gitignore index ac7502f384..537232d37f 100644 --- a/.gitignore +++ b/.gitignore @@ -35,17 +35,12 @@ coverage !/.config/example.yml !/.config/docker_example.yml !/.config/docker_example.env -!/.config/cypress-devcontainer.yml docker-compose.yml -./compose.yml -.devcontainer/compose.yml -!/.devcontainer/compose.yml +!/.devcontainer/docker-compose.yml # misskey /build built -built-test -js-built /data /.cache-loader /db @@ -62,14 +57,6 @@ api-docs.json ormconfig.json temp /packages/frontend/src/**/*.stories.ts -tsdoc-metadata.json -misskey-assets - -# Vite temporary files -vite.config.js.timestamp-* -vite.config.ts.timestamp-* -vite.config.local-dev.js.timestamp-* -vite.config.local-dev.ts.timestamp-* # blender backups *.blend1 @@ -77,6 +64,3 @@ vite.config.local-dev.ts.timestamp-* *.blend3 *.blend4 *.blend5 - -# VSCode addon -.favorites.json diff --git a/.gitmodules b/.gitmodules index 3218575273..225a69a652 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ +[submodule "misskey-assets"] + path = misskey-assets + url = https://github.com/misskey-dev/assets.git [submodule "fluent-emojis"] path = fluent-emojis url = https://github.com/misskey-dev/emojis.git diff --git a/.node-version b/.node-version index b8ffd70759..6d80269a4f 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -22.15.0 +18.16.0 diff --git a/.npmrc b/.npmrc deleted file mode 100644 index daebfd5218..0000000000 --- a/.npmrc +++ /dev/null @@ -1,3 +0,0 @@ -engine-strict = true -save-exact = true -shell-emulator = true diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 3cdf81e339..baca8db246 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,7 +3,9 @@ "editorconfig.editorconfig", "dbaeumer.vscode-eslint", "Vue.volar", + "Vue.vscode-typescript-vue-plugin", "Orta.vscode-jest", + "dbaeumer.vscode-eslint", "mrmlnc.vscode-json5" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 0ceec23acd..71fb02a59d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,11 @@ { - "search.exclude": { - "**/node_modules": true - }, - "typescript.tsdk": "node_modules/typescript/lib", - "files.associations": { - "*.test.ts": "typescript" - }, - "jest.jestCommandLine": "pnpm run jest", - "jest.runMode": "on-demand", - "editor.codeActionsOnSave": { - "source.fixAll": "explicit" - }, - "editor.formatOnSave": false -} + "search.exclude": { + "**/node_modules": true + }, + "typescript.tsdk": "node_modules/typescript/lib", + "files.associations": { + "*.test.ts": "typescript" + }, + "jest.jestCommandLine": "pnpm run jest", + "jest.autoRun": "off" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8add602c81..681105fb7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,1558 +1,21 @@ -## 2025.6.0 - -### Client -- Enhance: 非同期的なコンポーネントの読み込み時のハンドリングを強化 -- Fix: リアクションの一部の絵文字が重複して表示されることがある問題を修正 -- Fix: 非利用者に対するユーザー作成コンテンツの公開範囲が全て非公開になっている場合にログインできない問題を修正 - -### Server -- Fix: 非利用者に対するユーザー作成コンテンツの公開範囲が全て非公開になっている場合でもusers/showを許可するように - -## 2025.5.1 - -### Note -- 設定ファイルの以下の項目がコントロールパネルから設定するようになりました - - signToActivityPubGet - - proxyRemoteFiles - - disallowExternalApRedirect - - 許可しないかどうかではなく、許可するかどうかの設定(allowExternalApRedirect)になりました - -### General -- Feat: 非ログインでサーバーを閲覧された際に、サーバー内のコンテンツを非公開にすることができるようになりました - - モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます - - 「全て公開(今までの挙動)」「ローカルのコンテンツだけ公開(=サーバー内で受信されたリモートのコンテンツは公開しない)」「何も公開しない」から選択できます - - デフォルト値は「ローカルのコンテンツだけ公開」になっています -- Feat: ロールでアップロード可能なファイル種別を設定可能になりました - - デフォルトは**テキスト、JSON、画像、動画、音声ファイル**になっています。zipなど、その他の種別のファイルは含まれていないため、必要に応じて設定を変更してください。 - - 場合によってはファイル種別を正しく検出できないことがあります(特にテキストフォーマット)。その場合、ファイル種別は application/octet-stream と見做されます。 - - したがって、それらの種別不明ファイルを許可したい場合は application/octet-stream を指定に追加してください。 -- Feat: プレビュー先がリダイレクトを伴う場合、リダイレクト先のコンテンツを取得しに行くか否かを設定できるように(#16043) -- Enhance: UIのアイコンデータの読み込みを軽量化 - -### Client -- Feat: ドライブのUIが強化されました - - 複数のファイルをまとめて移動できるようになりました -- Feat: ファイルのアップロードUIが一新されました - - アップロード前にファイル情報を確認できるようになりました - - 圧縮の品質を選択できるようになりました - - アップロードに失敗したときに再試行できるようになりました - - アップロード前に画像のクロッピングを行えるようになりました - - ファイルサイズのチェックは圧縮後の実際にアップロードされるサイズで行われるようになりました - - ファイルのアップロードを中断できるようになりました -- Feat: サーバー初期設定ウィザードが実装されました - - 簡単なウィザードに従うだけで、サーバーに最適な設定が適用されます -- Feat: Websocket接続を行わずにMisskeyを利用するNo Websocketモードが実装されました(beta) - - サーバーのパフォーマンス向上に寄与することが期待されます - - 何らの理由によりWebsocket接続が行えない環境でも快適に利用可能です - - 従来のWebsocket接続を行うモードはリアルタイムモードとして再定義されました - - チャットなど、一部の機能は引き続き設定に関わらずWebsocket接続が行われます -- Feat: 絵文字をミュート可能にする機能 - - 絵文字(ユニコードの絵文字・カスタム絵文字)毎にミュートし、不可視化することができるようになりました -- Feat: モバイルデバイスで折りたたまれたUIの展開表示に全画面ページを使用できるように(実験的) -- Enhance: 設定の同期をオンにするときに競合したときに値をマージできるように -- Enhance: メモリ使用量を軽減しました -- Enhance: 画像の高品質なプレースホルダを無効化してパフォーマンスを向上させるオプションを追加 -- Enhance: 招待されているが参加していないルームを開いたときに、招待を承認するかどうか尋ねるように -- Enhance: リプライ元にアンケートがあることが表示されるように -- Enhance: ノートのサーバー情報のデザインを改善・パフォーマンス向上 - (Based on https://github.com/taiyme/misskey/pull/198, https://github.com/taiyme/misskey/pull/211, https://github.com/taiyme/misskey/pull/283) -- Enhance: ユーザー設定でURLプレビューを無効化できるように -- Enhance: ヒントとコツを追加 -- Enhance: ヒントとコツを再表示できるように -- Enhance: AiScriptからtoastを表示する関数 `Mk:toast` を追加 -- Enhance: シンタックスハイライトのエンジンをJavaScriptベースのものに変更 - - フロントエンドの読み込みサイズを軽量化しました - - ほとんどの言語のハイライトは問題なく行えますが、互換性の問題により一部の言語が正常にハイライトできなくなる可能性があります。詳しくは https://shiki.style/references/engine-js-compat をご覧ください。 -- Fix: チャットに動画ファイルを送付すると、動画の表示が崩れてしまい視聴出来ない問題を修正 -- Fix: アカウント依存かつ初期状態である設定値をサーバー同期しようとした際に正しくコンフリクト検出されない問題を修正 -- Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正 -- Fix: 一定時間操作がなかったら動画プレイヤーのコントロールを隠すように -- Fix: Twitchのクリップがプレイヤーで再生できない問題を修正 - -### Server -- Enhance: リストやフォローをエクスポートする際にリプライを含むかどうかの情報を含むように -- Enhance: チャットルームの最大メンバー数を30人から50人に調整 -- Enhance: ノートのレスポンスにアンケートが添付されているかどうかを示すフラグ`hasPoll`を追加 -- Enhance: チャットルームのレスポンスに招待されているかどうかを示すフラグ`invitationExists`を追加 -- Enhance: レートリミットの計算方法を調整 (#13997) -- Enhance: 外部サイトのOGPのキャッシュ期間を調整 -- Fix: チャットルームが削除された場合・チャットルームから抜けた場合に、未読状態が残り続けることがあるのを修正 -- Fix: ユーザ除外アンテナをインポートできない問題を修正 -- Fix: アンテナのセンシティブなチャンネルのノートを含むかどうかの情報がエクスポートされない問題を修正 -- Fix: ミュート対象ユーザーが引用されているノートがRNされたときにミュートを貫通してしまう問題を修正 #16009 -- Fix: 連合モードが「なし」の場合に、生成されるHTML内のactivity jsonへのリンクタグを省略するように -- Fix: コントロールパネルから招待コードを作成すると作成者の情報が記録されない問題を修正 -- Fix: コントロールパネルのジョブキューページからPausedなジョブ一覧を閲覧できない問題を修正 - -## 2025.5.0 - -### Note -- DockerのNode.jsが22.15.0に更新されました - -### Client -- Feat: マウスで中ボタンドラッグによりタイムラインを引っ張って更新できるように - - アクセシビリティ設定からオフにすることもできます -- Enhance: タイムラインのパフォーマンスを向上 -- Enhance: バックアップされた設定のプロファイルを削除できるように -- Fix: 一部のブラウザでアコーディオンメニューのアニメーションが動作しない問題を修正 -- Fix: ダイアログのお知らせが画面からはみ出ることがある問題を修正 -- Fix: ユーザーポップアップでエラーが生じてもインジケーターが表示され続けてしまう問題を修正 - -### Server -- Enhance: 凍結されたユーザのノートが各種タイムラインで表示されないように `#15775` -- Enhance: 連合先のソフトウェア及びバージョン名により配信停止を行えるように `#15727` -- Enhance: 2025.4.1 で追加されたインデックスの再生成をノートの追加しながら行えるようになりました。 `#15915` - - `MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY` 環境変数を `1` にセットしていると、巨大なテーブルの既存のカラムに関するインデックス再生成が`CREATE INDEX CONCURRENTLY`を使用するようになりました。 - - 複数のサーバープロセスをクラスタリングしているサーバーにおいて、一部のプロセスが起動している状態でこのオプションを有効にしてマイグレーションすることにより、ダウンタイムを削減することができます。 - - ただし、このオプションを有効にする場合、インデックスの作成にかかる時間が倍~3倍以上になることがあります。 - - また、大きなインスタンスである場合にはインデックスの作成に失敗し、複数回再試行する必要がある可能性があります。 -- Fix: チャンネルのフォロー一覧の結果が一部正しくないのを修正 (#12175) -- Fix: ファイルをアップロードした際にファイル名が常に untitled になる問題を修正 -- Fix: ファイルのアップロードに失敗することがある問題を修正 - - 投稿フォーム上で画像のクロップを行うと、`Invalid Param.`エラーでノートが投稿出来なくなる問題も解決されます。 - - この事象によって既にノートが投稿出来ない状態になっている場合は、投稿フォーム右上のメニューから、下書きデータの「リセット」を行ってください。 - -## 2025.4.1 - -### General -- Feat: bull-boardに代わるジョブキューの管理ツールが実装されました -- Feat: アップロード可能な最大ファイルサイズをロールごとに設定可能に - - デフォルトで10MBになっています -- Enhance: チャットの新規メッセージをプッシュ通知するように -- Enhance: サーバーブロックの対象になっているサーバーについて、当該サーバーのユーザーや既知投稿を見えないように -- Enhance: 依存関係の更新 -- Enhance: 翻訳の更新 -- Fix: セキュリティに関する修正 - -### Client -- Feat: チャットウィジェットを追加 -- Feat: デッキにチャットカラムを追加 -- Feat: タイトルバーを表示できるように -- Enhance: Unicode絵文字をslugから入力する際に`:ok:`のように最後の`:`を入力したあとにUnicode絵文字に変換できるように -- Enhance: コントロールパネルでジョブキューをクリアできるように -- Enhance: テーマでページヘッダーの色を変更できるように -- Enhance: スワイプでのタブ切り替えを強化 -- Enhance: デザインのブラッシュアップ -- Fix: ログアウトした際に処理が終了しない問題を修正 -- Fix: 自動バックアップが設定されている環境でログアウト直前に設定をバックアップするように -- Fix: フォルダを開いた状態でメニューからアップロードしてもルートフォルダにアップロードされる問題を修正 #15836 -- Fix: タイムラインのスクロール位置を記憶するように修正 -- Fix: ノートの直後のノートを表示する機能で表示が逆順になっていた問題を修正 #15841 -- Fix: アカウントの移行時にアンテナのフィルターのユーザが更新されない問題を修正 #15843 -- Fix: タイムラインでノートが重複して表示されることがあるのを修正 - -### Server -- Enhance: ジョブキューの成功/失敗したジョブも一定数・一定期間保存するようにし、後から問題を調査することを容易に -- Enhance: フォローしているユーザーならフォロワー限定投稿のノートでもアンテナで検知できるように - (Cherry-picked from https://github.com/yojo-art/cherrypick/pull/568 and https://github.com/team-shahu/misskey/pull/38) -- Enhance: ユーザーごとにノートの表示が高速化するように -- Fix: システムアカウントの名前がサーバー名と同期されない問題を修正 -- Fix: 大文字を含むユーザの URL で照会された場合に 404 エラーを返す問題 #15813 -- Fix: リードレプリカ設定時にレコードの追加・更新・削除を伴うクエリを発行した際はmasterノードで実行されるように調整( #10897 ) -- Fix: ファイルアップロード時の挙動を一部調整(#15895) - -## 2025.4.0 - -### General -- Feat: チャット(ダイレクトメッセージ)がリニューアルして復活しました - - 既存のDM機能よりも便利で効率的な実装になっています - - チャットを受け付ける相手を制限可能です - - 誰でも / フォローユーザーのみ / フォロワーのみ / 相互のみ / 受け付けない から選択できます - - 自分からメッセージを送った相手とは上記の設定に関わらずチャット可能です - - チャット機能を開放するかどうかをロールで制御可能です - - ルームを作成して、複数人でのチャットも可能です - - 過去自分が送ったメッセージ・自分に送られたメッセージの検索が可能です - - 参加中のルームをミュートして通知が来ないように設定可能です - - メッセージにはリアクションも可能です - - 現在、リモートユーザーがチャットを受け付ける設定になっているかどうかを取得する術がないため、ローカルユーザー間でのみ利用可能です -- Feat: アカウントの移行時に古いアカウントからあたらしいアカウントにロールをコピーできるようになりました。 - - 管理者がロールの設定でマイグレーション時にコピーするかを指定できるようになります。 -- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。 - - Misskeyネイティブでダッシュボードを実装予定です -- Enhance: フロントエンドのエラートラッキングができるように - - `.config/default.yml`中の項目`sentryForFrontend`を適宜設定してください。 - - 外部サービスであるSentryへエラー情報が送信されます。ご利用の地域の法令に従い、適切なプライバシーポリシーを策定の上で運用してください。 -- Enhance: ミュートしているユーザーをユーザー検索の結果から除外するように -- Enhance: アンテナでセンシティブなチャンネルのノートを除外できるように `#14177` -- Fix: 通知のページネーションで2つ以上読み込めなくなることがある問題を修正 - -### Client -- Feat: 設定の管理が強化されました - - 内部処理が一新され、安定性とパフォーマンスが向上しました - - 全てのクライアント設定がエクスポート(バックアップ)/インポート対象に含まれるようになりました - - プラグイン、テーマ、クライアントに追加されたすべてのアカウント情報も含まれるようになりました - - 自動で設定データをサーバーにバックアップできるように - - 設定→設定のプロファイル→自動バックアップ で有効にできます - - ログインしたとき、ブラウザから設定データが消えてしまったときに自動で復元されます(復元をスキップすることも可能) - - 任意の設定項目をデバイス間で同期できるように - - 設定項目の「...」メニュー→「デバイス間で同期」 - - 同期をオンにした際にサーバーに保存された値とローカルの値が競合する場合はどちらを優先するか選択できます - - 任意の設定項目を初期値にリセットできるように - - 設定項目の「...」メニュー→「初期値にリセット」 - - アカウントごとに設定値が分離される設定とそうでないクライアント設定が混在していた(かつ分離するかどうかを設定不可だった)のを、基本的に一律でクライアント全体に適用されるようにし、個別でアカウントごとに異なる設定を行えるように - - 設定項目の「...」メニュー→「アカウントで上書き」をオンにすることで、設定値をそのアカウントでだけ適用するようにできます - - ログアウトすると設定データもブラウザから消去されるようになりプライバシーが向上しました - - バックアップを有効にしている場合、ログインした後にバックアップから設定データを復元可能です - - エクスポートした設定データを他のサーバーでインポートして適用すること(設定の持ち運び)が可能になりました - - 設定情報の移行は自動で行われますが、何らかの理由で失敗した場合、設定→その他→旧設定情報を移行 で再試行可能です - - 過去に作成されたバックアップデータとは現在互換性がありませんのでご注意ください -- Feat: 画面を重ねて表示するオプションを実装(実験的) - - 設定 → その他 → 実験的機能 → Enable stacking router view -- Enhance: プラグインの管理が強化されました - - インストール/アンインストール/設定の変更時にリロード不要になりました -- Enhance: ログアウト時、ブラウザに保存されたWebクライアントのデータを全て消去するように -- Enhance: デッキUIでカラム間のマージンを設定できるように -- Enhance: デッキUIでデッキメニューの位置を設定できるように -- Enhance: デッキUIでナビゲーションバーの位置を設定できるように -- Enhance: アイコンのスクロール追従を無効化してパフォーマンス向上できるように -- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに -- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように -- Enhance: テーマ設定画面のデザインを改善 -- Enhance: 投稿フォームの設定メニューを改良 - - 投稿フォームをリセットできるように - - 文字数カウントを復活 -- Enhance: 2段階認証時のリカバリーコードのファイル名にサーバーURLを含めるように -- Enhance: 全体的なブラッシュアップ -- Enhance 全体的なパフォーマンス向上 -- Enhance: ファイルのアップロードでデフォルトで圧縮するかどうかのオプションが廃止され、アップロード時に圧縮するかどうかを選択するようになりました - - 画像データの貼り付け、ドロップ時は圧縮されるようになりました -- Fix: 読み込み直後にスクロールしようとすると途中で止まる場合があるのを修正 -- Fix: テーマ切り替え時に一部の色が変わらない問題を修正 -- Fix: iPadOSでdeck uiをマウスカーソルによってスクロールできない問題を修正 -- NOTE: 構造上クラシックUIを新しいデザインシステムに移行することが困難なため、クラシックUIが削除されました - - デッキUIでカラムを中央寄せにし、メインカラムの左右にウィジェットカラムを配置し、ナビゲーションバーを上部に表示することである程度クラシックUIを再現できます - -### Server -- Enhance 全体的なパフォーマンス向上 -- Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正 -- Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正 -- Fix: 連合無しモードでも外部から照会可能だった問題を修正 -- Fix: テスト用WebHookのペイロードの`emojis`パラメータが実際のものと異なる問題を修正 -- Fix: 非ログインでタイムラインのストリームに接続した際、表示にログイン必須のノートが流れる場合がある問題を修正 - -## 2025.3.1 - -### General -- pnpmをv10に更新 -- Corepackを削除 - -### Client -- Feat: 設定の検索を追加(実験的) -- Enhance: 設定項目の再配置 - -### Server -- Fix: DBマイグレーション際にシステムアカウントのユーザーID判定が正しくない問題を修正 -- Fix: user.featured列が状況によってJSON文字列になっていたのを修正 - - -## 2025.3.0 - -### General -- Enhance: プロキシアカウントをシステムアカウントとして作成するように -- Enhance: OAuthで外部アプリからロゴが提供されている場合、それを表示できるように - 書式は https://indieauth.spec.indieweb.org/20220212/#example-2 に準じます。 -- Fix: システムアカウントが削除できる問題を修正 - -### Client -- Enhance: モデレーターがセンシティブ設定を変更する際に確認ダイアログを出すように -- Enhance: 「UIのアニメーションを減らす」で画面上のエフェクトも減らせるように -- Enhance: 投稿フォームにおける、メディアの添付可能個数のカウントを反転しました - - これまでの表示は`添付可能残り個数/上限数`でしたが、`添付個数/上限数`としました -- Fix: フォローされたときのメッセージがちらつくことがある問題を修正 -- Fix: 投稿ダイアログがサイズ限界を超えた際にスクロールできない問題を修正 - -### Server -- Fix: 特定のケースでActivityPubの処理がデッドロックになることがあるのを修正 -- Fix: S3互換オブジェクトストレージでファイルのアップロードに失敗することがある問題を修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/895) - - -## 2025.2.1 - -### General -- Feat: アクセストークン発行時に通知するように -- Feat: 実験的なGoogleAnalyticsサポートを追加 -- 依存関係の更新 - -### Client -- Feat: 投稿フォームで画像をプレビュー可能に -- Enhance: 投稿フォームの「迷惑になる可能性があります」のダイアログを表示する条件においてCWを考慮するように -- Enhance: アンテナ、リスト等の名前をカラム名のデフォルト値にするように `#13992` -- Enhance: クライアントエラー画面の多言語対応 -- Enhance: 開発者モードでメニューからファイルIDをコピー出来るように `#15441' -- Enhance: ノートに埋め込まれたメディアのコンテキストメニューから管理者用のファイル管理画面を開けるように ( #15440 ) -- Enhance: リアクションする際に確認ダイアログを表示できるように -- Enhance: コントロールパネルのユーザ検索で入力された情報をページ遷移で損なわないように `#15437` -- Enhance: CWの注釈で入力済みの文字数を表示 -- Enhance: ノート検索ページのデザイン調整 - (Cherry-picked from https://github.com/taiyme/misskey/pull/273) -- Fix: ノートページで、クリップ一覧が表示されないことがある問題を修正 -- Fix: コンディショナルロールを手動で割り当てできる導線を削除 `#13529` -- Fix: 埋め込みプレイヤーから外部ページに移動できない問題を修正 -- Fix: Play の再読込時に UI が以前の状態を引き継いでしまう問題を修正 `#14378` -- Fix: カスタム絵文字管理画面(beta)にてisSensitive/localOnlyの絞り込みが上手くいかない問題の修正 ( #15445 ) -- Fix: ユーザのサジェスト中に@を入力してもサジェスト結果が消えないように `#14385` -- Fix: CWの注釈が100文字を超えている場合、ノート投稿ボタンを非アクティブに -- Fix: テーマ選択で現在のテーマが初期表示されていない問題を修正 -- 翻訳の更新 - -### Server -- Enhance: 成り済まし対策として、ActivityPub照会された時にリモートのリダイレクトを拒否できるように (config.disallowExternalApRedirect) -- Fix: `following/invalidate`でフォロワーを解除しようとしているユーザーの情報を返すように -- Fix: オブジェクトストレージの設定でPrefixを設定していなかった場合nullまたは空文字になる問題を修正 -- Fix: HTTPプロキシとその除外設定を行った状態でカスタム絵文字の一括インポートをしたとき、除外設定が効かないのを修正( #8766 ) -- Fix: pgroongaでの検索時にはじめのキーワードのみが検索に使用される問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/886) -- Fix: メールアドレスの形式が正しくなければ以降の処理を行わないように -- Fix: `update-meta`でobjectStoragePrefixにS3_SAFEかつURL-safeでない文字列を使えないように -- Fix: クリップの説明欄を更新する際に空にできない問題を修正 -- Fix: フォロワーではないユーザーにリノートもしくは返信された場合にノートのDeleteアクティビティが送られていない問題を修正 - -## 2025.2.0 - -### General -- Fix: Docker のビルドに失敗する問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/883) - -### Client -- Fix: パスキーでパスワードレスログインが出来ない問題を修正 -- Fix: 一部環境でセンシティブなファイルを含むノートの非表示が効かない問題 -- Fix: データセーバー有効時にもユーザーページの「ファイル」タブで画像が読み込まれてしまう問題を修正 -- Fix: MFMの `sparkle` エフェクトが正しく表示されない問題を修正 -- Fix: ページのURLにスラッシュが含まれている場合にページが正しく表示されない問題を修正 -- Fix: デッキのプロファイルが新規作成できない問題を修正 -- Fix: セキュリティに関する修正 -- ローカライゼーションの更新 -- Playが実装されたため、ページ機能の「ソースを見る」は削除されました - -### Server -- Enhance: ページのURLに使用可能な文字を限定するように -- Fix: 個別お知らせページのmetaタグ出力の条件が間違っていたのを修正 - -## 2025.1.0 - -### Note -- [重要] ノート検索プロバイダの追加に伴い、configファイル(default.ymlなど)の構成が少し変わります. - - 新しい設定項目"fulltextSearch.provider"が追加されました. sqlLike, sqlPgroonga, meilisearchのいずれかを設定出来ます. - - すでにMeilisearchをお使いの場合、 **"fulltextSearch.provider"を"meilisearch"に設定する必要** があります. - - 詳細は #14730 および `.config/example.yml` または `.config/docker_example.yml`の'Fulltext search configuration'をご参照願います. -- 【開発者向け】従来の開発モードでHMRが機能しない問題が修正されたため、バックエンド・フロントエンド分離型の開発モードが削除されました。開発環境においてconfigの変更が必要となる可能性があります。 - -### General -- Feat: カスタム絵文字管理画面をリニューアル #10996 - * β版として公開のため、旧画面も引き続き利用可能です - -### Client -- Enhance: PC画面でチャンネルが複数列で表示されるように - (Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13) -- Enhance: 照会に失敗した場合、その理由を表示するように -- Enhance: ワードミュートで検知されたワードを表示できるように -- Enhance: リモートのノートのリンクをコピーできるように -- Enhance: 連合がホワイトリスト化・無効化されているサーバー向けのデザイン修正 -- Enhance: AiScriptのセーブデータを明示的に削除する関数`Mk:remove`を追加 -- Enhance: ノートの添付ファイルを一覧で遡れる「ファイル」タブを追加 - (Based on https://github.com/Otaku-Social/maniakey/pull/14) -- Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に -- Enhance: クエリパラメータでuiを一時的に変更できるように #15240 -- Enhance: リモート絵文字のインポート時に詳細を確認できるように #15336 -- Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正 -- Fix: サーバー情報メニューに区切り線が不足していたのを修正 -- Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正 -- Fix: 公開範囲がホームのノートの埋め込みウィジェットが読み込まれない問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/803) -- Fix: 絵文字管理画面で一部の絵文字が表示されない問題を修正 -- Fix: プラグイン `register_note_view_interruptor` でノートのサーバー情報の書き換えができない問題を修正 -- Fix: Botプロテクションの設定変更時は実際に検証を通過しないと保存できないように( #15137 ) -- Fix: ノート検索が使用できない場合でもチャンネルのノート検索欄がでていた問題を修正 -- Fix: `Ui:C:select`で値の変更が画面に反映されない問題を修正 -- Fix: MiAuth認可画面で、認可処理に失敗した場合でもコールバックURLに遷移してしまう問題を修正 - (Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4) -- Fix: ノート作成画面でファイルの添付可能個数を超えてもノートボタンが押せていた問題を修正 -- Fix: 「アカウントを管理」画面で、ユーザー情報の取得に失敗したアカウント(削除されたアカウントなど)が表示されない問題を修正 -- Fix: MacOSでChrome系ブラウザを使用している場合に、Misskeyを閉じた際に他のタブのオーディオ機能と干渉する問題を修正 -- Fix: 言語データのキャッシュ状況によっては、埋め込みウィジェットが正しく起動しない問題を修正 -- Fix: 「削除して編集」でノートの引用を解除出来なかった問題を修正( #14476 ) -- Fix: RSSウィジェットが正しく表示されない問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/857) -- Fix: ワードミュートの保存失敗時にAPIエラーが握りつぶされる事があるのを修正 -- Fix: アンケートでリモートの絵文字が正しく描画できない問題の修正 - (Cherry-picked from https://github.com/yojo-art/cherrypick/pull/153) -- Fix: 非ログイン時のサーバー概要画面のメニューボタンが押せないことがあるのを修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/656) -- Fix: URLにはじめから`#pswp`が含まれている場合に画像ビューワーがブラウザの戻るボタンで閉じられない問題を修正 -- Fix: ロール作成画面で設定できるアイコンデコレーションの最大取付個数を16に制限 -- Fix: Firefox Nightlyなどでアイコンが読み込めない問題を修正 - -### Server -- Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように -- Enhance: ノート検索の選択肢としてpgroongaに対応 ( #14730 ) -- Enhance: チャート更新時にDBに同時接続しないように - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/830) -- Enhance: config(default.yml)からSQLログ全文を出力するか否かを設定可能に ( #15266 ) -- Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 ) -- Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737) -- Fix: ノートの閲覧にログイン必須にしてもFeedでノートが表示されてしまう問題を修正 -- Fix: 絵文字の連合でライセンス欄を相互にやり取りするように ( #10859, #14109 ) -- Fix: ロックダウンされた期間指定のノートがStreaming経由でLTLに出現するのを修正 ( #15200 ) -- Fix: disableClustering設定時の初期化ロジックを調整( #15223 ) -- Fix: URLとURIが異なるエンティティの照会に失敗する問題を修正( #15039 ) -- Fix: ActivityPubリクエストかどうかの判定が正しくない問題を修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/869) -- Fix: `/api/pages/update`にて`name`を指定せずにリクエストするとエラーが発生する問題を修正 -- Fix: AIセンシティブ判定が arm64 環境で動作しない問題を修正 -- Fix: 非Misskey系のソフトウェアからHTML``タグを含むノートを受信した場合、MFMの読み仮名(ルビ)文法に変換して表示 -- Fix: 連合OFFで投稿されたノートに対する冗長な処理を抑止 ( #15018 ) -- Fix: `/api.json`のレスポンスが2回目のリクエスト以降おかしくなる問題を修正 - -### Misskey.js -- Feat: allow setting `binaryType` of WebSocket connection - -## 2024.11.0 - -### Note -- Node.js 20.xは非推奨になりました。Node.js 22.x (LTS)の利用を推奨します。 - - なお、Node.js 23.xは対応していません。 -- DockerのNode.jsが22.11.0に更新されました - -### General -- Feat: コンテンツの表示にログインを必須にできるように -- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように -- Enhance: 依存関係の更新 -- Enhance: l10nの更新 -- Fix: お知らせ作成時に画像URL入力欄を空欄に変更できないのを修正 ( #14976 ) - -### Client -- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/751) -- Enhance: ドライブでソートができるように -- Enhance: アイコンデコレーション管理画面の改善 -- Enhance: 「単なるラッキー」の取得条件を変更 -- Enhance: 投稿フォームでEscキーを押したときIME入力中ならフォームを閉じないように( #10866 ) -- Enhance: MiAuth, OAuthの認可画面の改善 - - どのアカウントで認証しようとしているのかがわかるように - - 認証するアカウントを切り替えられるように -- Enhance: Self-XSS防止用の警告を追加 -- Enhance: カタルーニャ語 (ca-ES) に対応 -- Enhance: 個別お知らせページではMetaタグを出力するように -- Enhance: ノート詳細画面にロールのバッジを表示 -- Enhance: 過去に送信したフォローリクエストを確認できるように - (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/663) -- Enhance: サイドバーを簡単に展開・折りたたみできるように ( #14981 ) -- Enhance: リノートメニューに「リノートの詳細」を追加 -- Enhance: 非ログイン状態でMisskeyを開いた際のパフォーマンスを向上 -- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正 -- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768) -- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正 -- Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used -- Fix: リンク切れを修正 -- Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正 - (Cherry-picked from https://github.com/taiyme/misskey/pull/305) -- Fix: メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正 -- Fix: 画面幅が狭い環境でデザインが崩れる問題を修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/815) -- Fix: TypeScriptの型チェック対象ファイルを限定してビルドを高速化するように - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/725) - -### Server -- Enhance: DockerのNode.jsを22.11.0に更新 -- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように - (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588) - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715) -- Enhance: リモートユーザーの照会をオリジナルにリダイレクトするように -- Fix: sharedInboxが無いActorに紐づくリモートユーザーを照会できない -- Fix: Aproving request from GtS appears with some delay -- Fix: フォロワーへのメッセージの絵文字をemojisに含めるように -- Fix: Nested proxy requestsを検出した際にブロックするように - [ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236) -- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706) -- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/711) -- Fix: ローカルユーザーへのメンションを含むノートが連合される際に正しいURLに変換されないことがある問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/712) -- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709) -- Fix: User Webhookテスト機能のMock Payloadを修正 -- Fix: アカウント削除のモデレーションログが動作していないのを修正 (#14996) -- Fix: リノートミュートが新規投稿通知に対して作用していなかった問題を修正 -- Fix: Inboxの処理で生じるエラーを誤ってActivityとして処理することがある問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/730) -- Fix: セキュリティに関する修正 - -### Misskey.js -- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正 - -## 2024.10.1 - -### Note -- スパム対策として、モデレータ権限を持つユーザのアクティビティが7日以上確認できない場合は自動的に招待制へと切り替え(コントロールパネル -> モデレーション -> "誰でも新規登録できるようにする"をオフに変更)るようになりました。 ( #13437 ) - - 切り替わった際はモデレーターへお知らせとして通知されます。登録をオープンな状態で継続したい場合は、コントロールパネルから再度設定を行ってください。 - -### General -- Feat: ユーザーの名前に禁止ワードを設定できるように - -### Client -- Enhance: タイムライン表示時のパフォーマンスを向上 -- Enhance: アーカイブした個人宛のお知らせを表示・編集できるように -- Enhance: l10nの更新 -- Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正 - -### Server -- Feat: モデレータ権限を持つユーザが全員7日間活動しなかった場合は自動的に招待制へと切り替えるように ( #13437 ) -- Enhance: 個人宛のお知らせは「わかった」を押すと自動的にアーカイブされるように -- Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正 -- Fix: RBT有効時、リノートのリアクションが反映されない問題を修正 -- Fix: キューのエラーログを簡略化するように - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/649) - -## 2024.10.0 - -### Note -- セキュリティ向上のため、サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません) - - ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`setupPassword`をランダムな値に設定し、ユーザーに通知するようにシステムを更新することをおすすめします。 - - なお、初期パスワードが設定されていない場合でも初期設定を行うことが可能です(UI上で初期パスワードの入力欄を空欄にすると続行できます)。 -- ユーザーデータを読み込む際の型が一部変更されました。 - - `twoFactorEnabled`, `usePasswordLessLogin`, `securityKeys`: 自分とモデレーター以外のユーザーからは取得できなくなりました - -### General -- Feat: サーバー初期設定時に初期パスワードを設定できるように -- Feat: 通報にモデレーションノートを残せるように -- Feat: 通報の解決種別を設定できるように -- Enhance: 通報の解決と転送を個別に行えるように -- Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました -- Enhance: 依存関係の更新 -- Enhance: l10nの更新 -- Enhance: Playの「人気」タブで10件以上表示可能に #14399 -- Fix: 連合のホワイトリストが正常に登録されない問題を修正 - -### Client -- Enhance: デザインの調整 -- Enhance: ログイン画面の認証フローを改善 -- Fix: クライアント上での時間ベースの実績獲得動作が実績獲得後も発動していた問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/657) - -### Server -- Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように -- Enhance: 自分とモデレーター以外のユーザーから二要素認証関連のデータが取得できないように -- Enhance: 通報および通報解決時に送出されるSystemWebhookにユーザ情報を含めるように ( #14697 ) -- Fix: `admin/abuse-user-reports`エンドポイントのスキーマが間違っていた問題を修正 - -## 2024.9.0 - -### General -- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能 - - 埋め込みコードやウェブサイトへの実装方法の詳細は https://misskey-hub.net/docs/for-users/features/embed/ をご覧ください -- Feat: パスキーでログインボタンを実装 (#14574) -- Feat: フォローされた際のメッセージを設定できるように -- Feat: 連合をホワイトリスト制にできるように -- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445) -- Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680) -- Feat: データエクスポートが完了した際に通知を発行するように -- Enhance: ユーザーによるコンテンツインポートの可否をロールポリシーで制御できるように -- Enhance: 依存関係の更新 -- Enhance: l10nの更新 - -### Client -- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように -- Enhance: アイコンデコレーション管理画面にプレビューを追加 -- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく -- Enhance: ScratchpadにUIインスペクターを追加 -- Enhance: Play編集画面の項目の並びを少しリデザイン -- Enhance: 各種メニューをドロワー表示するかどうか設定可能に -- Enhance: AiScriptのMk:C:containerのオプションに`borderStyle`と`borderRadius`を追加 -- Enhance: CWでも絵文字をクリックしてメニューを表示できるように -- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正 -- Fix: コントロールパネル内のAp requests内のチャートの表示がおかしかった問題を修正 -- Fix: 月の違う同じ日はセパレータが表示されないのを修正 -- Fix: タッチ画面でレンジスライダーを操作するとツールチップが複数表示される問題を修正 - (Cherry-picked from https://github.com/taiyme/misskey/pull/265) -- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725) -- Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正 -- Fix: ファイルの詳細ページのファイルの説明で改行が正しく表示されない問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/bde6bb0bd2e8b0d027e724d2acdb8ae0585a8110) -- Fix: 一部画面のページネーションが動作しにくくなっていたのを修正 ( #12766 , #11449 ) - -### Server -- Feat: Misskey® Reactions Boost Technology™ (RBT)により、リアクションの作成負荷を低減することが可能に -- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように - - この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます -- Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 -- Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8) -- Fix: Continue importing from file if single emoji import fails -- Fix: `Retry-After`ヘッダーが送信されなかった問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/8a982c61c01909e7540ff1be9f019df07c3f0624) -- Fix: サーバーサイドのDOM解析完了時にリソースを開放するように - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/634) -- Fix: ``を追って照会するのはOKレスポンスが返却された場合のみに - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/633) -- Fix: メールにスタイルが適用されていなかった問題を修正 - -## 2024.8.0 - -### General -- Enhance: モデレーターはすべてのユーザーのフォロー・フォロワーの一覧を見られるように -- Enhance: アカウントの削除のモデレーションログを残すように -- Enhance: 不適切なページ、ギャラリー、Playを管理者権限で削除できるように -- Fix: リモートユーザのフォロー・フォロワーの一覧が非公開設定の場合も表示できてしまう問題を修正 - -### Client -- Enhance: 「自分のPlay」ページにおいてPlayが非公開かどうかが一目でわかるように -- Enhance: 不適切なページ、ギャラリー、Playを通報できるように -- Fix: Play編集時に公開範囲が「パブリック」にリセットされる問題を修正 -- Fix: ページ遷移に失敗することがある問題を修正 -- Fix: iOSでユーザー名などがリンクとして誤検知される現象を抑制 -- Fix: mCaptchaを使用していてもbotプロテクションに関する警告が消えないのを修正 -- Fix: ユーザーのモデレーションページにおいてユーザー名にドットが入っているとシステムアカウントとして表示されてしまう問題を修正 -- Fix: 特定の条件下でノートの削除ボタンが出ないのを修正 - -### Server -- Enhance: 照会時にURLがhtmlかつheadタグ内に`rel="alternate"`, `type="application/activity+json"`の`link`タグがある場合に追ってリンク先を照会できるように -- Enhance: 凍結されたアカウントのフォローリクエストを表示しないように -- Fix: WSの`readAllNotifications` メッセージが `body` を持たない場合に動作しない問題 #14374 - - 通知ページや通知カラム(デッキ)を開いている状態において、新たに発生した通知が既読されない問題が修正されます。 - - これにより、プッシュ通知が有効な同条件下の環境において、プッシュ通知が常に発生してしまう問題も修正されます。 -- Fix: Play各種エンドポイントの返り値に`visibility`が含まれていない問題を修正 -- Fix: サーバー情報取得の際にモデレーター限定の情報が取得できないことがあるのを修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/582) -- Fix: 公開範囲がダイレクトのノートをユーザーアクティビティのチャート生成に使用しないように - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/679) -- Fix: ActivityPubのエンティティタイプ判定で不明なタイプを受け取った場合でも処理を継続するように - - キュー処理のつまりが改善される可能性があります -- Fix: リバーシの対局設定の変更が反映されないのを修正 -- Fix: 無制限にストリーミングのチャンネルに接続できる問題を修正 -- Fix: ベースロールのポリシーを変更した際にモデログに記録されないのを修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/700) -- Fix: Prevent memory leak from memory caches (#14310) -- Fix: More reliable memory cache eviction (#14311) - -## 2024.7.0 - -### Note -- デッキUIの新着ノートをサウンドで通知する機能の追加(v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。 -- Streaming APIにて入力が不正な場合にはそのメッセージを無視するようになりました。 #14251 - -### General -- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705 -- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に - - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます -- Feat: ユーザ作成時にSystemWebhookを送信可能に #14281 -- Feat: メディアサイレンスを実装 #13842 - - メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。 -- Enhance: 管理画面でアーカイブにしたお知らせを表示・編集できるように -- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正 -- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題 -- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正 -- 翻訳の更新 -- 依存関係の更新 - -### Client -- Feat: ユーザーページから「このユーザーのノートを検索」できるように (#14128) -- Feat: 検索ページはクエリを受け付けるようになりました (#14128) -- Enhance: 検索ページのUI改善 (#14128) -- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善 -- Enhance: 非ログイン時に他サーバーに遷移するアクションを追加 -- Enhance: 非ログイン時のハイライトTLのデザインを改善 -- Enhance: フロントエンドのアクセシビリティ改善 - (Based on https://github.com/taiyme/misskey/pull/226) -- Enhance: サーバー情報ページ・お問い合わせページを改善 - (Cherry-picked from https://github.com/taiyme/misskey/pull/238) -- Enhance: AiScriptを0.19.0にアップデート -- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`) -- Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように -- Enhance: 検索(ノート/ユーザー)で `#` から始まる文字列を入力すると、そのハッシュタグのノート/ユーザー一覧ページが表示できるように -- Enhance: 検索(ノート/ユーザー)において、入力に空白が含まれている場合は照会を行わないように -- Enhance: 検索(ノート/ユーザー)において、照会を行うかどうか、ハッシュタグのノート/ユーザー一覧ページを表示するかどうかの確認ダイアログを出すように -- Enhance: 検索(ノート/ユーザー)で `@` から始まる文字列(`@user@host`など)を入力すると、そのユーザーを照会できるように -- Enhance: ドライブのファイル・フォルダをドラッグしなくても移動できるように - (Cherry-picked from https://github.com/nafu-at/misskey/commit/b89c2af6945c6a9f9f10e83f54d2bcf0f240b0b4, https://github.com/nafu-at/misskey/commit/8a7d710c6acb83f50c83f050bd1423c764d60a99) -- Enhance: デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように -- Enhance: ブラウザのコンテキストメニューを使用できるように -- Enhance: 連合の「連合中」,「購読中」,「配信中」に対してブロックしているサーバー、配信停止しているサーバーを含めないように -- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正 -- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968) -- Fix: リバーシの対局を正しく共有できないことがある問題を修正 -- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正 -- Fix: アンテナの編集画面のボタンに隙間を追加 -- Fix: テーマプレビューが見れない問題を修正 -- Fix: ショートカットキーが連打できる問題を修正 - (Cherry-picked from https://github.com/taiyme/misskey/pull/234) -- Fix: MkSignin.vueのcredentialRequestからReactivityを削除(ProxyがPasskey認証処理に渡ることを避けるため) -- Fix: 「アニメーション画像を再生しない」がオンのときでもサーバーのバナー画像・背景画像がアニメーションしてしまう問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/574) -- Fix: Twitchの埋め込みが開けない問題を修正 -- Fix: 子メニューの高さがウィンドウからはみ出ることがある問題を修正 -- Fix: 個人宛てのダイアログ形式のお知らせが即時表示されない問題を修正 -- Fix: 一部の画像がセンシティブ指定されているときに画面に何も表示されないことがあるのを修正 -- Fix: リアクションしたユーザー一覧のユーザー名がはみ出る問題を修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/672) -- Fix: `/share`ページにおいて絵文字ピッカーを開くことができない問題を修正 -- Fix: deck uiの通知音が重なる問題 (#14029) -- Fix: ダイレクト投稿の"削除して編集"において、宛先が保持されていなかった問題を修正 -- Fix: 投稿フォームへのURL貼り付けによる引用が下書きに保存されていなかった問題を修正 -- Fix: "削除して編集"や下書きにおいて、リアクションの受け入れ設定が保持/保存されていなかった問題を修正 -- Fix: 照会に `#` から始まる文字列を入力してそのハッシュタグのページを表示する際、入力が `#` のみの場合に「指定されたURLに該当するページはありませんでした。」が表示されてしまう問題を修正 -- Fix: 照会に `@` から始まる文字列を入力してユーザーを照会する際、入力が `@` のみの場合に「問題が発生しました」が表示されてしまう問題を修正 -- Fix: 投稿フォームにノートのURLを貼り付けて"引用として添付"した場合、投稿文を空にすることによるRenote化が出来なかった問題を修正 -- Fix: フォロー中のユーザーに関する"TLに他の人への返信を含める"の設定が分かりづらい問題を修正 -- Fix: タイムラインページを開いた時、`TLに他の人への返信を含める`がオフのときに`ファイル付きのみ`をオンにできない問題を修正 -- Fix: deck uiでタイムラインを切り替えた際にTLの設定項目が更新されず、`TLに他の人への返信を含める`のトグルが表示されない問題を修正 -- Fix: ウィジェットのタイムライン選択欄に無効化されたタイムラインが表示される問題を修正 -- Fix: サウンドにドライブの音声を使用している際にドライブの音声が再生できなくなると設定が変更できなくなる問題を修正 - -### Server -- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) -- Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに -- Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに -- Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに -- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに -- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに -- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに -- Enhance: `default.yml`内の`url`, `db.db`, `db.user`, `db.pass`を環境変数から読み込めるように -- Enhance: エンドポイント`api/meta`にプロパティ`noteSearchableScope`が増え、`string`値`local`または`global`を返却します -- Fix: チャート生成時にinstance.suspensionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正 -- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006) -- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036) -- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059) -- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正 -- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正 -- Fix: 空文字列のリアクションはフォールバックされるように -- Fix: リノートにリアクションできないように -- Fix: ユーザー名の前後に空白文字列がある場合は省略するように -- Fix: プロフィール編集時に名前を空白文字列のみにできる問題を修正 -- Fix: ユーザ名のサジェスト時に表示される内容と順番を調整(以下の順番になります) #14149 - 1. フォロー中かつアクティブなユーザ - 2. フォロー中かつ非アクティブなユーザ - 3. フォローしていないアクティブなユーザ - 4. フォローしていない非アクティブなユーザ - - また、自分自身のアカウントもサジェストされるようになりました。 -- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652) -- Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正 -- Fix: FTT有効時にリモートユーザーのノートがHTLにキャッシュされる問題を修正 -- Fix: 一部の通知がローカル上のリモートユーザーに対して行われていた問題を修正 -- Fix: エラーメッセージの誤字を修正 (#14213) -- Fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正 -- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正 - (Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1) -- Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251 -- Fix: `users/search`において `@` から始まる文字列が与えられた際の処理が正しくなかった問題を修正 - - 名前や自己紹介に `@` から始まる文言が含まれるユーザーも検索できるようになります -- Fix: 一部のMisskey以外のソフトウェアからファイルを受け取れない問題 - (Cherry-picked from https://github.com/Secineralyr/misskey.dream/pull/73/commits/652eaff1e8aa00b890d71d2e1e52c263c1e67c76) - - NOTE: `drive_file`の`url`, `uri`, `src`の上限が512から1024に変更されます - Migrationではカラム定義の変更のみが行われます。 - サーバー管理者は各サーバーの必要に応じ`drive_file` `("uri")`に対するインデックスを張りなおすことでより安定しDBの探索が行われる可能性があります。詳細 は [GitHub](https://github.com/misskey-dev/misskey/pull/14323#issuecomment-2257562228)で確認可能です -- Fix: 自分のフォロワー限定投稿に対するリプライがホームタイムラインで見えないことが有る問題を修正 -- Fix: フォローしていないユーザによるフォロワー限定投稿に対するリプライがソーシャルタイムラインで表示されることがある問題を修正 - -### Misskey.js -- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応) -- Feat: `/admin/role/create` のロールポリシーの型を修正 - -## 2024.5.0 - -### Note -- コントロールパネル内にあるサマリープロキシの設定個所がセキュリティから全般へ変更となります。 -- 悪意のある第三者がリモートユーザーになりすましたアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-2vxv-pv3m-3wvj)をご覧ください。 -- 管理者向け権限 `read:admin:show-users` は `read:admin:show-user` に統合されました。必要に応じてAPIトークンを再発行してください。 - -### General -- Feat: エラートラッキングにSentryを使用できるようになりました -- Enhance: URLプレビューの有効化・無効化を設定できるように #13569 -- Enhance: アンテナでBotによるノートを除外できるように - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545) -- Enhance: クリップのノート数を表示するように -- Enhance: コンディショナルロールの条件として以下を新たに追加 (#13667) - - 猫ユーザーか - - botユーザーか - - サスペンド済みユーザーか - - 鍵アカウントユーザーか - - 「アカウントを見つけやすくする」が有効なユーザーか -- Enhance: Goneを出さずに終了したサーバーへの配信停止を自動的に行うように - - もしそのようなサーバーからから配信が届いた場合には自動的に配信を再開します -- Enhance: 配信停止の理由を表示するように -- Enhance: サーバーのお問い合わせ先URLを設定できるようになりました -- Fix: Play作成時に設定した公開範囲が機能していない問題を修正 -- Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正 -- Fix: みつけるのアンケート欄にてチャンネルのアンケートが含まれてしまう問題を修正 - -### Client -- Feat: アップロードするファイルの名前をランダム文字列にできるように -- Feat: 個別のお知らせにリンクで飛べるように - (Based on https://github.com/MisskeyIO/misskey/pull/639) -- Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように -- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように -- Enhance: リアクション・いいねの総数を表示するように -- Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように -- Enhance: 設定>プラグインのページからプラグインの簡易的なログやエラーを見られるように - - 実装の都合により、プラグインは1つエラーを起こした時に即時停止するようになりました -- Enhance: ページのデザインを変更 -- Enhance: 2要素認証(ワンタイムパスワード)の入力欄を改善 -- Enhance: 「今日誕生日のフォロー中ユーザー」ウィジェットを手動でリロードできるように -- Enhance: 映像・音声の再生にブラウザのネイティブプレイヤーを使用できるように -- Enhance: 映像・音声の再生メニューに「再生速度」「ループ再生」「ピクチャインピクチャ」を追加 -- Enhance: 映像・音声の再生にキーボードショートカットが使えるように -- Enhance: ノートについているリアクションの「もっと!」から、リアクションの一覧を表示できるように -- Enhance: リプライにて引用がある場合テキストが空でもノートできるように - - 引用したいノートのURLをコピーしリプライ投稿画面にペーストして添付することで達成できます -- Enhance: フォローするかどうかの確認ダイアログを出せるように -- Enhance: Playを手動でリロードできるように -- Enhance: 通報のコメント内のリンクをクリックした際、ウィンドウで開くように -- Enhance: `Ui:C:postForm` および `Ui:C:postFormButton` に `localOnly` と `visibility` を設定できるように -- Enhance: AiScriptを0.18.0にバージョンアップ -- Enhance: 通常のノートでも、お気に入りに登録したチャンネルにリノートできるように -- Enhance: 長いテキストをペーストした際にテキストファイルとして添付するかどうかを選択できるように -- Enhance: 新着ノートをサウンドで通知する機能をdeck UIに追加しました -- Enhance: コントロールパネルのクイックアクションからファイルを照会できるように -- Enhance: コントロールパネルのクイックアクションから通常の照会を行えるように -- Fix: 一部のページ内リンクが正しく動作しない問題を修正 -- Fix: 周年の実績が閏年を考慮しない問題を修正 -- Fix: ローカルURLのプレビューポップアップが左上に表示される -- Fix: WebGL2をサポートしないブラウザで「季節に応じた画面の演出」が有効になっているとき、Misskeyが起動できなくなる問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/459) -- Fix: ページタイトルでローカルユーザーとリモートユーザーの区別がつかない問題を修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/528) -- Fix: コードブロックのシンタックスハイライトで使用される定義ファイルをCDNから取得するように #13177 - - CDNから取得せずMisskey本体にバンドルする場合は`pacakges/frontend/vite.config.ts`を修正してください。 -- Fix: タイムゾーンによっては、「今日誕生日のフォロー中ユーザー」ウィジェットが正しく動作しない問題を修正 -- Fix: CWのみの引用リノートが詳細ページで純粋なリノートとして誤って扱われてしまう問題を修正 -- Fix: ノート詳細ページにおいてCW付き引用リノートのCWボタンのラベルに「引用」が含まれていない問題を修正 -- Fix: ダイアログの入力で字数制限に違反していてもEnterキーが押せてしまう問題を修正 -- Fix: ダイレクト投稿の宛先が保存されない問題を修正 -- Fix: Playのページを離れたときに、Playが正常に初期化されない問題を修正 -- Fix: ページのOGP URLが間違っているのを修正 -- Fix: リバーシの対局を正しく共有できないことがある問題を修正 -- Fix: 通知をグループ化している際に、人数が正常に表示されないことがある問題を修正 -- Fix: 連合なしの状態の読み書きができない問題を修正 -- Fix: `/share` で日本語等を含むurlがurlエンコードされない問題を修正 -- Fix: ファイルを5つ以上添付してもテキストがないとノートが折りたたまれない問題を修正 - -### Server -- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに -- Enhance: misskey-dev/summaly@5.1.0の取り込み(プレビュー生成処理の効率化) -- Enhance: ドライブのファイルがNSFWかどうか個別に連合されるように (#13756) - - 可能な場合、ノートの添付ファイルのセンシティブ判定がファイル単位になります -- Fix: リモートから配送されたアクティビティにJSON-LD compactionをかける -- Fix: フォローリクエストを作成する際に既存のものは削除するように - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/440) -- Fix: エンドポイント`notes/translate`のエラーを改善 -- Fix: CleanRemoteFilesProcessorService report progress from 100% (#13632) -- Fix: 一部の音声ファイルが映像ファイルとして扱われる問題を修正 -- Fix: リプライのみの引用リノートと、CWのみの引用リノートが純粋なリノートとして誤って扱われてしまう問題を修正 -- Fix: 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/606) -- Fix: Add Cache-Control to Bull Board -- Fix: nginx経由で/files/にRangeリクエストされた場合に正しく応答できないのを修正 -- Fix: 一部のタイムラインのストリーミングでインスタンスミュートが効かない問題を修正 -- Fix: グローバルタイムラインで返信が表示されないことがある問題を修正 -- Fix: リノートをミュートしたユーザの投稿のリノートがミュートされる問題を修正 -- Fix: AP Link等は添付ファイル扱いしないようになど (#13754) -- Fix: FTTが有効かつsinceIdのみを指定した場合に帰って来るレスポンスが逆順である問題を修正 -- Fix: `/i/notifications`に `includeTypes`か`excludeTypes`を指定しているとき、通知が存在するのに空配列を返すことがある問題を修正 -- Fix: 複数idを指定する`users/show`が関係ないユーザを返すことがある問題を修正 -- Fix: `/tags` と `/user-tags` が検索エンジンにインデックスされないように -- Fix: もともとセンシティブではないと連合されていたファイルがセンシティブとして連合された場合にセンシティブとしてそのファイルを扱うように - - センシティブとして連合したファイルは非センシティブとして連合されてもセンシティブとして扱われます - -## 2024.3.1 + -### General -- Enhance: 投稿者のロールに応じて、一つのノートに含むことのできるメンションとダイレクト投稿の宛先の人数に上限を設定できるように - * デフォルトのメンション上限は20アカウントに設定されます。(管理者はベースロールの設定で変更可能です。) - * 連合の問い合わせに応答しないサーバーのリモートユーザーへのメンションは、上限の人数に含めない実装になっています。 -- Enhance: 通知がミュート、凍結を考慮するようになりました -- Enhance: サーバーごとにモデレーションノートを残せるように -- Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加 -- Enhance: 通知の受信設定に「フォロー中またはフォロワー」を追加 -- Enhance: 通知の履歴をリセットできるように -- Fix: ダイレクトなノートに対してはダイレクトでしか返信できないように +## 13.x.x (unreleased) ### Client -- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整 -- Fix: syuilo/misskeyの時代からあるインスタンスが改変されたバージョンであると誤認識される問題 -- Fix: MFMのオートコンプリートが出るべき状況で出ないことがある問題を修正 -- Fix: チャートのラベルが消えている問題を修正 -- Fix: 画面表示後最初の音声再生が爆音になることがある問題を修正 -- Fix: 設定のバックアップ作成時に名前を入力しなかった場合、ローカライゼーションがおかしくなる問題を修正 -- Fix: ページ`/admin/emojis`の絵文字編集ダイアログで「リアクションとして使えるロール」を追加する際に何も選択せずOKを押下すると画面が固まる問題を修正 -- Fix: 絵文字サジェストの順位で、絵文字自体の名前が同じものよりもタグで一致しているものが優先されてしまう問題を修正 -- Fix: ユーザの情報のポップアップが消えなくなることがある問題を修正 - -### Server -- Enhance: エンドポイント`flash/update`の`flashId`以外のパラメータは必須ではなくなりました -- Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正 -- Fix: 破損した通知をクライアントに送信しないように - * 通知欄が無限にリロードされる問題が改善する可能性があります -- Fix: 禁止キーワードを含むノートがDelayed Queueに追加されて再処理される問題を修正 -- Fix: 自分がフォローしていないアカウントのフォロワー限定ノートが閲覧できることがある問題を修正 -- Fix: タイムラインのオプションで「リノートを表示」を無効にしている際、投票のみの引用リノートが流れてこない問題を修正 -- Fix: エンドポイント`admin/emoji/update`の各種修正 - - 必須パラメータを`id`または`name`のいずれかのみに - - `id`の代わりに`name`で絵文字を指定可能に(`id`・`name`両指定時は従来通り`name`を変更する挙動) - - `category`および`licence`が指定なしの時勝手にnullに上書きされる挙動を修正 -- Fix: 通知の受信設定で「相互フォロー」が正しく動作しない問題を修正 - -## 2024.2.0 - -### Note -- 外部サイトからプラグインをインストールする場合のパスが`/install-extentions`から`/install-extensions`に変わります。以前のパスからは自動でリダイレクトされるようになっていますが、新しいパスに変更することをお勧めします。 - -### General -- Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加 -- Feat: Add support for TrueMail -- Feat: AGPLv3ライセンスに誤って違反するのを防止する機能を追加 - - 管理者がrepositoryUrlを変更したり、またはソースコードを直接頒布することを選択できるようになります - - 本体のソースコードに改変を加えた際に、ライセンスに基づく適切な案内を表示します -- Enhance: モデレーターはすべてのユーザーのリアクション一覧を見られるように -- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正 -- Fix: リモートユーザーのリアクション一覧がすべて見えてしまうのを修正 - * すべてのリモートユーザーのリアクション一覧を見えないようにします -- Fix: 特定のキーワード及び正規表現にマッチする文字列を含むノートが投稿された際、エラーに出来るような設定項目を追加 #13207 - * デフォルトは空欄なので適用前と同等の動作になります - -### Client -- Feat: 新しいゲームを追加 -- Feat: 音声・映像プレイヤーを追加 -- Feat: 絵文字の詳細ダイアログを追加 -- Feat: 枠線をつけるMFM`$[border.width=1,style=solid,color=fff,radius=0 ...]`を追加 - - デフォルトで枠線からはみ出る部分が隠されるようにしました。初期と同じ挙動にするには`$[border.noclip`が必要です -- Feat: スワイプでタブを切り替えられるように -- Enhance: MFM等のコードブロックに全文コピー用のボタンを追加 -- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように -- Enhance: チャンネルノートのピン留めをノートのメニューからできるように -- Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように -- Enhance: AiScriptを0.17.0に更新 [CHANGELOG](https://github.com/aiscript-dev/aiscript/blob/bb89d132b633a622d3cb0eff0d0cc7e476c0cfdd/CHANGELOG.md) - - 配列の範囲外・非整数のインデックスへの代入が完全禁止になるので注意 -- Enhance: 絵文字ピッカー・オートコンプリートで、完全一致した絵文字を優先的に表示するように -- Enhance: Playの説明欄にMFMを使えるように -- Enhance: チャンネルノートの場合は詳細ページからその前後のノートを見れるように -- Enhance: 季節に応じた画面の演出を南半球でも利用できるように -- Enhance: タイムラインフィルターの設定をすべて保持できるように - - 今までの「TLに他の人への返信を含める」設定は一旦リセットされます -- Enhance: タイムラインフィルターに「センシティブなファイルを含むノートを表示」を追加 -- Enhance: ノート作成画面のファイル添付メニューから直接ファイルを削除できるように -- Enhance: MFMの属性でオートコンプリートが使用できるように #12735 -- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように -- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように -- Enhance: リモートへの引用リノートと同一のリンクにはリンクプレビューを表示しないように -- Enhance: コードのシンタックスハイライトにテーマを適用できるように -- Enhance: リアクション権限がない場合、ハートにフォールバックするのではなくリアクションピッカーなどから打てないように - - リモートのユーザーにローカルのみのカスタム絵文字をリアクションしようとした場合 - - センシティブなリアクションを認めていないユーザーにセンシティブなカスタム絵文字をリアクションしようとした場合 - - ロールが必要な絵文字をリアクションしようとした場合 -- Enhance: ページ遷移時にPlayerを閉じるように -- Enhance: 通報ページのユーザをクリックした際にユーザをウィンドウで開くように -- Enhance: ノートの通報時にリモートのノートであっても自インスタンスにおけるノートのリンクを含むように -- Enhance: オフライン表示のデザインを改善・多言語対応 -- Fix: ネイティブモードの絵文字がモノクロにならないように -- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 -- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正 -- Fix: v2023.12.1で追加された`$[clickable ...]`および`onClickEv`が正しく機能していないのを修正 -- Fix: Renoteのキーボードショートカットが機能していなかった問題を修正 -- Fix: 投稿フォームでアンケートの日時指定をした状態で再読み込みをすると期日が復元されない問題を修正 -- Fix: アンケートを設定したノートを「削除して編集」をするとアンケートの期日が引き継がれず、リセットされてしまう問題を修正 -- Fix: デッキのプロファイル作成時に名前を空にできる問題を修正 -- Fix: テーマ作成時に名称が空欄でも作成できてしまう問題を修正 -- Fix: プラグインで`Plugin:register_note_post_interruptor`を使用すると、ノートが投稿できなくなる問題を修正 -- Fix: iOSで大きな画像を変換してアップロードできない問題を修正 -- Fix: 「アニメーション画像を再生しない」もしくは「データセーバー(アイコン)」を有効にしていても、アイコンデコレーションのアニメーションが停止されない問題を修正 -- Fix: 画像をクロップするとクロップ後の解像度が異様に低くなる問題の修正 -- Fix: 画像をクロップ時、正常に完了できない問題の修正 -- Fix: キャプションが空の画像をクロップするとキャプションにnullという文字列が入ってしまう問題の修正 -- Fix: プロフィールを編集してもリロードするまで反映されない問題を修正 -- Fix: エラー画像URLを設定した後解除すると,デフォルトの画像が表示されない問題の修正 -- Fix: MkCodeEditorで行がずれていってしまう問題の修正 -- Fix: Summaly proxy利用時にプレイヤーが動作しないことがあるのを修正 #13196 - -### Server -- Enhance: 連合先のレートリミットを超過した際にリトライするようになりました -- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916) -- Enhance: クリップをエクスポートできるように -- Enhance: `/files`のファイルに対してHTTP Rangeリクエストを行えるように -- Enhance: `api.json`のOpenAPI Specificationを3.1.0に更新 -- Enhance: 連合向けのノート配信を軽量化 #13192 -- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正 -- Fix: `notes/create`で、`text`が空白文字のみで構成されているか`null`であって、かつ`text`だけであるリクエストに対するレスポンスが400になるように変更 -- Fix: `notes/create`で、`text`が空白文字のみで構成されていてかつリノート、ファイルまたは投票を含んでいるリクエストに対するレスポンスの`text`が`""`から`null`になるように変更 -- Fix: ipv4とipv6の両方が利用可能な環境でallowedPrivateNetworksが設定されていた場合プライベートipの検証ができていなかった問題を修正 -- Fix: properly handle cc followers -- Fix: ジョブに関する設定の名前を修正 relashionshipJobPerSec -> relationshipJobPerSec -- Fix: コントロールパネル->モデレーション->「誰でも新規登録できるようにする」の初期値をONからOFFに変更 #13122 -- Fix: リモートユーザーが復活してもキャッシュにより該当ユーザーのActivityが受け入れられないのを修正 #13273 - -## 2023.12.2 - -### General -- v2023.12.1でDockerを利用してサーバーを起動できない問題を修正 - -### Client -- Enhance: 検索画面においてEnterキー押下で検索できるように - -## 2023.12.1 - -### Note -- アクセストークンの権限が再整理されたため、一部のAPIが古いAPIトークンでは動作しなくなりました。\ - 権限不足になる場合には権限を再設定して再生成してください。 - -### General -- Enhance: ローカリゼーションの更新 -- Fix: 自分のdirect noteがuser list timelineに追加されない - -### Client -- Feat: AiScript専用のMFM構文`$[clickable.ev=EVENTNAME ...]`を追加。`Mk:C:mfm`のオプション`onClickEv`に関数を渡すと、クリック時に`EVENTNAME`を引数にして呼び出す -- Enhance: MFM入力補助ボタンを投稿フォームに表示できるように #12787 -- Fix: 一部のモデログ(logYellowでの表示対象)について、表示の色が変わらない問題を修正 -- Fix: `fg`/`bg`MFMに長い単語を指定すると、オーバーフローされずはみ出る問題を修正 - -### Server -- Enhance: センシティブワードの設定がハッシュタグトレンドにも適用されるようになりました -- Enhance: `oauth/token`エンドポイントのCORS対応 -- Fix: 1702718871541-ffVisibility.jsのdownが壊れている -- Fix:「非センシティブのみ(リモートはいいねのみ)」を設定していても、センシティブに設定されたカスタム絵文字をリアクションできる問題を修正 -- Fix: ロールアサイン時の通知で,ロールアイコンが縮小されずに表示される問題を修正 -- Fix: サードパーティアプリケーションがWebsocket APIに無条件にアクセスできる問題を修正 -- Fix: サードパーティアプリケーションがユーザーの許可なしに非公開の情報を見ることができる問題を修正 - -## 2023.12.0 - -### Note -- 依存関係の更新に伴い、Node.js 20.10.0が最小要件になりました -- 絵文字の追加辞書を既にインストールしている場合は、お手数ですが再インストールのほどお願いします -- 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。 - - **影響:** - それにより、投稿フォームから表示される絵文字ピッカーのピン留め絵文字がリセットされたように感じるかもしれません(新設された"ピン留め(全般)"の設定が使われるため)。 - 投稿用のピン留め絵文字をアップデート前の状態にするには、以下の手順で操作します。 - - 1. 「設定」メニューに移動し、「絵文字ピッカー」タブを選択します。 - 2. 「ピン留 (全般)」のタブを選択します。 - 3. 「リアクション設定から上書きする」ボタンを押すことで、アップデート前の状態に戻すことができます。 - -### General -- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed) -- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) -- Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加 -- Enhance: 指定したドメインのメールアドレスの登録を弾くことができるように -- Enhance: 公開ロールにアサインされたときに通知が作成されるように -- Enhance: アイコンデコレーションを複数設定できるように -- Enhance: アイコンデコレーションの位置を微調整できるように -- Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072 -- Enhance: ローカリゼーションの更新 -- Enhance: 依存関係の更新 -- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 - -### Client -- Feat: 今日誕生日のフォロー中のユーザーを一覧表示できるウィジェットを追加 -- Feat: 画面に雪を降らせられるように -- Enhance: MFMのアニメーション要素(`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)に `delay` オプションを追加 -- Enhance: センシティブと判断されたウェブサイトのサムネイルを非表示に - - ウェブサイトをセンシティブと判断する仕組みが動いていないため、summalyProxyを使用しないと機能しません。 -- Enhance: 投稿フォームの絵文字ピッカーをリアクション時に使用するものと同じのを使用するように #12336 #12560 -- Enhance: リアクション用ピン留め絵文字と投稿時の絵文字入力用ピン留め絵文字を分けて設定できるように #12560 -- Enhance: 絵文字のオートコンプリート機能強化 #12364 -- Enhance: ユーザーのRawデータを表示するページが復活 -- Enhance: リアクション選択時に音を鳴らせるように -- Enhance: サウンドにドライブのファイルを使用できるように -- Enhance: ナビゲーションバーに項目「キャッシュを削除」を追加 -- Enhance: Shareページで投稿を完了すると、親ウィンドウ(親フレーム)にpostMessageするように -- Enhance: チャンネル、クリップ、ページ、Play、ギャラリーにURLのコピーボタンを設置 #11305 -- Enhance: ノートプレビューに「内容を隠す」が反映されるように -- Enhance: データセーバーでコードハイライトの読み込みを削減できるように -- Enhance: データセーバーの適用範囲を個別で設定できるように - - 従来のデータセーバーの設定はリセットされます -- Enhance: タイムライン上のタブからリスト、アンテナ、チャンネルの管理ページにジャンプできるように -- Enhance: ユーザー名、プロフィール、お知らせ、ページの編集画面でMFMや絵文字のオートコンプリートが使用できるように -- Enhance: プロフィール、お知らせの編集画面でMFMのプレビューを表示できるように -- Enhance: 絵文字の詳細ページに記載される情報を追加 -- Enhance: リアクションの表示幅制限を設定可能に -- Enhance: Unicode 15.0のサポート -- Enhance: コードブロックのハイライト機能を利用するには言語を明示的に指定させるように - - MFMでコードブロックを利用する際に意図しないハイライトが起こらないようになりました - - 逆に、MFMでコードハイライトを利用したい際は言語を明示的に指定する必要があります - (例: ` ```js ` → Javascript, ` ```ais ` → AiScript) -- Enhance: 絵文字などのオートコンプリートでShift+Tabを押すと前の候補を選択できるように -- Enhance: チャンネルに新規の投稿がある場合にバッジを表示させる -- Enhance: サウンド設定に「サウンドを出力しない」と「Misskeyがアクティブな時のみサウンドを出力する」を追加 -- Enhance: 設定したタグをトレンドに表示させないようにする項目を管理画面で設定できるように -- Enhance: 絵文字ピッカーのカテゴリに「/」を入れることでフォルダ分け表示できるように -- Fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正 -- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367 -- Fix: コードエディタが正しく表示されない問題を修正 -- Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正 -- Fix: 一度に大量の通知が入った際に通知音が音割れする問題を修正 -- Fix: 共有機能をサポートしていないブラウザの場合は共有ボタンを非表示にする #11305 -- Fix: 通知のグルーピング設定を変更してもリロードされるまで表示が変わらない問題を修正 #12470 -- Fix: 長い名前のチャンネルにおける投稿フォームの表示が崩れる問題を修正 -- Fix: セキュリティ向上のためAiScriptの`Mk:apiExternal`を無効化 -- Fix: ノート中の絵文字をタップして「リアクションする」からリアクションした際にリアクションサウンドが鳴らない不具合を修正 -- Fix: ノート中のリアクションの表示を微調整 #12650 -- Fix: AiScriptの`readline`が不正な値を返すことがある問題を修正 -- Fix: 投票のみ/画像のみの引用RNが、通知欄でただのRNとして判定されるバグを修正 -- Fix: CWをつけて引用RNしても、普通のRNとして扱われてしまうバグを修正しました。 -- Fix: 「画像が1枚のみのメディアリストの高さ」を「デフォルト」以外に設定していると、CWの中などに添付された画像が見られないバグを修正 -- Fix: DeepL TranslationのPro accountトグルスイッチが表示されていなかったのを修正 -- Fix: twitterの埋め込みカード内リンクからリンク先を開けない問題を修正 -- Fix: WebKitブラウザー上でも「デバイスの画面を常にオンにする」機能が効くように -- Fix: ページ一覧ページの表示がモバイル環境において崩れているのを修正 -- Fix: MFMでルビの中のテキストがnyaizeされない問題を修正 - -### Server -- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように -- Enhance: Meilisearchを有効にした検索で、ユーザーのミュートやブロックを考慮するように -- Enhance: カスタム絵文字のインポート時の動作を改善 -- Enhance: json-schema(OpenAPIの戻り値として使用されるスキーマ定義)を出来る限り最新化 #12311 -- Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303 -- Fix: ロールタイムラインが保存されない問題を修正 -- Fix: api.jsonの生成ロジックを改善 #12402 -- Fix: 招待コードが使い回せる問題を修正 -- Fix: 特定の条件下でチャンネルやユーザーのノート一覧に最新のノートが表示されなくなる問題を修正 -- Fix: 何もノートしていないユーザーのフィードにアクセスするとエラーになる問題を修正 -- Fix: リストタイムラインにてミュートが機能しないケースがある問題と、チャンネル投稿がストリーミングで流れてきてしまう問題を修正 #10443 -- Fix: 「みつける」のなかにミュートしたユーザが現れてしまう問題を修正 #12383 -- Fix: Social/Local/Home Timelineにてインスタンスミュートが効かない問題 -- Fix: ユーザのノート一覧にてインスタンスミュートが効かない問題 -- Fix: チャンネルのノート一覧にてインスタンスミュートが効かない問題 -- Fix: 「みつける」が年越し時に壊れる問題を修正 -- Fix: アカウントをブロックした際に、自身のユーザーのページでノートが相手に表示される問題を修正 -- Fix: モデレーションログがモデレーターは閲覧できないように修正 -- Fix: ハッシュタグのトレンド除外設定が即時に効果を持つように修正 -- Fix: HTTP Digestヘッダのアルゴリズム部分に大文字の"SHA-256"しか使えない - -## 2023.11.1 - -### Note -- 悪意のある第三者がリモートユーザーになりすました任意のアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-3f39-6537-3cgc)をご覧ください。 - -### General -- Feat: 管理者がコントロールパネルからメールアドレスの照会を行えるようになりました -- Enhance: ローカリゼーションの更新 -- Enhance: 依存関係の更新 - -### Client -- Enhance: MFMでルビを振れるように - - 例: `$[ruby 三須木 みすき]` -- Enhance: MFMでUNIX時間を指定して日時を表示できるように - - 例: `$[unixtime 1701356400]` -- Enhance: プラグインでエラーが発生した場合のハンドリングを強化 -- Enhance: 細かなUIのブラッシュアップ -- Fix: 効果音が再生されるとデバイスで再生している動画や音声が停止する問題を修正 #12339 -- Fix: デッキに表示されたチャンネルの表示先チャンネルを切り替えた際、即座に反映されない問題を修正 #12236 -- Fix: プラグインでノートの表示を書き換えられない問題を修正 -- Fix: アイコンデコレーションが見切れる場合がある問題を修正 -- Fix: 「フォロー中の人全員の返信を含める/含めないようにする」のボタンを押下した際の確認が機能していない問題を修正 -- Fix: 非ログイン時に「メモを追加」を表示しないように変更 #12309 -- Fix: 絵文字ピッカーでの検索が更新されない問題を修正 -- Fix: 特定の条件下でノートがnyaizeされない問題を修正 - -### Server -- Enhance: FTTのデータベースへのフォールバック処理を行うかどうかを設定可能に -- Fix: トークンのないプラグインをアンインストールするときにエラーが出ないように -- Fix: 投稿通知がオンでもダイレクト投稿はユーザーに通知されないようにされました -- Fix: ユーザタイムラインの「ノート」選択時にリノートが混ざり込んでしまうことがある問題の修正 #12306 -- Fix: LTLに特定条件下にてチャンネルへの投稿が混ざり込む現象を修正 -- Fix: ActivityPub: 追加情報のカスタム絵文字がユーザー情報のtagに含まれない問題を修正 -- Fix: ActivityPubに関するセキュリティの向上 -- Fix: 非公開の投稿に対して返信できないように - -## 2023.11.0 - -### Note -- iOS 16.4未満を使用している場合はiOS 16.4以上にアップデートをお願いします - -### General -- Feat: アイコンデコレーション機能 - - サーバーで用意された画像をアイコンに重ねることができます - - 画像のテンプレートはこちらです: https://misskey-hub.net/brand-assets/ - - 最大でも黄色いエリア内にデコレーションを収めることを推奨します。 - - 画像は512x512pxを推奨します。 -- Feat: チャンネル設定にリノート/引用リノートの可否を設定できる項目を追加 -- Enhance: アカウント登録時のメールアドレス認証に30分の有効期限を設定 - - 有効期限が切れた後であれば、登録時に使用した招待コードを再度利用できるように変更しました。 - - ユーザーが誤ったメールアドレスを入力した場合に招待コードが失効してしまう問題が解消されます。 -- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように -- Enhance: 未読の通知数を表示できるように -- Enhance: 通知されず、確認の必要もないお知らせ(silence)を作成可能になりました -- Enhance: ローカリゼーションの更新 -- Enhance: 依存関係の更新 -- Change: CWを使用する場合、注釈を空にすることは許可されなくなりました - -### Client -- Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました - - 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください - https://misskey-hub.net/docs/for-developers/publish-on-your-website/ -- Feat: 通知をグルーピングして表示するオプション(オプトアウト) -- Feat: Misskeyの基本的なチュートリアルを実装 -- Feat: スワイプしてタイムラインを再読込できるように - - PCの場合は右上のボタンからでも再読込できます -- Enhance: タイムラインの自動更新を無効にできるように -- Enhance: コードのシンタックスハイライトエンジンをShikiに変更 - - AiScriptのシンタックスハイライトに対応 - - MFMでAiScriptをハイライトする場合、コードブロックの開始部分を ` ```is ` もしくは ` ```aiscript ` としてください -- Enhance: データセーバー有効時はアニメーション付きのアバター画像が停止するように -- Enhance: プラグインを削除した際には、使用されていたアクセストークンも同時に削除されるようになりました -- Enhance: プラグインで`Plugin:register_note_view_interruptor`を用いてnoteの代わりにnullを返却することでノートを非表示にできるようになりました -- Enhance: AiScript関数`Mk:nyaize()`が追加されました -- Enhance: 情報→ツール はナビゲーションバーにツールとして独立した項目になりました -- Enhance: ノート内の絵文字をクリックすることで、コピーおよびリアクションができるように -- Enhance: その他細かなブラッシュアップ -- Fix: 投稿フォームでのユーザー変更がプレビューに反映されない問題を修正 -- Fix: ユーザーページの ノート > ファイル付き タブにリプライが表示されてしまう -- Fix: 「検索」MFMにおいて一部の検索キーワードが正しく認識されない問題を修正 -- Fix: 一部の言語でMisskey Webがクラッシュする問題を修正 -- Fix: チャンネルの作成・更新時に失敗した場合何も表示されない問題を修正 #11983 -- Fix: 個人カードのemojiがバッテリーになっている問題を修正 -- Fix: 標準テーマと同じIDを使用してインストールできてしまう問題を修正 -- Fix: 絵文字ピッカーでバッテリーの絵文字が複数表示される問題を修正 #12197 -- Fix: 11以上されているリアクションにおいてツールチップで示されるリアクション数が本来よりも1多い問題を修正 #12174 -- Fix: サイレンス状態で公開範囲のパブリックを選択できてしまう問題を修正 #12224 -- Fix: In deck layout, replies option is not saved after refresh -- Fix: アーカイブしたお知らせがコントロールパネルに表示される問題を修正 -- Note: アップデート後、サウンドに関する設定が初期化されます - -### Server -- Feat: Registry APIがサードパーティから利用可能になりました -- Enhance: RedisへのTLのキャッシュ(FTT)をオフにできるように -- Enhance: フォローしているチャンネルをフォロー解除した時(またはその逆)、タイムラインに反映される間隔を改善 -- Enhance: プロフィールの自己紹介欄のMFMが連合するようになりました - - 相手がMisskey v2023.11.0以降である必要があります -- Enhance: チャンネル取得時のパフォーマンスを向上 -- Enhance: AP: ApplicationタイプのアカウントをisBotとして扱うように -- Fix: リストTLに自分のフォロワー限定投稿が含まれない問題を修正 -- Fix: ローカルタイムラインに投稿者自身の投稿への返信が含まれない問題を修正 -- Fix: 自分のフォローしているユーザーの自分のフォローしていないユーザーの visibility: followers な投稿への返信がストリーミングで流れてくる問題を修正 -- Fix: RedisへのTLキャッシュが有効の場合にHTL/LTL/STLが空になることがある問題を修正 -- Fix: STLでフォローしていないチャンネルが取得される問題を修正 -- Fix: `hashtags/trend`にてRedisからトレンドの情報が取得できない際にInternal Server Errorになる問題を修正 -- Fix: HTLをリロードまたは遡行したとき、フォローしているチャンネルのノートが含まれない問題を修正 #11765 #12181 -- Fix: リノートをリノートできるのを修正 -- Fix: アクセストークンを削除すると、通知が取得できなくなる場合がある問題を修正 -- Fix: 自身の宛先なしダイレクト投稿がストリーミングで流れてこない問題を修正 -- Fix: サーバーサイドからのテスト通知を正しく行えるように修正 -- Fix: GTLの「リノートを表示」オプションが機能しないのを修正 #12233 - -## 2023.10.2 - -### General -- Feat: アンテナでローカルの投稿のみ収集できるようになりました -- Feat: サーバーサイレンス機能が追加されました -- Enhance: 新規にフォローした人の返信をデフォルトでTLに追加できるオプションを追加 -- Enhance: HTL/LTL/STLを2023.10.0アップデート以前まで遡れるように -- Enhance: フォロー/フォロー解除したときに過去分のHTLにも含まれる投稿が反映されるように -- Enhance: ローカリゼーションの更新 -- Enhance: 依存関係の更新 - -### Client -- Enhance: TLの返信表示オプションを記憶するように -- Enhance: 投稿されてから時間が経過しているノートであることを視覚的に分かりやすく - -### Server -- Enhance: タイムライン取得時のパフォーマンスを向上 -- Enhance: ストリーミングAPIのパフォーマンスを向上 -- Fix: users/notesでDBから参照した際にチャンネル投稿のみ取得される問題を修正 -- Fix: コントロールパネルの設定項目が正しく保存できない問題を修正 -- Fix: 管理者権限のロールを持っていても一部のAPIが使用できないことがある問題を修正 -- Change: ユーザーのisCatがtrueでも、サーバーではnyaizeが行われなくなりました - - isCatな場合、クライアントでnyaize処理を行うことを推奨します - -## 2023.10.1 -### General -- Enhance: ローカルタイムライン、ソーシャルタイムラインで返信を含むかどうか設定可能に - -### Client -- Fix: 絵文字ピッカーで横に長いカスタム絵文字が見切れる問題を修正 - -### Server -- Fix: フォローしているユーザーからの自分の投稿への返信がタイムラインに含まれない問題を修正 -- Fix: users/notesでセンシティブチャンネルの投稿が含まれる場合がある問題を修正 - -## 2023.10.0 -### NOTE -- 2023.9.2で導入されたノート編集機能はクオリティの高い実装が困難であることが判明したため撤回されました -- アップデートを行うと、タイムラインが一時的にリセットされます - - アンテナ内のノートも含む -- ソフトミュート設定はクライアントではなくサーバー側に保存されるようになったため、アップデートを行うとソフトミュートの設定がリセットされます - -### Changes -- API: users/notes, notes/local-timeline で fileType 指定はできなくなりました -- API: notes/featured でページネーションは他APIと同様 untilId を使って行うようになりました - -### General -- Feat: ユーザーごとに他ユーザーへの返信をタイムラインに含めるか設定可能になりました -- Feat: ユーザーリスト内のメンバーごとに他ユーザーへの返信をユーザーリストタイムラインに含めるか設定可能になりました -- Feat: ユーザーごとのハイライト -- Feat: プライバシーポリシー・運営者情報(Impressum)の指定が可能になりました - - プライバシーポリシーはサーバー登録時に同意確認が入ります -- Feat: タイムラインがリアルタイム更新中に広告を挿入できるようになりました - - デフォルトは無効 - - 頻度はコントロールパネルから設定できます。運営中のサーバーのTLの流速を見て、最適な値を指定してください。 -- Enhance: ソフトワードミュートとハードワードミュートは統合されました -- Enhance: モデレーションログ機能の強化 -- Enhance: ローカリゼーションの更新 -- Enhance: 依存関係の更新 -- Fix: ダイレクト投稿をリノートできてしまう問題を修正 -- Fix: ユーザーリストTLにチャンネル投稿が含まれる問題を修正 - -### Client -- Feat: 「ファイルの詳細」ページを追加 - - ドライブのファイルの拡大プレビューができるように - - ファイルが添付されたノートの一覧が表示できるように -- Enhance: 二要素認証のバックアップコード一覧をテキストファイルでダウンロード可能に -- Enhance: 動画再生時のデフォルトボリュームを30%に -- Fix: リアクションしたユーザ一覧のUIが稀に左上に残ってしまう不具合を修正 - -### Server -- Enhance: drive/files/attached-notes がページネーションに対応しました -- Enhance: タイムライン取得時のパフォーマンスを大幅に向上 -- Enhance: ハイライト取得時のパフォーマンスを大幅に向上 -- Enhance: トレンドハッシュタグ取得時のパフォーマンスを大幅に向上 -- Enhance: WebSocket接続が多い場合のパフォーマンスを向上 -- Enhance: 不要なPostgreSQLのインデックスを削除しパフォーマンスを向上 -- Fix: 連合なしアンケートに投票をするとUpdateがリモートに配信されてしまうのを修正 -- Fix: nodeinfoにおいてCORS用のヘッダーが設定されていないのを修正 -- Fix: 同じ種類のTLのストリーミングを複数接続できない問題を修正 -- Fix: アンテナTLを途中までしかページネーションできなくなることがある問題を修正 -- Fix: 「ファイル付きのみ」のTLでファイル無しの新着ノートが流れる問題を修正 -- Fix: プロセスが終了しない、あるいは非常に時間がかかる問題を修正 - -## 2023.9.3 -### General -- Enhance: ノートの翻訳機能の利用可否をロールで設定可能に - -### Client -- Enhance: AiScriptでホストのアドレスを参照する定数`SERVER_URL`を追加 -- Enhance: モデレーションログ機能の強化 -- Enhance: ローカリゼーションの更新 - -### Server -- Fix: Redisに古いバージョンのキャッシュが残っている場合、キャッシュが消えるまでの間通知が届かなくなる問題を修正 -- Fix: 後方互換性の修正 - -## 2023.9.2 - -### General -- Feat: ノートの編集をできるように - - ロールで編集可否を設定可能 -- Feat: 通知を種類ごとに 全員から受け取る/フォロー中のユーザーのみ受け取る/フォロワーのみ受け取る/相互のみ受け取る/指定したリストのメンバーのみ受け取る/受け取らない から選べるように -- Enhance: タイムラインからRenoteを除外するオプションを追加 -- Enhance: ユーザーページのノート一覧でRenoteを除外できるように -- Enhance: タイムラインでファイルが添付されたノートのみ表示するオプションを追加 -- Enhance: モデレーションログ機能の強化 -- Enhance: 依存関係の更新 -- Enhance: ローカリゼーションの更新 - -### Client -- Enhance: Plugin:register_post_form_actionを用いてCWを取得・変更できるように -- Enhance: admin/ad/listにて掲載中の広告が絞り込めるように -- Enhance: AiScriptにリモートサーバーのAPIを叩く用の関数を追加(`Mk:apiExternal`) - -### Server -- Enhance: MasterプロセスのPIDを書き出せるように -- Enhance: admin/ad/createにてレスポンス200、設定した広告情報を返すように - -## 2023.9.1 - -### General -- Enhance: モデレーションログ機能の強化 - -### Client -- Fix: ノートのメニューにある「詳細」ボタンの表示がログイン/ログアウト状態で統一されていない問題を修正 - -### Server -- Fix: お知らせのページネーションが機能しない -- Fix: 「ユーザーの新規投稿」の通知設定を切り替えるとサーバー内部エラーが出る - -## 2023.9.0 - -### Note -- meilisearchを使用する場合、v1.2以上が必要です - -### General -- Feat: OAuth 2.0のサポート -- Feat: お知らせ機能の強化 - - ユーザー個別のお知らせを作成可能に - - お知らせのバナー表示やダイアログ表示が可能に - - お知らせのアイコンを設定可能に -- Feat: チャンネルをセンシティブ指定できるようになりました - - センシティブチャンネルのNoteのReNoteはデフォルトでHome TLに流れるようになりました - - センシティブチャンネルのノートはユーザープロフィールに表示されません -- Feat: 二要素認証のバックアップコードが生成されるようになりました - - ref. https://github.com/MisskeyIO/misskey/pull/121 -- Feat: 二要素認証でパスキーをサポートするようになりました -- Feat: 指定したユーザーが投稿したときに通知できるようになりました -- Feat: プロフィールでのリンク検証 -- Feat: モデレーションログ機能 -- Feat: 通知をテストできるようになりました -- Feat: PWAのアイコンが設定できるようになりました -- Enhance: サーバー名の略称が設定できるようになりました -- Enhance: アンテナの受信ソースに指定したユーザを除外するものを追加 -- Enhance: 二要素認証設定時のセキュリティを強化 - - パスワード入力が必要な操作を行う際、二要素認証が有効であれば確認コードの入力も必要になりました -- Enhance: manifest.jsonをオーバーライド可能に -- Enhance: 依存関係の更新 -- Enhance: ローカリゼーションの更新 - -### Client -- Feat: 任意のユーザーリストをタイムラインページにピン留めできるように - - 設定->クライアント設定->全般 から設定可能です -- Feat: Playで直接投稿フォームを埋め込めるように(`Ui:C:postForm`) -- Feat: クライアントを起動している間、デバイスの画面が自動でオフになるのを防ぐオプションを追加 -- Feat: 新しい実績を追加 -- Enhance: ノート詳細ページでリノート一覧、リアクション一覧タブを追加 - - ノートのメニューからは当該項目は消えました -- Enhance: センシティブなメディアを目立たせる設定を追加 -- Enhance: プロフィールにその人が作ったPlayの一覧出せるように -- Enhance: メニューのスイッチの動作を改善 -- Enhance: 絵文字ピッカーの検索の表示件数を100件に増加 -- Enhance: 投稿フォームのプレビューの表示状態を記憶するように -- Enhance: ユーザーメニューでスイッチでユーザーリストに追加・削除できるように -- Enhance: 自分が押したリアクションのデザインを改善 -- Enhance: ノート検索にローカルのみ検索可能なオプションの追加 -- Enhance: Renote自体を通報できるように -- Enhance: データセーバーモードの強化 -- Enhance: Renoteを管理者権限で削除可能に -- Enhance: `$[rainbow ]`記法が、動きのあるMFMが無効になっていても使用できるようになりました -- Enhance: Playの操作を行うAPI TokenをAPIコンソールから発行できるように -- Enhance: リアクションの表示サイズをより大きくできるように -- Enhance: AiScriptを0.16.0に更新 -- Enhance: AiScriptからMisskeyサーバーAPIを呼び出す際の制限を撤廃 -- Enhance: AiScriptで`LOCALE`として現在の設定言語を取得できるように -- Enhance: Mk:apiが失敗した時にエラー型の値(AiScript 0.16.0で追加)を返すように -- Enhance: ScratchpadでAsync:系関数やボタンのコールバックなどのエラーにもダイアログを出すように(試験的なためPlayなどには未実装) -- Enhance: ノート詳細ページ読み込み時のパフォーマンスが向上しました -- Enhance: タイムラインでリスト/アンテナ選択時のパフォーマンスを改善 -- Enhance: 「Moderation note」、「Add moderation note」をローカライズできるように -- Enhance: プラグインのソースコードを確認・コピーできるように -- Enhance: 細かなデザインの調整 -- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正 -- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正 -- Fix: iOSで画面を回転させるとテキストサイズが変わる問題を修正 -- Fix: word mute for sub note is not applied -- Fix: タイムラインを下にスクロールしてノート画面に移動して再び戻ったら以前のスクロール位置を失う問題を修正 -- Fix: Misskeyプラグインをインストールする際のAiScriptバージョンのチェックが0.14.0以降に対応していない問題を修正 -- Fix: 他のサーバーのユーザーへ「メッセージを送信」した時の初期テキストのメンションが間違っている問題を修正 -- Fix: 環境によってはMisskey Webが開けない問題を修正 -- Fix: プラグインの権限リストが見れない問題を修正 -- Fix: 複数の階層があるメニューで、短くタップすると正常に動かない場合がある問題を修正 -- Fix: アニメーションがオフのとき、スマホで子メニューの選択ができない問題を修正 -- Fix: ドロワーメニューで、親メニュー項目をマウスでホバーすると子メニューが表示されてしまう問題を修正 -- Fix: AiScriptでMk:apiが外部と通信できる問題を修正 - -### Server -- Change: cacheRemoteFilesの初期値はfalseになりました -- Enhance: ファイルアップロード時等にファイル名の拡張子を修正する関数(correctFilename)の挙動を改善 -- Enhance: Webhookのペイロードにサーバーのurlが含まれるようになりました -- Enhance: Webhook設定でsecretを空に出来るように -- Enhance: 使われていないアンテナの自動停止を設定可能に -- Enhance: nodeinfo 2.1対応 -- Enhance: 自分へのメンション一覧を取得する際のパフォーマンスを向上 -- Enhance: Docker環境でjemallocを使用することでメモリ使用量を削減 -- Enhance: ID生成方式としてaidxを追加、かつデフォルトに -- Enhance: Add address bind config option (outgoingAddress) -- Fix: MK_ONLY_SERVERオプションを指定した際にクラッシュする問題を修正 -- Fix: notes/reactionsのページネーションが機能しない問題を修正 -- Fix: ノート検索 `notes/search` にてhostを指定した際に検索結果に反映されるように -- Fix: 一部のfeatured noteを照会できない問題を修正 -- Fix: muteがapiからのuser list timeline取得で機能しない問題を修正 -- Fix: ジョブキュー管理画面の認証を回避できる問題を修正 -- Fix: 一部のサーバー内部エラーがスタックトレースを返さないように修正 -- Fix: 一部のリモートユーザーをフォローすることができない問題を修正 - -## 13.14.2 - -### Client -- リストTLで、ユーザーが追加・削除されてもTLを初期化しないように -- URL取得変数を関数に変更 CURRENT_URL -> Mk:url() -- Fix: モバイル表示のときページ下部がナビゲーションバーに隠れる問題を修正 -- Fix: 一部モーダルダイアログでスクロールできない問題を修正 -- Fix: Selecting all emojis in Custom emoji is impossible -- Fix: PhotoSwipeによるメモリリークの修正 - -### Server -- Fix: APIのオフセットが壊れていたせいで「もっと見る」でもっと見れない問題を修正 -- Fix: 外部サーバーの投稿がタイムラインに表示されないことがある問題を修正 - -## 13.14.1 - -### General -- 招待機能を改善しました - * 過去に発行した招待コードを確認できるようになりました - * ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました - * 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました -- ユーザーにロールが期限付きでアサインされている場合、その期限をユーザーのモデレーションページで確認できるようになりました -- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました -- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました - -### Client -- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように -- ドライブファイルのメニューで画像をクロップできるように -- 画像を動画と同様に簡単に隠せるように -- Enhance: ノートの埋め込みが複数画像と動画を表示されるように -- オリジナル画像を保持せずにアップロードする場合webpでアップロードされるように(Safari以外) -- 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように -- フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように -- 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように -- プロフィールURLをコピーできるボタンを追加 #11190 -- `CURRENT_URL`で現在表示中のURLを取得できるように(AiScript) -- ユーザーのContextMenuに「アンテナに追加」ボタンを追加 -- フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように -- 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように -- オフライン時の画面にリロードボタンを追加 -- Renote時に公開範囲のデフォルト設定が適用されるように -- Deckで非ルートページにアクセスした際に簡易UIで表示しない設定を追加 -- ロール設定画面でロールIDを確認できるように -- コンテキストメニュー表示時のパフォーマンスを改善 -- フォロー/フォロワー非公開時の表示を改善 -- 本文にMFMが含まれている場合に自動でたたまれる機能が、返信先や引用RNにも適用されるように - - position は対象外になりました -- AiScriptを0.15.0に更新 - Fix: サーバーメトリクスが90度傾いている -- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正 -- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正 -- Fix: ZenUIでポップアップの表示位置がおかしい問題を修正 -- Fix: ページ遷移でスクロール位置が保持されない問題を修正 -- Fix: フォルダーのページネーションが機能しない #11180 -- Fix: 長い文章を投稿する際、プレビューが画面からはみ出る問題を修正 -- Fix: システムフォント設定が正しく反映されない問題を修正 -- Fix: アンケート終了時のプッシュ通知が正しく表示されない問題を修正 -- Fix: MasterVolumeが0の時だけでなく各通知音の音量設定が0のときも、HTMLAudioElement.playが実行されないように変更 - -### Server -- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました -- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように -- 連合の配送ジョブのパフォーマンスを向上(ロック機構の見直し、Redisキャッシュの活用) -- featuredノートのsignedGet回数を減らしました -- リモートサーバーのセンシティブなファイルのキャッシュだけを無効化できるオプションを追加 -- MeilisearchにIndexするノートの範囲を設定できるように -- Export notes with file detail -- Add unix socket support -- 設定ファイルでioredisの全てのオプションを指定可能に -- Fix: エクスポートしたカスタム絵文字のzipが大きいと読み込めない問題を修正 -- Fix: リモートサーバーに無意味なActivityPubの配信を行うことがあるのを修正 -- Fix: Remove Meilisearch index when notes are deleted -- Fix: 非英語環境でのPostgreSQLのエラーハンドリングを修正 -- Fix: インスタンスのアイコンがbase64の場合の挙動を修正 -- Fix: ローカルの `Person` を指す `acct` URI を解析するときのバグを修正しました -- Fix: 無効化されたアンテナが再度有効化されないことがある問題を修正 ## 13.13.2 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 1bbfb082af..cd9cf8302a 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,131 +2,45 @@ ## Our Pledge -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, caste, color, religion, or sexual -identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards -Examples of behavior that contributes to a positive environment for our -community include: +Examples of behavior that contributes to creating a positive environment include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall - community +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members -Examples of unacceptable behavior include: +Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery, and sexual attention or advances of - any kind -* Trolling, insulting or derogatory comments, and personal or political attacks +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or email address, - without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting -## Enforcement Responsibilities +## Our Responsibilities -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -. -All complaints will be reviewed and investigated promptly and fairly. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at syuilotan@yahoo.co.jp. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of -actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or permanent -ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the -community. +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.1, available at -[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] -Community Impact Guidelines were inspired by -[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. - -For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at -[https://www.contributor-covenant.org/translations][translations]. - -[homepage]: https://www.contributor-covenant.org -[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html -[Mozilla CoC]: https://github.com/mozilla/diversity -[FAQ]: https://www.contributor-covenant.org/faq -[translations]: https://www.contributor-covenant.org/translations +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e10806686..f6b3804f84 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contribution guide We're glad you're interested in contributing Misskey! In this document you will find the information you need to contribute to the project. -> [!NOTE] +> **Note** > This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.** > Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\ > The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language. @@ -15,33 +15,18 @@ Before creating an issue, please check the following: - To avoid duplication, please search for similar issues before creating a new issue. - Do not use Issues to ask questions or troubleshooting. - Issues should only be used to feature requests, suggestions, and bug tracking. - - Please ask questions or troubleshooting in [GitHub Discussions](https://github.com/misskey-dev/misskey/discussions) or [Discord](https://discord.gg/Wp8gVStHW3). + - Please ask questions or troubleshooting in ~~the [Misskey Forum](https://forum.misskey.io/)~~ [GitHub Discussions](https://github.com/misskey-dev/misskey/discussions) or [Discord](https://discord.gg/Wp8gVStHW3). -> [!WARNING] +> **Warning** > Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged. -### Recommended discussing before implementation -We welcome your proposal. - +## Before implementation When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented. At this point, you also need to clarify the goals of the PR you will create, and make sure that the other members of the team are aware of them. PRs that do not have a clear set of do's and don'ts tend to be bloated and difficult to review. -Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask Committer to assign you). -By expressing your intention to work on the Issue, you can prevent conflicts in the work. - -To the Committers: you should not assign someone on it before the Final Decision. - -### How issues are triaged - -The Committers may: -* close an issue that is not reproducible on latest stable release, -* merge an issue into another issue, -* split an issue into multiple issues, -* or re-open that has been closed for some reason which is not applicable anymore. - -@syuilo reserves the Final Decision rights including whether the project will implement feature and how to implement, these rights are not always exercised. +Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask another member to assign you). By expressing your intention to work the Issue, you can prevent conflicts in the work. ## Well-known branches - **`master`** branch is tracking the latest release and used for production purposes. @@ -52,45 +37,25 @@ The Committers may: ## Creating a PR Thank you for your PR! Before creating a PR, please check the following: - If possible, prefix the title with a keyword that identifies the type of this PR, as shown below. - - `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc - - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR. + - `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc + - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR. - If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text. - Please add the summary of the changes to [`CHANGELOG.md`](/CHANGELOG.md). However, this is not necessary for changes that do not affect the users, such as refactoring. - 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 `pnpm test` and `pnpm 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 🤗 -### Additional things for ActivityPub payload changes -*This section is specific to misskey-dev implementation. Other fork or implementation may take different way. A significant difference is that non-"misskey-dev" extension is not described in the misskey-hub's document.* - -If PR includes changes to ActivityPub payload, please reflect it in [misskey-hub's document](https://github.com/misskey-dev/misskey-hub-next/blob/master/content/ns.md) by sending PR. - -The name of purporsed extension property (referred as "extended property" in later) to ActivityPub shall be prefixed by `_misskey_`. (i.e. `_misskey_quote`) - -The extended property in `packages/backend/src/core/activitypub/type.ts` **must** be declared as optional because ActivityPub payloads that comes from older Misskey or other implementation may not contain it. - -The extended property must be included in the context definition. Context is defined in `packages/backend/src/core/activitypub/misc/contexts.ts`. -The key shall be same as the name of extended property, and the value shall be same as "short IRI". - -"Short IRI" is defined in misskey-hub's document, but usually takes form of `misskey:`. (i.e. `misskey:_misskey_quote`) - -One should not add property that has defined before by other implementation, or add custom variant value to "well-known" property. - ## Reviewers guide Be willing to comment on the good points and not just the things you want fixed 💯 -読んでおくといいやつ -- https://blog.lacolaco.net/posts/1e2cf439b3c2/ -- https://konifar-zatsu.hatenadiary.jp/entry/2024/11/05/192421 - ### Review perspective - Scope - - Are the goals of the PR clear? - - Is the granularity of the PR appropriate? + - Are the goals of the PR clear? + - Is the granularity of the PR appropriate? - Security - Does merging this PR create a vulnerability? - Performance @@ -101,22 +66,6 @@ Be willing to comment on the good points and not just the things you want fixed - Are there any omissions or gaps? - Does it check for anomalies? -## Security Advisory -### For reporter -Thank you for your reporting! - -If you can also create a patch to fix the vulnerability, please create a PR on the private fork. - -> [!note] -> There is a GitHub bug that prevents merging if a PR not following the develop branch of upstream, so please keep follow the develop branch. - -### For misskey-dev member -修正PRがdevelopに追従されていないとマージできないので、マージできなかったら - -> Could you merge or rebase onto upstream develop branch? - -などと伝える。 - ## Deploy The `/deploy` command by issue comment can be used to deploy the contents of a PR to the preview environment. ``` @@ -128,7 +77,7 @@ An actual domain will be assigned so you can test the federation. ## Release ### Release Instructions -1. Commit version changes in the `develop` branch ([package.json](package.json)) +1. Commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json)) 2. Create a release PR. - Into `master` from `develop` branch. - The title must be in the format `Release: x.y.z`. @@ -139,7 +88,7 @@ An actual domain will be assigned so you can test the federation. - The target branch must be `master` - The tag name must be the version -> [!NOTE] +> **Note** > Why this instruction is necessary: > - To perform final QA checks > - To distribute responsibility @@ -152,30 +101,26 @@ You can improve our translations with your Crowdin account. Your changes in Crowdin are automatically submitted as a PR (with the title "New Crowdin translations") to the repository. The owner [@syuilo](https://github.com/syuilo) merges the PR into the develop branch before the next release. -If your language is not listed in Crowdin, please open an issue. We will add it to Crowdin. -For newly added languages, once the translation progress per language exceeds 70%, it will be officially introduced into Misskey and made available to users. +If your language is not listed in Crowdin, please open an issue. ![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg) ## Development -### Setup -Before developing, you have to set up environment. Misskey requires Redis, PostgreSQL, and FFmpeg. +During development, it is useful to use the -You would want to install Meilisearch to experiment related features. Technically, meilisearch is not strict requirement, but some features and tests require it. +``` +pnpm dev +``` -There are a few ways to proceed. +command. -#### Use system-wide software -You could install them in system-wide (such as from package manager). - -#### Use `docker compose` -You could obtain middleware container by typing `docker compose -f $PROJECT_ROOT/compose.local-db.yml up -d`. - -#### Use Devcontainer -Devcontainer also has necessary setting. This method can be done by connecting from VSCode. +- Server-side source files and automatically builds them if they are modified. Automatically start the server process(es). +- Vite HMR (just the `vite` command) is available. The behavior may be different from production. +- Service Worker is watched by esbuild. +### Dev Container Instead of running `pnpm` locally, you can use Dev Container to set up your development environment. -To use Dev Container, open the project directory on VSCode with Dev Containers installed. +To use Dev Container, open the project directory on VSCode with Dev Containers installed. **Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled. It will run the following command automatically inside the container. @@ -187,61 +132,38 @@ pnpm build pnpm migrate ``` -After finishing the migration, you can proceed. +After finishing the migration, run the `pnpm dev` command to start the development server. -### Start developing -During development, it is useful to use the -``` +``` bash pnpm dev ``` -command. - -- Server-side source files and automatically builds them if they are modified. Automatically start the server process(es). -- Service Worker is watched by esbuild. -- Vite HMR (just the `vite` command) is available. The behavior may be different from production. -- Vite runs behind the backend (the backend will proxy Vite at /vite and /embed_vite except for websocket used for HMR). -- You can see Misskey by accessing `http://localhost:3000` (Replace `3000` with the port configured with `port` in .config/default.yml). ## Testing -You can run non-backend tests by executing following commands: -```sh -pnpm --filter frontend test -pnpm --filter misskey-js test +- Test codes are located in [`/packages/backend/test`](/packages/backend/test). + +### Run test +Create a config file. ``` - -Backend tests require manual preparation of servers. See the next section for more on this. - -### Backend -There are three types of test codes for the backend: -- Unit tests: [`/packages/backend/test/unit`](/packages/backend/test/unit) -- Single-server E2E tests: [`/packages/backend/test/e2e`](/packages/backend/test/e2e) -- Multiple-server E2E tests: [`/packages/backend/test-federation`](/packages/backend/test-federation) - -#### Running Unit Tests or Single-server E2E Tests -1. Create a config file: -```sh cp .github/misskey/test.yml .config/ ``` - -2. Start DB and Redis servers for testing: -```sh -docker compose -f packages/backend/test/compose.yml up +Prepare DB/Redis for testing. ``` -Instead, you can prepare an empty (data can be erased) DB and edit `.config/test.yml` appropriately. - -3. Run all tests: -```sh -pnpm --filter backend test # unit tests -pnpm --filter backend test:e2e # single-server E2E tests +docker compose -f packages/backend/test/docker-compose.yml up ``` -If you want to run a specific test, run as a following command: -```sh -pnpm --filter backend test -- packages/backend/test/unit/activitypub.ts -pnpm --filter backend test:e2e -- packages/backend/test/e2e/nodeinfo.ts +Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. + +Run all test. +``` +pnpm test ``` -#### Running Multiple-server E2E Tests -See [`/packages/backend/test-federation/README.md`](/packages/backend/test-federation/README.md). +#### Run specify test +``` +pnpm jest -- foo.ts +``` + +### e2e tests +TODO ## Environment Variable @@ -258,12 +180,6 @@ Misskey uses Vue(v3) as its front-end framework. - **When creating a new component, please use the Composition API (with [setup sugar](https://v3.vuejs.org/api/sfc-script-setup.html) and [ref sugar](https://github.com/vuejs/rfcs/discussions/369)) instead of the Options API.** - Some of the existing components are implemented in the Options API, but it is an old implementation. Refactors that migrate those components to the Composition API are also welcome. -## Tabler Icons -アイコンは、Production Build時に使用されていないものが削除されるようになっています。 - -**アイコンを動的に設定する際には、 `ti-${someVal}` のような、アイコン名のみを動的に変化させる実装を行わないでください。** -必ず `ti-xxx` のような完全なクラス名を含めるようにしてください。 - ## nirax niraxは、Misskeyで使用しているオリジナルのフロントエンドルーティングシステムです。 **vue-routerから影響を多大に受けているので、まずはvue-routerについて学ぶことをお勧めします。** @@ -271,7 +187,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド ### ルート定義 ルート定義は、以下の形式のオブジェクトの配列です。 -```ts +``` ts { name?: string; path: string; @@ -279,11 +195,12 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド query?: Record; loginRequired?: boolean; hash?: string; + globalCacheKey?: string; children?: RouteDef[]; } ``` -> [!WARNING] +> **Warning** > 現状、ルートは定義された順に評価されます。 > たとえば、`/foo/:id`ルート定義の次に`/foo/bar`ルート定義がされていた場合、後者がマッチすることはありません。 @@ -297,13 +214,30 @@ Misskey uses [Storybook](https://storybook.js.org/) for UI development. ### Setup & Run -#### Setup +#### Universal + +##### Setup + +```bash +pnpm --filter misskey-js build +pnpm --filter frontend tsc -p .storybook && (node packages/frontend/.storybook/preload-locale.js & node packages/frontend/.storybook/preload-theme.js) +``` + +##### Run + +```bash +node packages/frontend/.storybook/generate.js && pnpm --filter frontend storybook dev +``` + +#### macOS & Linux + +##### Setup ```bash pnpm --filter misskey-js build ``` -#### Run +##### Run ```bash pnpm --filter frontend storybook-dev @@ -345,7 +279,7 @@ export const Default = { parameters: { layout: 'centered', }, -} satisfies StoryObj; +} satisfies StoryObj; ``` If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file. @@ -365,127 +299,25 @@ export const argTypes = { min: 1, max: 4, }, - }, }; ``` Also, you can use msw to mock API requests in the storybook. Creating a `MyComponent.stories.msw.ts` file to define the mock handlers. ```ts -import { HttpResponse, http } from 'msw'; +import { rest } from 'msw'; export const handlers = [ - http.post('/api/notes/timeline', ({ request }) => { - return HttpResponse.json([]); + rest.post('/api/notes/timeline', (req, res, ctx) => { + return res( + ctx.json([]), + ); }), ]; ``` Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files. -## Nest - -### Nest Service Circular dependency / Nestでサービスの循環参照でエラーが起きた場合 - -#### forwardRef -まずは簡単に`forwardRef`を試してみる - -```typescript -export class FooService { - constructor( - @Inject(forwardRef(() => BarService)) - private barService: BarService - ) { - } -} -``` - -#### OnModuleInit -できなければ`OnModuleInit`を使う - -```typescript -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { ModuleRef } from '@nestjs/core'; -import { BarService } from '@/core/BarService'; - -@Injectable() -export class FooService implements OnModuleInit { - private barService: BarService // constructorから移動してくる - - constructor( - private moduleRef: ModuleRef, - ) { - } - - async onModuleInit() { - this.barService = this.moduleRef.get(BarService.name); - } - - public async niceMethod() { - return await this.barService.incredibleMethod({ hoge: 'fuga' }); - } -} -``` - -##### Service Unit Test -テストで`onModuleInit`を呼び出す必要がある - -```typescript -// import ... - -describe('test', () => { - let app: TestingModule; - let fooService: FooService; // for test case - let barService: BarService; // for test case - - beforeEach(async () => { - app = await Test.createTestingModule({ - imports: ..., - providers: [ - FooService, - { // mockする (mockは必須ではないかもしれない) - provide: BarService, - useFactory: () => ({ - incredibleMethod: jest.fn(), - }), - }, - { // Provideにする - provide: BarService.name, - useExisting: BarService, - }, - ], - }) - .useMocker(... - .compile(); - - fooService = app.get(FooService); - barService = app.get(BarService) as jest.Mocked; - - // onModuleInitを実行する - await fooService.onModuleInit(); - }); - - test('nice', () => { - await fooService.niceMethod(); - - expect(barService.incredibleMethod).toHaveBeenCalled(); - expect(barService.incredibleMethod.mock.lastCall![0]) - .toEqual({ hoge: 'fuga' }); - }); -}) -``` - ## Notes - -### Misskeyのドメイン固有の概念は`Mi`をprefixする -例えばGoogleが自社サービスをMap、Earth、DriveではなくGoogle Map、Google Earth、Google Driveのように命名するのと同じ -コード上でMisskeyのドメイン固有の概念には`Mi`をprefixすることで、他のドメインの同様の概念と区別できるほか、名前の衝突を防ぐ。 -ただし、文脈上Misskeyのものを指すことが明らかであり、名前の衝突の恐れがない場合は、一時的なローカル変数に限って`Mi`を省略してもよい。 - -### Misskey.jsの型生成 -```bash -pnpm build-misskey-js-with-types -``` - ### How to resolve conflictions occurred at pnpm-lock.yaml? Just execute `pnpm` to fix it. @@ -581,6 +413,27 @@ pnpm dlx typeorm migration:generate -d ormconfig.js -o - 生成後、ファイルをmigration下に移してください - 作成されたスクリプトは不必要な変更を含むため除去してください +### JSON SchemaのobjectでanyOfを使うとき +JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。 +バリデーションが効かないため。(SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます) +https://github.com/misskey-dev/misskey/pull/10082 + +テキストhogeおよびfugaについて、片方を必須としつつ両方の指定もありうる場合: + +``` +export const paramDef = { + type: 'object', + properties: { + hoge: { type: 'string', minLength: 1 }, + fuga: { type: 'string', minLength: 1 }, + }, + anyOf: [ + { required: ['hoge'] }, + { required: ['fuga'] }, + ], +} as const; +``` + ### コネクションには`markRaw`せよ **Vueのコンポーネントのdataオプションとして**misskey.jsのコネクションを設定するとき、必ず`markRaw`でラップしてください。インスタンスが不必要にリアクティブ化されることで、misskey.js内の処理で不具合が発生するとともに、パフォーマンス上の問題にも繋がる。なお、Composition APIを使う場合はこの限りではない(リアクティブ化はマニュアルなため)。 @@ -594,27 +447,3 @@ marginはそのコンポーネントを使う側が設定する ## その他 ### HTMLのクラス名で follow という単語は使わない 広告ブロッカーで誤ってブロックされる - -### indexというファイル名を使うな -ESMではディレクトリインポートは廃止されているのと、ディレクトリインポートせずともファイル名が index だと何故か一部のライブラリ?でディレクトリインポートだと見做されてエラーになる - -## CSS Recipe - -### Lighten CSS vars - -``` css -color: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); -``` - -### Darken CSS vars - -``` css -color: hsl(from var(--MI_THEME-accent) h s calc(l - 10)); -``` - -### Add alpha to CSS vars - -``` css -color: color(from var(--MI_THEME-accent) srgb r g b / 0.5); -``` - diff --git a/COPYING b/COPYING index 7635bfc913..c218443d42 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,5 @@ Unless otherwise stated this repository is -Copyright © 2014-2025 syuilo and contributors +Copyright © 2014-2023 syuilo and contributers And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE. diff --git a/Dockerfile b/Dockerfile index 77277db8cb..fb389659bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=22.15.0-bookworm +ARG NODE_VERSION=18.16.0-bullseye # build assets & compile TypeScript @@ -14,29 +14,24 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ && apt-get install -yqq --no-install-recommends \ build-essential +RUN corepack enable + WORKDIR /misskey COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] COPY --link ["scripts", "./scripts"] COPY --link ["packages/backend/package.json", "./packages/backend/"] -COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-shared/"] COPY --link ["packages/frontend/package.json", "./packages/frontend/"] -COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"] -COPY --link ["packages/icons-subsetter/package.json", "./packages/icons-subsetter/"] COPY --link ["packages/sw/package.json", "./packages/sw/"] COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"] -COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"] -COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"] - -ARG NODE_ENV=production - -RUN node -e "console.log(JSON.parse(require('node:fs').readFileSync('./package.json')).packageManager)" | xargs npm install -g RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --frozen-lockfile --aggregate-output COPY --link . ./ +ARG NODE_ENV=production + RUN git submodule update --init RUN pnpm build RUN rm -rf .git/ @@ -49,18 +44,13 @@ RUN apt-get update \ && apt-get install -yqq --no-install-recommends \ build-essential +RUN corepack enable + WORKDIR /misskey COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] COPY --link ["scripts", "./scripts"] COPY --link ["packages/backend/package.json", "./packages/backend/"] -COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"] -COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"] -COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"] - -ARG NODE_ENV=production - -RUN node -e "console.log(JSON.parse(require('node:fs').readFileSync('./package.json')).packageManager)" | xargs npm install -g RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --frozen-lockfile --aggregate-output @@ -72,36 +62,25 @@ ARG GID="991" RUN apt-get update \ && apt-get install -y --no-install-recommends \ - ffmpeg tini curl libjemalloc-dev libjemalloc2 \ - && ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so \ + ffmpeg tini curl \ + && corepack enable \ && groupadd -g "${GID}" misskey \ && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \ - && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \ - && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; \ + && find / -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \ + && find / -type d -path /proc -prune -o -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; \ && apt-get clean \ && rm -rf /var/lib/apt/lists -# add package.json to add pnpm -COPY ./package.json ./package.json -RUN node -e "console.log(JSON.parse(require('node:fs').readFileSync('./package.json')).packageManager)" | xargs npm install -g - USER misskey WORKDIR /misskey COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules -COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules -COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-reversi/node_modules ./packages/misskey-reversi/node_modules -COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-bubble-game/node_modules ./packages/misskey-bubble-game/node_modules COPY --chown=misskey:misskey --from=native-builder /misskey/built ./built -COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/built ./packages/misskey-js/built -COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built -COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis COPY --chown=misskey:misskey . ./ -ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so ENV NODE_ENV=production HEALTHCHECK --interval=5s --retries=20 CMD ["/bin/bash", "/misskey/healthcheck.sh"] ENTRYPOINT ["/usr/bin/tini", "--"] diff --git a/README.md b/README.md index 92e8fef639..2aae4bb865 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,16 @@
- Misskey logo + Misskey logo - -**🌎 **Misskey** is an open source, federated social media platform that's free forever! 🚀** - -[Learn more](https://misskey-hub.net/) - + +**🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀** + --- - + find an instance - + create an instance @@ -23,15 +21,46 @@ become a patron + +--- + +[![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey)
+
+ + + +## ✨ Features +- **ActivityPub support**\ +Not on Misskey? No problem! Not only can Misskey instances talk to each other, but you can make friends with people on other networks like Mastodon and Pixelfed! +- **Reactions**\ +You can add emoji reactions to any post! No longer are you bound by a like button, show everyone exactly how you feel with the tap of a button. +- **Drive**\ +With Misskey's built in drive, you get cloud storage right in your social media, where you can upload any files, make folders, and find media from posts you've made! +- **Rich Web UI**\ + Misskey has a rich and easy to use Web UI! + It is highly customizable, from changing the layout and adding widgets to making custom themes. + Furthermore, plugins can be created using AiScript, an original programming language. +- And much more... + +
+ +
+ +## Documentation + +Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/), some of the links and graphics above also lead to specific portions of it. + +## Sponsors + +
+ RSS3 +
+ ## Thanks -Sentry - -Thanks to [Sentry](https://sentry.io/) for providing the error tracking platform that helps us catch unexpected errors. - Chromatic Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions. diff --git a/ROADMAP.md b/ROADMAP.md index 509ecb9fe7..420f728758 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,7 +6,6 @@ Also, the later tasks are more indefinite and are subject to change as developme This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development. - ~~Make the number of type errors zero (backend)~~ → Done ✔️ -- Make the number of type errors zero (frontend) - Improve CI - ~~Fix tests~~ → Done ✔️ - Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986 @@ -23,7 +22,7 @@ This is the phase we are at now. We need to make a high-maintenance environment Once Phase 1 is complete and an environment conducive to the development of a stable system is in place, the implementation of new functions can begin gradually. - Improve features for moderation -- ~~OAuth2 support https://github.com/misskey-dev/misskey/issues/8262~~ → Done ✔️ +- OAuth2 support https://github.com/misskey-dev/misskey/issues/8262 - GraphQL support? ## (3) Improve scalability diff --git a/SECURITY.md b/SECURITY.md index 19f5f2eea2..2c026a5f33 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,20 +1,9 @@ # Reporting Security Issues -If you discover a security issue in Misskey, please report it by **[this form](https://github.com/misskey-dev/misskey/security/advisories/new)**. +If you discover a security issue in Misskey, please report it by sending an +email to [syuilotan@yahoo.co.jp](mailto:syuilotan@yahoo.co.jp). This will allow us to assess the risk, and make a fix available before we add a bug report to the GitHub repository. Thanks for helping make Misskey safe for everyone. - -> [!note] -> CNA [requires](https://www.cve.org/ResourcesSupport/AllResources/CNARules#section_5-2_Description) that CVEs include a description in English for inclusion in the CVE Catalog. -> -> When creating a security advisory, all content must be written in English (it is acceptable to include a non-English description along with the English one). - -## When create a patch - -If you can also create a patch to fix the vulnerability, please create a PR on the private fork. - -> [!note] -> There is a GitHub bug that prevents merging if a PR not following the develop branch of upstream, so please keep follow the develop branch. diff --git a/assets/about/drive.png b/assets/about/drive.png new file mode 100644 index 0000000000..16037aae39 Binary files /dev/null and b/assets/about/drive.png differ diff --git a/assets/about/post.png b/assets/about/post.png new file mode 100644 index 0000000000..3c55f66c56 Binary files /dev/null and b/assets/about/post.png differ diff --git a/assets/about/reaction.png b/assets/about/reaction.png new file mode 100644 index 0000000000..e4e7e06bc0 Binary files /dev/null and b/assets/about/reaction.png differ diff --git a/assets/about/ui.png b/assets/about/ui.png new file mode 100644 index 0000000000..0601837f4c Binary files /dev/null and b/assets/about/ui.png differ diff --git a/assets/ss/explore.jpg b/assets/ss/explore.jpg new file mode 100644 index 0000000000..bf81d794c3 Binary files /dev/null and b/assets/ss/explore.jpg differ diff --git a/assets/ss/user.jpg b/assets/ss/user.jpg new file mode 100644 index 0000000000..3ec595c199 Binary files /dev/null and b/assets/ss/user.jpg differ diff --git a/assets/title_float.svg b/assets/title_float.svg index ed1749e321..43205ac1c4 100644 --- a/assets/title_float.svg +++ b/assets/title_float.svg @@ -23,13 +23,13 @@ - - - - - - diff --git a/packages/backend/assets/avatar.png b/packages/backend/assets/avatar.png deleted file mode 100644 index 1b95a0c560..0000000000 Binary files a/packages/backend/assets/avatar.png and /dev/null differ diff --git a/packages/backend/assets/embed.js b/packages/backend/assets/embed.js deleted file mode 100644 index 24fccc1b6c..0000000000 --- a/packages/backend/assets/embed.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: MIT - */ -//@ts-check -(() => { - /** @type {NodeListOf} */ - const els = document.querySelectorAll('iframe[data-misskey-embed-id]'); - - window.addEventListener('message', function (event) { - els.forEach((el) => { - if (event.source !== el.contentWindow) { - return; - } - - const id = el.dataset.misskeyEmbedId; - - if (event.data.type === 'misskey:embed:ready') { - el.contentWindow?.postMessage({ - type: 'misskey:embedParent:registerIframeId', - payload: { - iframeId: id, - } - }, '*'); - } - if (event.data.type === 'misskey:embed:changeHeight' && event.data.iframeId === id) { - el.style.height = event.data.payload.height + 'px'; - } - }); - }); -})(); diff --git a/packages/backend/assets/redoc.html b/packages/backend/assets/redoc.html new file mode 100644 index 0000000000..a9ebf662fc --- /dev/null +++ b/packages/backend/assets/redoc.html @@ -0,0 +1,24 @@ + + + + Misskey API + + + + + + + + + + + + + diff --git a/packages/backend/assets/tabler-badges/bell.png b/packages/backend/assets/tabler-badges/bell.png deleted file mode 100644 index ab3b2a110f..0000000000 Binary files a/packages/backend/assets/tabler-badges/bell.png and /dev/null differ diff --git a/packages/backend/assets/tabler-badges/login-2.png b/packages/backend/assets/tabler-badges/login-2.png deleted file mode 100644 index f3ca8de3dd..0000000000 Binary files a/packages/backend/assets/tabler-badges/login-2.png and /dev/null differ diff --git a/packages/backend/check_connect.js b/packages/backend/check_connect.js new file mode 100644 index 0000000000..ef0a350fbf --- /dev/null +++ b/packages/backend/check_connect.js @@ -0,0 +1,17 @@ +import Redis from 'ioredis'; +import { loadConfig } from './built/config.js'; + +const config = loadConfig(); +const redis = new Redis({ + port: config.redis.port, + host: config.redis.host, + family: config.redis.family == null ? 0 : config.redis.family, + password: config.redis.pass, + keyPrefix: `${config.redis.prefix}:`, + db: config.redis.db ?? 0, +}); + +redis.on('connect', () => redis.disconnect()); +redis.on('error', (e) => { + throw e; +}); diff --git a/packages/backend/eslint.config.js b/packages/backend/eslint.config.js deleted file mode 100644 index d15a703ba2..0000000000 --- a/packages/backend/eslint.config.js +++ /dev/null @@ -1,54 +0,0 @@ -import tsParser from '@typescript-eslint/parser'; -import globals from 'globals'; -import sharedConfig from '../shared/eslint.config.js'; - -export default [ - ...sharedConfig, - { - ignores: ['**/node_modules', 'built', '@types/**/*', 'migration'], - }, - { - languageOptions: { - globals: { - ...globals.node, - }, - }, - }, - { - files: ['**/*.ts', '**/*.tsx'], - languageOptions: { - parserOptions: { - parser: tsParser, - project: ['./tsconfig.json', './test/tsconfig.json', './test-federation/tsconfig.json'], - sourceType: 'module', - tsconfigRootDir: import.meta.dirname, - }, - }, - rules: { - 'import/order': ['warn', { - groups: [ - 'builtin', - 'external', - 'internal', - 'parent', - 'sibling', - 'index', - 'object', - 'type', - ], - pathGroups: [{ - pattern: '@/**', - group: 'external', - position: 'after', - }], - }], - 'no-restricted-globals': ['error', { - name: '__dirname', - message: 'Not in ESModule. Use `import.meta.url` instead.', - }, { - name: '__filename', - message: 'Not in ESModule. Use `import.meta.url` instead.', - }], - }, - }, -]; diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index 5a4aa4e15a..6b1afec734 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -160,6 +160,7 @@ module.exports = { testMatch: [ "/test/unit/**/*.ts", "/src/**/*.test.ts", + "/test/e2e/**/*.ts", ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped @@ -215,6 +216,4 @@ module.exports = { maxWorkers: 1, // Make it use worker (that can be killed and restarted) logHeapUsage: true, // To debug when out-of-memory happens on CI workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB) - - maxConcurrency: 32, }; diff --git a/packages/backend/jest.config.e2e.cjs b/packages/backend/jest.config.e2e.cjs deleted file mode 100644 index 4502da47df..0000000000 --- a/packages/backend/jest.config.e2e.cjs +++ /dev/null @@ -1,15 +0,0 @@ -/* -* For a detailed explanation regarding each configuration property and type check, visit: -* https://jestjs.io/docs/en/configuration.html -*/ - -const base = require('./jest.config.cjs') - -module.exports = { - ...base, - globalSetup: "/built-test/entry.js", - setupFilesAfterEnv: ["/test/jest.setup.ts"], - testMatch: [ - "/test/e2e/**/*.ts", - ], -}; diff --git a/packages/backend/jest.config.fed.cjs b/packages/backend/jest.config.fed.cjs deleted file mode 100644 index fae187bc23..0000000000 --- a/packages/backend/jest.config.fed.cjs +++ /dev/null @@ -1,13 +0,0 @@ -/* - * For a detailed explanation regarding each configuration property and type check, visit: - * https://jestjs.io/docs/en/configuration.html - */ - -const base = require('./jest.config.cjs'); - -module.exports = { - ...base, - testMatch: [ - '/test-federation/test/**/*.test.ts', - ], -}; diff --git a/packages/backend/jest.config.unit.cjs b/packages/backend/jest.config.unit.cjs deleted file mode 100644 index aa5992936b..0000000000 --- a/packages/backend/jest.config.unit.cjs +++ /dev/null @@ -1,14 +0,0 @@ -/* -* For a detailed explanation regarding each configuration property and type check, visit: -* https://jestjs.io/docs/en/configuration.html -*/ - -const base = require('./jest.config.cjs') - -module.exports = { - ...base, - testMatch: [ - "/test/unit/**/*.ts", - "/src/**/*.test.ts", - ], -}; diff --git a/packages/backend/jest.js b/packages/backend/jest.js deleted file mode 100644 index 0e761d8c92..0000000000 --- a/packages/backend/jest.js +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env node -import child_process from 'node:child_process'; -import path from 'node:path'; -import url from 'node:url'; - -import semver from 'semver'; - -const __filename = url.fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const args = []; -args.push(...[ - ...semver.satisfies(process.version, '^20.17.0 || ^22.0.0') ? ['--no-experimental-require-module'] : [], - '--experimental-vm-modules', - '--experimental-import-meta-resolve', - path.join(__dirname, 'node_modules/jest/bin/jest.js'), - ...process.argv.slice(2), -]); - -const child = child_process.spawn(process.execPath, args, { stdio: 'inherit' }); -child.on('error', (err) => { - console.error('Failed to start Jest:', err); - process.exit(1); -}); -child.on('exit', (code, signal) => { - if (code === null) { - process.exit(128 + signal); - } else { - process.exit(code); - } -}); diff --git a/packages/backend/migration/1000000000000-Init.js b/packages/backend/migration/1000000000000-Init.js index c06885fd40..1140be7e84 100644 --- a/packages/backend/migration/1000000000000-Init.js +++ b/packages/backend/migration/1000000000000-Init.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class Init1000000000000 { async up(queryRunner) { diff --git a/packages/backend/migration/1556348509290-Pages.js b/packages/backend/migration/1556348509290-Pages.js index c7542e808c..50caa2ce91 100644 --- a/packages/backend/migration/1556348509290-Pages.js +++ b/packages/backend/migration/1556348509290-Pages.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class Pages1556348509290 { async up(queryRunner) { diff --git a/packages/backend/migration/1556746559567-UserProfile.js b/packages/backend/migration/1556746559567-UserProfile.js index 13ff6ce6bf..50a9d1a8be 100644 --- a/packages/backend/migration/1556746559567-UserProfile.js +++ b/packages/backend/migration/1556746559567-UserProfile.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class UserProfile1556746559567 { async up(queryRunner) { diff --git a/packages/backend/migration/1557476068003-PinnedUsers.js b/packages/backend/migration/1557476068003-PinnedUsers.js index f2f1deae2f..d9cce25435 100644 --- a/packages/backend/migration/1557476068003-PinnedUsers.js +++ b/packages/backend/migration/1557476068003-PinnedUsers.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class PinnedUsers1557476068003 { async up(queryRunner) { diff --git a/packages/backend/migration/1557761316509-AddSomeUrls.js b/packages/backend/migration/1557761316509-AddSomeUrls.js index eaf78b85b6..ab8736f7cc 100644 --- a/packages/backend/migration/1557761316509-AddSomeUrls.js +++ b/packages/backend/migration/1557761316509-AddSomeUrls.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class AddSomeUrls1557761316509 { async up(queryRunner) { diff --git a/packages/backend/migration/1557932705754-ObjectStorageSetting.js b/packages/backend/migration/1557932705754-ObjectStorageSetting.js index 0e1ef321ab..19a0b9d5cd 100644 --- a/packages/backend/migration/1557932705754-ObjectStorageSetting.js +++ b/packages/backend/migration/1557932705754-ObjectStorageSetting.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class ObjectStorageSetting1557932705754 { async up(queryRunner) { diff --git a/packages/backend/migration/1558072954435-PageLike.js b/packages/backend/migration/1558072954435-PageLike.js index a08f68a0e6..31b08418a9 100644 --- a/packages/backend/migration/1558072954435-PageLike.js +++ b/packages/backend/migration/1558072954435-PageLike.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class PageLike1558072954435 { async up(queryRunner) { diff --git a/packages/backend/migration/1558103093633-UserGroup.js b/packages/backend/migration/1558103093633-UserGroup.js index f762dc2371..b670b31c3d 100644 --- a/packages/backend/migration/1558103093633-UserGroup.js +++ b/packages/backend/migration/1558103093633-UserGroup.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class UserGroup1558103093633 { async up(queryRunner) { diff --git a/packages/backend/migration/1558257926829-UserGroupInvite.js b/packages/backend/migration/1558257926829-UserGroupInvite.js index 853b52d17d..e48bd3a7ff 100644 --- a/packages/backend/migration/1558257926829-UserGroupInvite.js +++ b/packages/backend/migration/1558257926829-UserGroupInvite.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class UserGroupInvite1558257926829 { async up(queryRunner) { diff --git a/packages/backend/migration/1558266512381-UserListJoining.js b/packages/backend/migration/1558266512381-UserListJoining.js index e161d52f12..3398aed139 100644 --- a/packages/backend/migration/1558266512381-UserListJoining.js +++ b/packages/backend/migration/1558266512381-UserListJoining.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class UserListJoining1558266512381 { async up(queryRunner) { diff --git a/packages/backend/migration/1561706992953-webauthn.js b/packages/backend/migration/1561706992953-webauthn.js index 4c81035ff1..b007ffef14 100644 --- a/packages/backend/migration/1561706992953-webauthn.js +++ b/packages/backend/migration/1561706992953-webauthn.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class webauthn1561706992953 { async up(queryRunner) { diff --git a/packages/backend/migration/1561873850023-ChartIndexes.js b/packages/backend/migration/1561873850023-ChartIndexes.js index 3f190ce143..3ce53567fc 100644 --- a/packages/backend/migration/1561873850023-ChartIndexes.js +++ b/packages/backend/migration/1561873850023-ChartIndexes.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class ChartIndexes1561873850023 { async up(queryRunner) { diff --git a/packages/backend/migration/1562422242907-PasswordLessLogin.js b/packages/backend/migration/1562422242907-PasswordLessLogin.js index 4c0fbbbc9f..b73c7db4d3 100644 --- a/packages/backend/migration/1562422242907-PasswordLessLogin.js +++ b/packages/backend/migration/1562422242907-PasswordLessLogin.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class PasswordLessLogin1562422242907 { async up(queryRunner) { diff --git a/packages/backend/migration/1562444565093-PinnedPage.js b/packages/backend/migration/1562444565093-PinnedPage.js index 89639399f0..9a999a9150 100644 --- a/packages/backend/migration/1562444565093-PinnedPage.js +++ b/packages/backend/migration/1562444565093-PinnedPage.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class PinnedPage1562444565093 { async up(queryRunner) { diff --git a/packages/backend/migration/1562448332510-PageTitleHideOption.js b/packages/backend/migration/1562448332510-PageTitleHideOption.js index 70d54aa777..8fc78d202f 100644 --- a/packages/backend/migration/1562448332510-PageTitleHideOption.js +++ b/packages/backend/migration/1562448332510-PageTitleHideOption.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class PageTitleHideOption1562448332510 { async up(queryRunner) { diff --git a/packages/backend/migration/1562869971568-ModerationLog.js b/packages/backend/migration/1562869971568-ModerationLog.js index 3dd9b22edf..dd66d16eec 100644 --- a/packages/backend/migration/1562869971568-ModerationLog.js +++ b/packages/backend/migration/1562869971568-ModerationLog.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class ModerationLog1562869971568 { async up(queryRunner) { diff --git a/packages/backend/migration/1563757595828-UsedUsername.js b/packages/backend/migration/1563757595828-UsedUsername.js index 258e5abab2..8972df297d 100644 --- a/packages/backend/migration/1563757595828-UsedUsername.js +++ b/packages/backend/migration/1563757595828-UsedUsername.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class UsedUsername1563757595828 { async up(queryRunner) { diff --git a/packages/backend/migration/1565634203341-room.js b/packages/backend/migration/1565634203341-room.js index 04c9749c1b..679940f244 100644 --- a/packages/backend/migration/1565634203341-room.js +++ b/packages/backend/migration/1565634203341-room.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class room1565634203341 { async up(queryRunner) { diff --git a/packages/backend/migration/1571220798684-CustomEmojiCategory.js b/packages/backend/migration/1571220798684-CustomEmojiCategory.js index 1fc78a65ff..37c07366e1 100644 --- a/packages/backend/migration/1571220798684-CustomEmojiCategory.js +++ b/packages/backend/migration/1571220798684-CustomEmojiCategory.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class CustomEmojiCategory1571220798684 { async up(queryRunner) { diff --git a/packages/backend/migration/1572760203493-nodeinfo.js b/packages/backend/migration/1572760203493-nodeinfo.js index ea7a67bc3e..54d5f914a4 100644 --- a/packages/backend/migration/1572760203493-nodeinfo.js +++ b/packages/backend/migration/1572760203493-nodeinfo.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class nodeinfo1572760203493 { async up(queryRunner) { diff --git a/packages/backend/migration/1576269851876-TalkFederationId.js b/packages/backend/migration/1576269851876-TalkFederationId.js index c49c716e7a..35861d571f 100644 --- a/packages/backend/migration/1576269851876-TalkFederationId.js +++ b/packages/backend/migration/1576269851876-TalkFederationId.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class TalkFederationId1576269851876 { constructor() { diff --git a/packages/backend/migration/1576869585998-ProxyRemoteFiles.js b/packages/backend/migration/1576869585998-ProxyRemoteFiles.js index 192dbe3485..d6d134be40 100644 --- a/packages/backend/migration/1576869585998-ProxyRemoteFiles.js +++ b/packages/backend/migration/1576869585998-ProxyRemoteFiles.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class ProxyRemoteFiles1576869585998 { constructor() { diff --git a/packages/backend/migration/1579267006611-v12.js b/packages/backend/migration/1579267006611-v12.js index 9267be5630..7f6318a192 100644 --- a/packages/backend/migration/1579267006611-v12.js +++ b/packages/backend/migration/1579267006611-v12.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class v121579267006611 { constructor() { diff --git a/packages/backend/migration/1579270193251-v12-2.js b/packages/backend/migration/1579270193251-v12-2.js index e2ca9709ea..c51ce63066 100644 --- a/packages/backend/migration/1579270193251-v12-2.js +++ b/packages/backend/migration/1579270193251-v12-2.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class v1221579270193251 { constructor() { diff --git a/packages/backend/migration/1579282808087-v12-3.js b/packages/backend/migration/1579282808087-v12-3.js index 4098f041c8..aeb4f5a873 100644 --- a/packages/backend/migration/1579282808087-v12-3.js +++ b/packages/backend/migration/1579282808087-v12-3.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class v1231579282808087 { constructor() { diff --git a/packages/backend/migration/1579544426412-v12-4.js b/packages/backend/migration/1579544426412-v12-4.js index 1153993f35..f1e093413e 100644 --- a/packages/backend/migration/1579544426412-v12-4.js +++ b/packages/backend/migration/1579544426412-v12-4.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class v1241579544426412 { constructor() { diff --git a/packages/backend/migration/1579977526288-v12-5.js b/packages/backend/migration/1579977526288-v12-5.js index d9e1b48bb2..6d2b5c584a 100644 --- a/packages/backend/migration/1579977526288-v12-5.js +++ b/packages/backend/migration/1579977526288-v12-5.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class v1251579977526288 { constructor() { diff --git a/packages/backend/migration/1579993013959-v12-6.js b/packages/backend/migration/1579993013959-v12-6.js index 9c249422a2..3941c1391d 100644 --- a/packages/backend/migration/1579993013959-v12-6.js +++ b/packages/backend/migration/1579993013959-v12-6.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class v1261579993013959 { constructor() { diff --git a/packages/backend/migration/1580069531114-v12-7.js b/packages/backend/migration/1580069531114-v12-7.js index ceee6b2031..4b4790cb7d 100644 --- a/packages/backend/migration/1580069531114-v12-7.js +++ b/packages/backend/migration/1580069531114-v12-7.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class v1271580069531114 { constructor() { diff --git a/packages/backend/migration/1580148575182-v12-8.js b/packages/backend/migration/1580148575182-v12-8.js index 6841dcc38f..cc30200c14 100644 --- a/packages/backend/migration/1580148575182-v12-8.js +++ b/packages/backend/migration/1580148575182-v12-8.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class v1281580148575182 { constructor() { diff --git a/packages/backend/migration/1580154400017-v12-9.js b/packages/backend/migration/1580154400017-v12-9.js index c01d8089d0..3715798f19 100644 --- a/packages/backend/migration/1580154400017-v12-9.js +++ b/packages/backend/migration/1580154400017-v12-9.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class v1291580154400017 { constructor() { diff --git a/packages/backend/migration/1580276619901-v12-10.js b/packages/backend/migration/1580276619901-v12-10.js index be6e467fab..d5decb882e 100644 --- a/packages/backend/migration/1580276619901-v12-10.js +++ b/packages/backend/migration/1580276619901-v12-10.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class v12101580276619901 { constructor() { diff --git a/packages/backend/migration/1580331224276-v12-11.js b/packages/backend/migration/1580331224276-v12-11.js index af817a8c8a..129720adbf 100644 --- a/packages/backend/migration/1580331224276-v12-11.js +++ b/packages/backend/migration/1580331224276-v12-11.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class v12111580331224276 { constructor() { diff --git a/packages/backend/migration/1580508795118-v12-12.js b/packages/backend/migration/1580508795118-v12-12.js index 4bd855f7ab..c5cec23a36 100644 --- a/packages/backend/migration/1580508795118-v12-12.js +++ b/packages/backend/migration/1580508795118-v12-12.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class v12121580508795118 { constructor() { diff --git a/packages/backend/migration/1580543501339-v12-13.js b/packages/backend/migration/1580543501339-v12-13.js index be76c02163..2fa490392d 100644 --- a/packages/backend/migration/1580543501339-v12-13.js +++ b/packages/backend/migration/1580543501339-v12-13.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class v12131580543501339 { constructor() { diff --git a/packages/backend/migration/1580864313253-v12-14.js b/packages/backend/migration/1580864313253-v12-14.js index f8891a2b66..a3756ad029 100644 --- a/packages/backend/migration/1580864313253-v12-14.js +++ b/packages/backend/migration/1580864313253-v12-14.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class v12141580864313253 { constructor() { diff --git a/packages/backend/migration/1581526429287-user-group-invitation.js b/packages/backend/migration/1581526429287-user-group-invitation.js index 51703e2ba1..181b0aba86 100644 --- a/packages/backend/migration/1581526429287-user-group-invitation.js +++ b/packages/backend/migration/1581526429287-user-group-invitation.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class userGroupInvitation1581526429287 { constructor() { diff --git a/packages/backend/migration/1581695816408-user-group-antenna.js b/packages/backend/migration/1581695816408-user-group-antenna.js index e6791ba1a4..267b58cd9b 100644 --- a/packages/backend/migration/1581695816408-user-group-antenna.js +++ b/packages/backend/migration/1581695816408-user-group-antenna.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class userGroupAntenna1581695816408 { constructor() { diff --git a/packages/backend/migration/1581708415836-drive-user-folder-id-index.js b/packages/backend/migration/1581708415836-drive-user-folder-id-index.js index 28ce4cc142..43c2ce6cee 100644 --- a/packages/backend/migration/1581708415836-drive-user-folder-id-index.js +++ b/packages/backend/migration/1581708415836-drive-user-folder-id-index.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class driveUserFolderIdIndex1581708415836 { constructor() { diff --git a/packages/backend/migration/1581979837262-promo.js b/packages/backend/migration/1581979837262-promo.js index 707c85fcb3..4813a5f480 100644 --- a/packages/backend/migration/1581979837262-promo.js +++ b/packages/backend/migration/1581979837262-promo.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class promo1581979837262 { constructor() { diff --git a/packages/backend/migration/1582019042083-featured-injecttion.js b/packages/backend/migration/1582019042083-featured-injecttion.js index f308f0a454..7f8790b01b 100644 --- a/packages/backend/migration/1582019042083-featured-injecttion.js +++ b/packages/backend/migration/1582019042083-featured-injecttion.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class featuredInjecttion1582019042083 { constructor() { diff --git a/packages/backend/migration/1582210532752-antenna-exclude.js b/packages/backend/migration/1582210532752-antenna-exclude.js index 9b87e3ff39..ff8d7b80d8 100644 --- a/packages/backend/migration/1582210532752-antenna-exclude.js +++ b/packages/backend/migration/1582210532752-antenna-exclude.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class antennaExclude1582210532752 { constructor() { diff --git a/packages/backend/migration/1582875306439-note-reaction-length.js b/packages/backend/migration/1582875306439-note-reaction-length.js index e801d1ac44..e99501f012 100644 --- a/packages/backend/migration/1582875306439-note-reaction-length.js +++ b/packages/backend/migration/1582875306439-note-reaction-length.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class noteReactionLength1582875306439 { constructor() { diff --git a/packages/backend/migration/1585361548360-miauth.js b/packages/backend/migration/1585361548360-miauth.js index d5932c6083..e59aa3b6ef 100644 --- a/packages/backend/migration/1585361548360-miauth.js +++ b/packages/backend/migration/1585361548360-miauth.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class miauth1585361548360 { constructor() { diff --git a/packages/backend/migration/1585385921215-custom-notification.js b/packages/backend/migration/1585385921215-custom-notification.js index 35303b99e9..c3ddb2be17 100644 --- a/packages/backend/migration/1585385921215-custom-notification.js +++ b/packages/backend/migration/1585385921215-custom-notification.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class customNotification1585385921215 { constructor() { diff --git a/packages/backend/migration/1585772678853-ap-url.js b/packages/backend/migration/1585772678853-ap-url.js index f978fc80b4..5fb809ff53 100644 --- a/packages/backend/migration/1585772678853-ap-url.js +++ b/packages/backend/migration/1585772678853-ap-url.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class apUrl1585772678853 { constructor() { diff --git a/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js b/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js index fde8629bba..e13bb217e3 100644 --- a/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js +++ b/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class AddObjectStorageUseProxy1586624197029 { constructor() { diff --git a/packages/backend/migration/1586641139527-remote-reaction.js b/packages/backend/migration/1586641139527-remote-reaction.js index 3e907af5f1..5b23103a17 100644 --- a/packages/backend/migration/1586641139527-remote-reaction.js +++ b/packages/backend/migration/1586641139527-remote-reaction.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class remoteReaction1586641139527 { constructor() { diff --git a/packages/backend/migration/1586708940386-pageAiScript.js b/packages/backend/migration/1586708940386-pageAiScript.js index ce5007cea1..eed616c111 100644 --- a/packages/backend/migration/1586708940386-pageAiScript.js +++ b/packages/backend/migration/1586708940386-pageAiScript.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class pageAiScript1586708940386 { constructor() { diff --git a/packages/backend/migration/1588044505511-hCaptcha.js b/packages/backend/migration/1588044505511-hCaptcha.js index aeacb653b3..a33dbd7133 100644 --- a/packages/backend/migration/1588044505511-hCaptcha.js +++ b/packages/backend/migration/1588044505511-hCaptcha.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class hCaptcha1588044505511 { constructor() { diff --git a/packages/backend/migration/1589023282116-pubRelay.js b/packages/backend/migration/1589023282116-pubRelay.js index 8739adb733..48a1028d39 100644 --- a/packages/backend/migration/1589023282116-pubRelay.js +++ b/packages/backend/migration/1589023282116-pubRelay.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class pubRelay1589023282116 { constructor() { diff --git a/packages/backend/migration/1595075960584-blurhash.js b/packages/backend/migration/1595075960584-blurhash.js index 9752625cd2..f24d3722cf 100644 --- a/packages/backend/migration/1595075960584-blurhash.js +++ b/packages/backend/migration/1595075960584-blurhash.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class blurhash1595075960584 { constructor() { diff --git a/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js b/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js index fdff8c633a..f18f6f972a 100644 --- a/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js +++ b/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class blurhashForAvatarBanner1595077605646 { constructor() { diff --git a/packages/backend/migration/1595676934834-instance-icon-url.js b/packages/backend/migration/1595676934834-instance-icon-url.js index 5f834064c4..df9d8199bd 100644 --- a/packages/backend/migration/1595676934834-instance-icon-url.js +++ b/packages/backend/migration/1595676934834-instance-icon-url.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class instanceIconUrl1595676934834 { constructor() { diff --git a/packages/backend/migration/1595771249699-word-mute.js b/packages/backend/migration/1595771249699-word-mute.js index f4fa1227e3..e8e4ac838b 100644 --- a/packages/backend/migration/1595771249699-word-mute.js +++ b/packages/backend/migration/1595771249699-word-mute.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class wordMute1595771249699 { constructor() { diff --git a/packages/backend/migration/1595782306083-word-mute2.js b/packages/backend/migration/1595782306083-word-mute2.js index 3c2062ec07..ab1e40a041 100644 --- a/packages/backend/migration/1595782306083-word-mute2.js +++ b/packages/backend/migration/1595782306083-word-mute2.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class wordMute21595782306083 { constructor() { diff --git a/packages/backend/migration/1596548170836-channel.js b/packages/backend/migration/1596548170836-channel.js index ee6753a476..242db7d45a 100644 --- a/packages/backend/migration/1596548170836-channel.js +++ b/packages/backend/migration/1596548170836-channel.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class channel1596548170836 { constructor() { diff --git a/packages/backend/migration/1596786425167-channel2.js b/packages/backend/migration/1596786425167-channel2.js index 9e6ead4378..4b17048fef 100644 --- a/packages/backend/migration/1596786425167-channel2.js +++ b/packages/backend/migration/1596786425167-channel2.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class channel21596786425167 { constructor() { diff --git a/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js b/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js index bc32d4a052..07283e31df 100644 --- a/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js +++ b/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class objectStorageSetPublicRead1597230137744 { constructor() { diff --git a/packages/backend/migration/1597236229720-IncludingNotificationTypes.js b/packages/backend/migration/1597236229720-IncludingNotificationTypes.js index 99686bd70e..f498fa7d9a 100644 --- a/packages/backend/migration/1597236229720-IncludingNotificationTypes.js +++ b/packages/backend/migration/1597236229720-IncludingNotificationTypes.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class IncludingNotificationTypes1597236229720 { constructor() { diff --git a/packages/backend/migration/1597385880794-add-sensitive-index.js b/packages/backend/migration/1597385880794-add-sensitive-index.js index a67810880b..8c5c040ba0 100644 --- a/packages/backend/migration/1597385880794-add-sensitive-index.js +++ b/packages/backend/migration/1597385880794-add-sensitive-index.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class addSensitiveIndex1597385880794 { constructor() { diff --git a/packages/backend/migration/1597459042300-channel-unread.js b/packages/backend/migration/1597459042300-channel-unread.js index ced9b5265a..3157ab7793 100644 --- a/packages/backend/migration/1597459042300-channel-unread.js +++ b/packages/backend/migration/1597459042300-channel-unread.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class channelUnread1597459042300 { constructor() { diff --git a/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js b/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js index ca4eba385e..2bd8aee358 100644 --- a/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js +++ b/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class ChannelNoteIdDescIndex1597893996136 { constructor() { diff --git a/packages/backend/migration/1600353287890-mutingNotificationTypes.js b/packages/backend/migration/1600353287890-mutingNotificationTypes.js index 0996aa21f6..ed3eb7d146 100644 --- a/packages/backend/migration/1600353287890-mutingNotificationTypes.js +++ b/packages/backend/migration/1600353287890-mutingNotificationTypes.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class mutingNotificationTypes1600353287890 { constructor() { diff --git a/packages/backend/migration/1603094348345-refine-abuse-user-report.js b/packages/backend/migration/1603094348345-refine-abuse-user-report.js index 354915b165..4918032a2b 100644 --- a/packages/backend/migration/1603094348345-refine-abuse-user-report.js +++ b/packages/backend/migration/1603094348345-refine-abuse-user-report.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class refineAbuseUserReport1603094348345 { constructor() { diff --git a/packages/backend/migration/1603095701770-refine-abuse-user-report2.js b/packages/backend/migration/1603095701770-refine-abuse-user-report2.js index 75dd3513b5..64e92672f2 100644 --- a/packages/backend/migration/1603095701770-refine-abuse-user-report2.js +++ b/packages/backend/migration/1603095701770-refine-abuse-user-report2.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class refineAbuseUserReport21603095701770 { constructor() { diff --git a/packages/backend/migration/1603776877564-instance-theme-color.js b/packages/backend/migration/1603776877564-instance-theme-color.js index c8ab89ab56..92440d3f64 100644 --- a/packages/backend/migration/1603776877564-instance-theme-color.js +++ b/packages/backend/migration/1603776877564-instance-theme-color.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class instanceThemeColor1603776877564 { constructor() { diff --git a/packages/backend/migration/1603781553011-instance-favicon.js b/packages/backend/migration/1603781553011-instance-favicon.js index 7d793d4f1f..f607c49ffb 100644 --- a/packages/backend/migration/1603781553011-instance-favicon.js +++ b/packages/backend/migration/1603781553011-instance-favicon.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class instanceFavicon1603781553011 { constructor() { diff --git a/packages/backend/migration/1604821689616-delete-auto-watch.js b/packages/backend/migration/1604821689616-delete-auto-watch.js index 8160877038..4706e8bae9 100644 --- a/packages/backend/migration/1604821689616-delete-auto-watch.js +++ b/packages/backend/migration/1604821689616-delete-auto-watch.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class deleteAutoWatch1604821689616 { constructor() { diff --git a/packages/backend/migration/1605408848373-clip-description.js b/packages/backend/migration/1605408848373-clip-description.js index 77a218791c..edd5505b30 100644 --- a/packages/backend/migration/1605408848373-clip-description.js +++ b/packages/backend/migration/1605408848373-clip-description.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class clipDescription1605408848373 { constructor() { diff --git a/packages/backend/migration/1605408971051-comments.js b/packages/backend/migration/1605408971051-comments.js index 494bfb7950..400efd5e70 100644 --- a/packages/backend/migration/1605408971051-comments.js +++ b/packages/backend/migration/1605408971051-comments.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class comments1605408971051 { constructor() { diff --git a/packages/backend/migration/1605585339718-instance-pinned-pages.js b/packages/backend/migration/1605585339718-instance-pinned-pages.js index 15a0cecd19..56ccd44c8e 100644 --- a/packages/backend/migration/1605585339718-instance-pinned-pages.js +++ b/packages/backend/migration/1605585339718-instance-pinned-pages.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class instancePinnedPages1605585339718 { constructor() { diff --git a/packages/backend/migration/1605965516823-instance-images.js b/packages/backend/migration/1605965516823-instance-images.js index 9cc2eb4032..710c75981d 100644 --- a/packages/backend/migration/1605965516823-instance-images.js +++ b/packages/backend/migration/1605965516823-instance-images.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class instanceImages1605965516823 { constructor() { diff --git a/packages/backend/migration/1606191203881-no-crawle.js b/packages/backend/migration/1606191203881-no-crawle.js index af04566eaa..b9ada4354e 100644 --- a/packages/backend/migration/1606191203881-no-crawle.js +++ b/packages/backend/migration/1606191203881-no-crawle.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class noCrawle1606191203881 { constructor() { diff --git a/packages/backend/migration/1607151207216-instance-pinned-clip.js b/packages/backend/migration/1607151207216-instance-pinned-clip.js index f85c3d42d7..9a4195e74c 100644 --- a/packages/backend/migration/1607151207216-instance-pinned-clip.js +++ b/packages/backend/migration/1607151207216-instance-pinned-clip.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class instancePinnedClip1607151207216 { constructor() { diff --git a/packages/backend/migration/1607353487793-isExplorable.js b/packages/backend/migration/1607353487793-isExplorable.js index e07fe6c306..d9f1ff4c69 100644 --- a/packages/backend/migration/1607353487793-isExplorable.js +++ b/packages/backend/migration/1607353487793-isExplorable.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class isExplorable1607353487793 { constructor() { diff --git a/packages/backend/migration/1610277136869-registry.js b/packages/backend/migration/1610277136869-registry.js index 1a10f23590..184c062ddb 100644 --- a/packages/backend/migration/1610277136869-registry.js +++ b/packages/backend/migration/1610277136869-registry.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class registry1610277136869 { constructor() { diff --git a/packages/backend/migration/1610277585759-registry2.js b/packages/backend/migration/1610277585759-registry2.js index 46e56279f4..591bafae31 100644 --- a/packages/backend/migration/1610277585759-registry2.js +++ b/packages/backend/migration/1610277585759-registry2.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class registry21610277585759 { constructor() { diff --git a/packages/backend/migration/1610283021566-registry3.js b/packages/backend/migration/1610283021566-registry3.js index 402040f38b..e0289f17ee 100644 --- a/packages/backend/migration/1610283021566-registry3.js +++ b/packages/backend/migration/1610283021566-registry3.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class registry31610283021566 { constructor() { diff --git a/packages/backend/migration/1611354329133-followersUri.js b/packages/backend/migration/1611354329133-followersUri.js index 15abb2a9d1..669ddb480e 100644 --- a/packages/backend/migration/1611354329133-followersUri.js +++ b/packages/backend/migration/1611354329133-followersUri.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class followersUri1611354329133 { constructor() { diff --git a/packages/backend/migration/1611397665007-gallery.js b/packages/backend/migration/1611397665007-gallery.js index cbd2b62c56..f49b2df468 100644 --- a/packages/backend/migration/1611397665007-gallery.js +++ b/packages/backend/migration/1611397665007-gallery.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class gallery1611397665007 { constructor() { diff --git a/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js b/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js index c5440b7a48..e4d3c0e8ec 100644 --- a/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js +++ b/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class objectStorageS3ForcePathStyle1611547387175 { constructor() { diff --git a/packages/backend/migration/1612619156584-announcement-email.js b/packages/backend/migration/1612619156584-announcement-email.js index ddacab322b..bcc718d1c2 100644 --- a/packages/backend/migration/1612619156584-announcement-email.js +++ b/packages/backend/migration/1612619156584-announcement-email.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class announcementEmail1612619156584 { constructor() { diff --git a/packages/backend/migration/1613155914446-emailNotificationTypes.js b/packages/backend/migration/1613155914446-emailNotificationTypes.js index d34ba7e826..cd49924d2d 100644 --- a/packages/backend/migration/1613155914446-emailNotificationTypes.js +++ b/packages/backend/migration/1613155914446-emailNotificationTypes.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class emailNotificationTypes1613155914446 { constructor() { diff --git a/packages/backend/migration/1613181457597-user-lang.js b/packages/backend/migration/1613181457597-user-lang.js index 6ef5245953..d2cd06848e 100644 --- a/packages/backend/migration/1613181457597-user-lang.js +++ b/packages/backend/migration/1613181457597-user-lang.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class userLang1613181457597 { constructor() { diff --git a/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js b/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js index 8529ea3247..f2e2c5d357 100644 --- a/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js +++ b/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class useBigintForDriveUsage1613503367223 { constructor() { diff --git a/packages/backend/migration/1615965918224-chart-v2.js b/packages/backend/migration/1615965918224-chart-v2.js index deecde7227..86fa5b0c00 100644 --- a/packages/backend/migration/1615965918224-chart-v2.js +++ b/packages/backend/migration/1615965918224-chart-v2.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV21615965918224 { constructor() { diff --git a/packages/backend/migration/1615966519402-chart-v2-2.js b/packages/backend/migration/1615966519402-chart-v2-2.js index 7842a27108..c62f1b875c 100644 --- a/packages/backend/migration/1615966519402-chart-v2-2.js +++ b/packages/backend/migration/1615966519402-chart-v2-2.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV221615966519402 { constructor() { diff --git a/packages/backend/migration/1618637372000-user-last-active-date.js b/packages/backend/migration/1618637372000-user-last-active-date.js index 7caf179fa5..6c77ace467 100644 --- a/packages/backend/migration/1618637372000-user-last-active-date.js +++ b/packages/backend/migration/1618637372000-user-last-active-date.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class userLastActiveDate1618637372000 { constructor() { diff --git a/packages/backend/migration/1618639857000-user-hide-online-status.js b/packages/backend/migration/1618639857000-user-hide-online-status.js index 2012962742..e63c8ae11f 100644 --- a/packages/backend/migration/1618639857000-user-hide-online-status.js +++ b/packages/backend/migration/1618639857000-user-hide-online-status.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class userHideOnlineStatus1618639857000 { constructor() { diff --git a/packages/backend/migration/1619942102890-password-reset.js b/packages/backend/migration/1619942102890-password-reset.js index 7784da2bce..922d225dc9 100644 --- a/packages/backend/migration/1619942102890-password-reset.js +++ b/packages/backend/migration/1619942102890-password-reset.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class passwordReset1619942102890 { constructor() { diff --git a/packages/backend/migration/1620019354680-ad.js b/packages/backend/migration/1620019354680-ad.js index 7630ed01a1..c96d2bfb33 100644 --- a/packages/backend/migration/1620019354680-ad.js +++ b/packages/backend/migration/1620019354680-ad.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class ad1620019354680 { constructor() { diff --git a/packages/backend/migration/1620364649428-ad2.js b/packages/backend/migration/1620364649428-ad2.js index 7959185685..db1c3e1de1 100644 --- a/packages/backend/migration/1620364649428-ad2.js +++ b/packages/backend/migration/1620364649428-ad2.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class ad21620364649428 { constructor() { diff --git a/packages/backend/migration/1621479946000-add-note-indexes.js b/packages/backend/migration/1621479946000-add-note-indexes.js index f72bf8211e..dcf97fa4dc 100644 --- a/packages/backend/migration/1621479946000-add-note-indexes.js +++ b/packages/backend/migration/1621479946000-add-note-indexes.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class addNoteIndexes1621479946000 { constructor() { diff --git a/packages/backend/migration/1622679304522-user-profile-description-length.js b/packages/backend/migration/1622679304522-user-profile-description-length.js index 7324175b46..22f6c1c5d9 100644 --- a/packages/backend/migration/1622679304522-user-profile-description-length.js +++ b/packages/backend/migration/1622679304522-user-profile-description-length.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class userProfileDescriptionLength1622679304522 { constructor() { diff --git a/packages/backend/migration/1622681548499-log-message-length.js b/packages/backend/migration/1622681548499-log-message-length.js index b4d8d497e3..ac16c0e1ba 100644 --- a/packages/backend/migration/1622681548499-log-message-length.js +++ b/packages/backend/migration/1622681548499-log-message-length.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class logMessageLength1622681548499 { constructor() { diff --git a/packages/backend/migration/1626509500668-fix-remote-file-proxy.js b/packages/backend/migration/1626509500668-fix-remote-file-proxy.js index 9145247ab1..30c562007b 100644 --- a/packages/backend/migration/1626509500668-fix-remote-file-proxy.js +++ b/packages/backend/migration/1626509500668-fix-remote-file-proxy.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class fixRemoteFileProxy1626509500668 { constructor() { diff --git a/packages/backend/migration/1629004542760-chart-reindex.js b/packages/backend/migration/1629004542760-chart-reindex.js index 072cdec3c1..a7d459276d 100644 --- a/packages/backend/migration/1629004542760-chart-reindex.js +++ b/packages/backend/migration/1629004542760-chart-reindex.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartReindex1629004542760 { constructor() { diff --git a/packages/backend/migration/1629024377804-deepl-integration.js b/packages/backend/migration/1629024377804-deepl-integration.js index 5889196f15..19c49ffcde 100644 --- a/packages/backend/migration/1629024377804-deepl-integration.js +++ b/packages/backend/migration/1629024377804-deepl-integration.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class deeplIntegration1629024377804 { constructor() { diff --git a/packages/backend/migration/1629288472000-fix-channel-userId.js b/packages/backend/migration/1629288472000-fix-channel-userId.js index d7907d05bd..02a1199b09 100644 --- a/packages/backend/migration/1629288472000-fix-channel-userId.js +++ b/packages/backend/migration/1629288472000-fix-channel-userId.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class fixChannelUserId1629288472000 { constructor() { diff --git a/packages/backend/migration/1629512953000-user-is-deleted.js b/packages/backend/migration/1629512953000-user-is-deleted.js index 94165e466b..a7848d5690 100644 --- a/packages/backend/migration/1629512953000-user-is-deleted.js +++ b/packages/backend/migration/1629512953000-user-is-deleted.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class isUserDeleted1629512953000 { constructor() { diff --git a/packages/backend/migration/1629778475000-deepl-integration2.js b/packages/backend/migration/1629778475000-deepl-integration2.js index a54daf8fb3..699f06c768 100644 --- a/packages/backend/migration/1629778475000-deepl-integration2.js +++ b/packages/backend/migration/1629778475000-deepl-integration2.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class deeplIntegration21629778475000 { constructor() { diff --git a/packages/backend/migration/1629833361000-AddShowTLReplies.js b/packages/backend/migration/1629833361000-AddShowTLReplies.js index b80e2ef67f..5d4c938a7b 100644 --- a/packages/backend/migration/1629833361000-AddShowTLReplies.js +++ b/packages/backend/migration/1629833361000-AddShowTLReplies.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class addShowTLReplies1629833361000 { constructor() { diff --git a/packages/backend/migration/1629968054000_userInstanceBlocks.js b/packages/backend/migration/1629968054000_userInstanceBlocks.js index e88fa8aece..1f202d9f66 100644 --- a/packages/backend/migration/1629968054000_userInstanceBlocks.js +++ b/packages/backend/migration/1629968054000_userInstanceBlocks.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class userInstanceBlocks1629968054000 { constructor() { diff --git a/packages/backend/migration/1633068642000-email-required-for-signup.js b/packages/backend/migration/1633068642000-email-required-for-signup.js index d23db2052f..d592f3ca21 100644 --- a/packages/backend/migration/1633068642000-email-required-for-signup.js +++ b/packages/backend/migration/1633068642000-email-required-for-signup.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class emailRequiredForSignup1633068642000 { constructor() { diff --git a/packages/backend/migration/1633071909016-user-pending.js b/packages/backend/migration/1633071909016-user-pending.js index db0f2fde1a..17cf5c11be 100644 --- a/packages/backend/migration/1633071909016-user-pending.js +++ b/packages/backend/migration/1633071909016-user-pending.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class userPending1633071909016 { constructor() { diff --git a/packages/backend/migration/1634486652000-user-public-reactions.js b/packages/backend/migration/1634486652000-user-public-reactions.js index ce1818886a..e741122491 100644 --- a/packages/backend/migration/1634486652000-user-public-reactions.js +++ b/packages/backend/migration/1634486652000-user-public-reactions.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class userPublicReactions1634486652000 { constructor() { diff --git a/packages/backend/migration/1634902659689-delete-log.js b/packages/backend/migration/1634902659689-delete-log.js index 2e2267f9f4..555a0020c3 100644 --- a/packages/backend/migration/1634902659689-delete-log.js +++ b/packages/backend/migration/1634902659689-delete-log.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class deleteLog1634902659689 { constructor() { diff --git a/packages/backend/migration/1635500777168-note-thread-mute.js b/packages/backend/migration/1635500777168-note-thread-mute.js index d5fca59594..a790cace33 100644 --- a/packages/backend/migration/1635500777168-note-thread-mute.js +++ b/packages/backend/migration/1635500777168-note-thread-mute.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class noteThreadMute1635500777168 { constructor() { diff --git a/packages/backend/migration/1636197624383-ff-visibility.js b/packages/backend/migration/1636197624383-ff-visibility.js index 27faae1c92..89028f3c22 100644 --- a/packages/backend/migration/1636197624383-ff-visibility.js +++ b/packages/backend/migration/1636197624383-ff-visibility.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class ffVisibility1636197624383 { constructor() { diff --git a/packages/backend/migration/1636697408073-remove-via-mobile.js b/packages/backend/migration/1636697408073-remove-via-mobile.js index 81f0b63443..36e96fd21e 100644 --- a/packages/backend/migration/1636697408073-remove-via-mobile.js +++ b/packages/backend/migration/1636697408073-remove-via-mobile.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class removeViaMobile1636697408073 { name = 'removeViaMobile1636697408073' diff --git a/packages/backend/migration/1637320813000-forwarded-report.js b/packages/backend/migration/1637320813000-forwarded-report.js index 8125468aae..1e39bd5c3f 100644 --- a/packages/backend/migration/1637320813000-forwarded-report.js +++ b/packages/backend/migration/1637320813000-forwarded-report.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class forwardedReport1637320813000 { name = 'forwardedReport1637320813000'; diff --git a/packages/backend/migration/1639325650583-chart-v3.js b/packages/backend/migration/1639325650583-chart-v3.js index 2255476394..e2a4e920c9 100644 --- a/packages/backend/migration/1639325650583-chart-v3.js +++ b/packages/backend/migration/1639325650583-chart-v3.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV31639325650583 { name = 'chartV31639325650583' diff --git a/packages/backend/migration/1642611822809-emoji-url.js b/packages/backend/migration/1642611822809-emoji-url.js index 421614b408..d38f8cc08c 100644 --- a/packages/backend/migration/1642611822809-emoji-url.js +++ b/packages/backend/migration/1642611822809-emoji-url.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class emojiUrl1642611822809 { name = 'emojiUrl1642611822809' diff --git a/packages/backend/migration/1642613870898-drive-file-webpublic-type.js b/packages/backend/migration/1642613870898-drive-file-webpublic-type.js index e61a3fc49e..15434f7d0c 100644 --- a/packages/backend/migration/1642613870898-drive-file-webpublic-type.js +++ b/packages/backend/migration/1642613870898-drive-file-webpublic-type.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class driveFileWebpublicType1642613870898 { name = 'driveFileWebpublicType1642613870898' diff --git a/packages/backend/migration/1643963705770-chart-v4.js b/packages/backend/migration/1643963705770-chart-v4.js index 77355cd7f3..8b320c2b41 100644 --- a/packages/backend/migration/1643963705770-chart-v4.js +++ b/packages/backend/migration/1643963705770-chart-v4.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV41643963705770 { name = 'chartV41643963705770' diff --git a/packages/backend/migration/1643966656277-chart-v5.js b/packages/backend/migration/1643966656277-chart-v5.js index 54e4705e56..df84002f78 100644 --- a/packages/backend/migration/1643966656277-chart-v5.js +++ b/packages/backend/migration/1643966656277-chart-v5.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV51643966656277 { name = 'chartV51643966656277' diff --git a/packages/backend/migration/1643967331284-chart-v6.js b/packages/backend/migration/1643967331284-chart-v6.js index aa64bc9faa..119198f4a5 100644 --- a/packages/backend/migration/1643967331284-chart-v6.js +++ b/packages/backend/migration/1643967331284-chart-v6.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV61643967331284 { name = 'chartV61643967331284' diff --git a/packages/backend/migration/1644010796173-convert-hard-mutes.js b/packages/backend/migration/1644010796173-convert-hard-mutes.js index 9aec21b5ff..207a759b8e 100644 --- a/packages/backend/migration/1644010796173-convert-hard-mutes.js +++ b/packages/backend/migration/1644010796173-convert-hard-mutes.js @@ -1,10 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import RE2 from 're2'; + export class convertHardMutes1644010796173 { name = 'convertHardMutes1644010796173' diff --git a/packages/backend/migration/1644058404077-chart-v7.js b/packages/backend/migration/1644058404077-chart-v7.js index a09fff1bc7..f05ad003db 100644 --- a/packages/backend/migration/1644058404077-chart-v7.js +++ b/packages/backend/migration/1644058404077-chart-v7.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV71644058404077 { name = 'chartV71644058404077' diff --git a/packages/backend/migration/1644059847460-chart-v8.js b/packages/backend/migration/1644059847460-chart-v8.js index 43b95926b6..a5339c0ebd 100644 --- a/packages/backend/migration/1644059847460-chart-v8.js +++ b/packages/backend/migration/1644059847460-chart-v8.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV81644059847460 { name = 'chartV81644059847460' @@ -17,9 +14,9 @@ export class chartV81644059847460 { await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___local_users" TYPE integer USING "___local_users"::integer`); await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___remote_users" TYPE integer USING "___remote_users"::integer`); } - + async down(queryRunner) { - + await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___local_users" TYPE bigint USING "___local_users"::bigint`); await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___remote_users" TYPE bigint USING "___remote_users"::bigint`); await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___local_users" TYPE bigint USING "___local_users"::bigint`); diff --git a/packages/backend/migration/1644060125705-chart-v9.js b/packages/backend/migration/1644060125705-chart-v9.js index dc99f3c8f8..da35d42315 100644 --- a/packages/backend/migration/1644060125705-chart-v9.js +++ b/packages/backend/migration/1644060125705-chart-v9.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV91644060125705 { name = 'chartV91644060125705' @@ -17,9 +14,9 @@ export class chartV91644060125705 { await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ALTER COLUMN "___local_users" TYPE integer USING "___local_users"::integer`); await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ALTER COLUMN "___remote_users" TYPE integer USING "___remote_users"::integer`); } - + async down(queryRunner) { - + await queryRunner.query(`ALTER TABLE "__chart__hashtag" ALTER COLUMN "___local_users" TYPE bigint USING "___local_users"::bigint`); await queryRunner.query(`ALTER TABLE "__chart__hashtag" ALTER COLUMN "___remote_users" TYPE bigint USING "___remote_users"::bigint`); await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ALTER COLUMN "___local_users" TYPE bigint USING "___local_users"::bigint`); diff --git a/packages/backend/migration/1644073149413-chart-v10.js b/packages/backend/migration/1644073149413-chart-v10.js index 4d36235729..7260bbeca4 100644 --- a/packages/backend/migration/1644073149413-chart-v10.js +++ b/packages/backend/migration/1644073149413-chart-v10.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV101644073149413 { name = 'chartV101644073149413' diff --git a/packages/backend/migration/1644095659741-chart-v11.js b/packages/backend/migration/1644095659741-chart-v11.js index 80bacbf710..309fff1d9a 100644 --- a/packages/backend/migration/1644095659741-chart-v11.js +++ b/packages/backend/migration/1644095659741-chart-v11.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV111644095659741 { name = 'chartV111644095659741' diff --git a/packages/backend/migration/1644328606241-chart-v12.js b/packages/backend/migration/1644328606241-chart-v12.js index 15c0dd9040..c3c7e44f95 100644 --- a/packages/backend/migration/1644328606241-chart-v12.js +++ b/packages/backend/migration/1644328606241-chart-v12.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV121644328606241 { name = 'chartV121644328606241' diff --git a/packages/backend/migration/1644331238153-chart-v13.js b/packages/backend/migration/1644331238153-chart-v13.js index 0c2db66f27..639f7b4e20 100644 --- a/packages/backend/migration/1644331238153-chart-v13.js +++ b/packages/backend/migration/1644331238153-chart-v13.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV131644331238153 { name = 'chartV131644331238153' diff --git a/packages/backend/migration/1644344266289-chart-v14.js b/packages/backend/migration/1644344266289-chart-v14.js index 0f4688ab77..a0d9cfc38c 100644 --- a/packages/backend/migration/1644344266289-chart-v14.js +++ b/packages/backend/migration/1644344266289-chart-v14.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV141644344266289 { name = 'chartV141644344266289' diff --git a/packages/backend/migration/1644395759931-instance-theme-color.js b/packages/backend/migration/1644395759931-instance-theme-color.js index fd7356e68a..8f335ad210 100644 --- a/packages/backend/migration/1644395759931-instance-theme-color.js +++ b/packages/backend/migration/1644395759931-instance-theme-color.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class instanceThemeColor1644395759931 { name = 'instanceThemeColor1644395759931' diff --git a/packages/backend/migration/1644481657998-chart-v15.js b/packages/backend/migration/1644481657998-chart-v15.js index 964bea3d07..b50ca87c40 100644 --- a/packages/backend/migration/1644481657998-chart-v15.js +++ b/packages/backend/migration/1644481657998-chart-v15.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class chartV151644481657998 { name = 'chartV151644481657998' diff --git a/packages/backend/migration/1644551208096-following-indexes.js b/packages/backend/migration/1644551208096-following-indexes.js index 8d1d4890dc..276473ff6c 100644 --- a/packages/backend/migration/1644551208096-following-indexes.js +++ b/packages/backend/migration/1644551208096-following-indexes.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class followingIndexes1644551208096 { name = 'followingIndexes1644551208096' diff --git a/packages/backend/migration/1645340161439-remove-max-note-text-length.js b/packages/backend/migration/1645340161439-remove-max-note-text-length.js index 1cf6b0801b..c88cb70bfb 100644 --- a/packages/backend/migration/1645340161439-remove-max-note-text-length.js +++ b/packages/backend/migration/1645340161439-remove-max-note-text-length.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class removeMaxNoteTextLength1645340161439 { name = 'removeMaxNoteTextLength1645340161439' diff --git a/packages/backend/migration/1645599900873-federation-chart-pubsub.js b/packages/backend/migration/1645599900873-federation-chart-pubsub.js index 3042c8ecd9..fd7cb6d5a1 100644 --- a/packages/backend/migration/1645599900873-federation-chart-pubsub.js +++ b/packages/backend/migration/1645599900873-federation-chart-pubsub.js @@ -1,7 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ + export class federationChartPubsub1645599900873 { name = 'federationChartPubsub1645599900873' diff --git a/packages/backend/migration/1646143552768-instance-default-theme.js b/packages/backend/migration/1646143552768-instance-default-theme.js index 8f0755e3a2..029354fd92 100644 --- a/packages/backend/migration/1646143552768-instance-default-theme.js +++ b/packages/backend/migration/1646143552768-instance-default-theme.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class instanceDefaultTheme1646143552768 { name = 'instanceDefaultTheme1646143552768' diff --git a/packages/backend/migration/1646387162108-mute-expires-at.js b/packages/backend/migration/1646387162108-mute-expires-at.js index 412db14881..c8be8f3c54 100644 --- a/packages/backend/migration/1646387162108-mute-expires-at.js +++ b/packages/backend/migration/1646387162108-mute-expires-at.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class muteExpiresAt1646387162108 { name = 'muteExpiresAt1646387162108' diff --git a/packages/backend/migration/1646549089451-poll-ended-notification.js b/packages/backend/migration/1646549089451-poll-ended-notification.js index 6c481c6ac6..38a38ce64d 100644 --- a/packages/backend/migration/1646549089451-poll-ended-notification.js +++ b/packages/backend/migration/1646549089451-poll-ended-notification.js @@ -1,7 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ export class pollEndedNotification1646549089451 { name = 'pollEndedNotification1646549089451' diff --git a/packages/backend/migration/1646633030285-chart-federation-active.js b/packages/backend/migration/1646633030285-chart-federation-active.js index 13d54c3180..952289c8f8 100644 --- a/packages/backend/migration/1646633030285-chart-federation-active.js +++ b/packages/backend/migration/1646633030285-chart-federation-active.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class chartFederationActive1646633030285 { name = 'chartFederationActive1646633030285' diff --git a/packages/backend/migration/1646655454495-remove-instance-drive-columns.js b/packages/backend/migration/1646655454495-remove-instance-drive-columns.js index 04d6fce887..a0ee1b2c43 100644 --- a/packages/backend/migration/1646655454495-remove-instance-drive-columns.js +++ b/packages/backend/migration/1646655454495-remove-instance-drive-columns.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class removeInstanceDriveColumns1646655454495 { name = 'removeInstanceDriveColumns1646655454495' diff --git a/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js b/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js index 289b929ad9..c9a847cbcf 100644 --- a/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js +++ b/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class chartFederationActiveSubPub1646732390560 { name = 'chartFederationActiveSubPub1646732390560' diff --git a/packages/backend/migration/1648548247382-webhook.js b/packages/backend/migration/1648548247382-webhook.js index f31d3c5bb5..aea369a5cc 100644 --- a/packages/backend/migration/1648548247382-webhook.js +++ b/packages/backend/migration/1648548247382-webhook.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class webhook1648548247382 { name = 'webhook1648548247382' diff --git a/packages/backend/migration/1648816172177-webhook-2.js b/packages/backend/migration/1648816172177-webhook-2.js index 4d1b293b2c..2feb68d611 100644 --- a/packages/backend/migration/1648816172177-webhook-2.js +++ b/packages/backend/migration/1648816172177-webhook-2.js @@ -1,7 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ export class webhook21648816172177 { name = 'webhook21648816172177' diff --git a/packages/backend/migration/1651224615271-foreign-key.js b/packages/backend/migration/1651224615271-foreign-key.js index fa51bb5e31..535d21731a 100644 --- a/packages/backend/migration/1651224615271-foreign-key.js +++ b/packages/backend/migration/1651224615271-foreign-key.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class foreignKeyReports1651224615271 { name = 'foreignKeyReports1651224615271' diff --git a/packages/backend/migration/1652859567549-uniform-themecolor.js b/packages/backend/migration/1652859567549-uniform-themecolor.js index 754e089824..8da1fd7fbb 100644 --- a/packages/backend/migration/1652859567549-uniform-themecolor.js +++ b/packages/backend/migration/1652859567549-uniform-themecolor.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import tinycolor from 'tinycolor2'; export class uniformThemecolor1652859567549 { diff --git a/packages/backend/migration/1655368940105-nsfw-detection.js b/packages/backend/migration/1655368940105-nsfw-detection.js index d2d0d00117..9268f43407 100644 --- a/packages/backend/migration/1655368940105-nsfw-detection.js +++ b/packages/backend/migration/1655368940105-nsfw-detection.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class nsfwDetection1655368940105 { name = 'nsfwDetection1655368940105' diff --git a/packages/backend/migration/1655371960534-nsfw-detection-2.js b/packages/backend/migration/1655371960534-nsfw-detection-2.js index e5adbddca4..aac6f37dad 100644 --- a/packages/backend/migration/1655371960534-nsfw-detection-2.js +++ b/packages/backend/migration/1655371960534-nsfw-detection-2.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class nsfwDetection21655371960534 { name = 'nsfwDetection21655371960534' diff --git a/packages/backend/migration/1655388169582-nsfw-detection-3.js b/packages/backend/migration/1655388169582-nsfw-detection-3.js index 12fc281327..a5c80cf968 100644 --- a/packages/backend/migration/1655388169582-nsfw-detection-3.js +++ b/packages/backend/migration/1655388169582-nsfw-detection-3.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class nsfwDetection31655388169582 { name = 'nsfwDetection31655388169582' diff --git a/packages/backend/migration/1655393015659-nsfw-detection-4.js b/packages/backend/migration/1655393015659-nsfw-detection-4.js index 39fb175679..e780732623 100644 --- a/packages/backend/migration/1655393015659-nsfw-detection-4.js +++ b/packages/backend/migration/1655393015659-nsfw-detection-4.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class nsfwDetection41655393015659 { name = 'nsfwDetection41655393015659' diff --git a/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js b/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js index e64c8c1b82..f257cd112f 100644 --- a/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js +++ b/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class driveCapacityOverrideMb1655813815729 { name = 'driveCapacityOverrideMb1655813815729' diff --git a/packages/backend/migration/1655918165614-user-ip.js b/packages/backend/migration/1655918165614-user-ip.js index 668c6d909b..2294fbaf19 100644 --- a/packages/backend/migration/1655918165614-user-ip.js +++ b/packages/backend/migration/1655918165614-user-ip.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class userIp1655918165614 { name = 'userIp1655918165614' diff --git a/packages/backend/migration/1656122560740-file-ip.js b/packages/backend/migration/1656122560740-file-ip.js index e5efaf3d9f..b59e7a911f 100644 --- a/packages/backend/migration/1656122560740-file-ip.js +++ b/packages/backend/migration/1656122560740-file-ip.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class fileIp1656122560740 { name = 'fileIp1656122560740' diff --git a/packages/backend/migration/1656251734807-nsfw-detection-5.js b/packages/backend/migration/1656251734807-nsfw-detection-5.js index 9b36bd76eb..6f0c536907 100644 --- a/packages/backend/migration/1656251734807-nsfw-detection-5.js +++ b/packages/backend/migration/1656251734807-nsfw-detection-5.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class nsfwDetection51656251734807 { name = 'nsfwDetection51656251734807' diff --git a/packages/backend/migration/1656328812281-ip-2.js b/packages/backend/migration/1656328812281-ip-2.js index 39fcd1d83d..b0ee1ebfc7 100644 --- a/packages/backend/migration/1656328812281-ip-2.js +++ b/packages/backend/migration/1656328812281-ip-2.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class ip21656328812281 { name = 'ip21656328812281' diff --git a/packages/backend/migration/1656408772602-nsfw-detection-6.js b/packages/backend/migration/1656408772602-nsfw-detection-6.js index efadd22e5d..7ef223a4c6 100644 --- a/packages/backend/migration/1656408772602-nsfw-detection-6.js +++ b/packages/backend/migration/1656408772602-nsfw-detection-6.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class nsfwDetection61656408772602 { name = 'nsfwDetection61656408772602' diff --git a/packages/backend/migration/1656772790599-user-moderation-note.js b/packages/backend/migration/1656772790599-user-moderation-note.js index ef2f0f6522..133bcffe1a 100644 --- a/packages/backend/migration/1656772790599-user-moderation-note.js +++ b/packages/backend/migration/1656772790599-user-moderation-note.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class userModerationNote1656772790599 { name = 'userModerationNote1656772790599' diff --git a/packages/backend/migration/1657346559800-active-email-validation.js b/packages/backend/migration/1657346559800-active-email-validation.js index e8d5b29cdf..f8e03eeb07 100644 --- a/packages/backend/migration/1657346559800-active-email-validation.js +++ b/packages/backend/migration/1657346559800-active-email-validation.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class activeEmailValidation1657346559800 { name = 'activeEmailValidation1657346559800' diff --git a/packages/backend/migration/1664694635394-turnstile.js b/packages/backend/migration/1664694635394-turnstile.js index a9baf4c657..4a33443950 100644 --- a/packages/backend/migration/1664694635394-turnstile.js +++ b/packages/backend/migration/1664694635394-turnstile.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class turnstile1664694635394 { name = 'turnstile1664694635394' diff --git a/packages/backend/migration/1665091090561-add-renote-muting.js b/packages/backend/migration/1665091090561-add-renote-muting.js index 5748572517..d2ed2bd2e9 100644 --- a/packages/backend/migration/1665091090561-add-renote-muting.js +++ b/packages/backend/migration/1665091090561-add-renote-muting.js @@ -1,7 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ export class addRenoteMuting1665091090561 { constructor() { @@ -16,9 +12,5 @@ export class addRenoteMuting1665091090561 { } async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_renote_muting_muterId"`); - await queryRunner.query(`DROP INDEX "IDX_renote_muting_muteeId"`); - await queryRunner.query(`DROP INDEX "IDX_renote_muting_createdAt"`); - await queryRunner.query(`DROP TABLE "renote_muting"`); } } diff --git a/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js b/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js index 431241897d..2265b00617 100644 --- a/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js +++ b/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class whetherPushNotifyToSendReadMessage1669138716634 { name = 'whetherPushNotifyToSendReadMessage1669138716634' diff --git a/packages/backend/migration/1671924750884-RetentionAggregation.js b/packages/backend/migration/1671924750884-RetentionAggregation.js index 67079bb7a1..ed81a4b5e9 100644 --- a/packages/backend/migration/1671924750884-RetentionAggregation.js +++ b/packages/backend/migration/1671924750884-RetentionAggregation.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class RetentionAggregation1671924750884 { name = 'RetentionAggregation1671924750884' diff --git a/packages/backend/migration/1671926422832-RetentionAggregation2.js b/packages/backend/migration/1671926422832-RetentionAggregation2.js index f26e0f7d2e..725429e6ef 100644 --- a/packages/backend/migration/1671926422832-RetentionAggregation2.js +++ b/packages/backend/migration/1671926422832-RetentionAggregation2.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class RetentionAggregation21671926422832 { name = 'RetentionAggregation21671926422832' diff --git a/packages/backend/migration/1672562400597-PerUserPvChart.js b/packages/backend/migration/1672562400597-PerUserPvChart.js index 844f665a8b..4da6b9a8b3 100644 --- a/packages/backend/migration/1672562400597-PerUserPvChart.js +++ b/packages/backend/migration/1672562400597-PerUserPvChart.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class PerUserPvChart1672562400597 { name = 'PerUserPvChart1672562400597' diff --git a/packages/backend/migration/1672703171386-remove-latestRequestSentAt.js b/packages/backend/migration/1672703171386-remove-latestRequestSentAt.js index fa73fc8977..c9b28dd7e1 100644 --- a/packages/backend/migration/1672703171386-remove-latestRequestSentAt.js +++ b/packages/backend/migration/1672703171386-remove-latestRequestSentAt.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class removeLatestRequestSentAt1672703171386 { name = 'removeLatestRequestSentAt1672703171386' diff --git a/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js b/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js index abf209162b..38a6769851 100644 --- a/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js +++ b/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class removeLastCommunicatedAt1672704017999 { name = 'removeLastCommunicatedAt1672704017999' diff --git a/packages/backend/migration/1672704136584-remove-latestStatus.js b/packages/backend/migration/1672704136584-remove-latestStatus.js index d75344c053..937c2fe8fd 100644 --- a/packages/backend/migration/1672704136584-remove-latestStatus.js +++ b/packages/backend/migration/1672704136584-remove-latestStatus.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class removeLatestStatus1672704136584 { name = 'removeLatestStatus1672704136584' diff --git a/packages/backend/migration/1672822262496-Flash.js b/packages/backend/migration/1672822262496-Flash.js index fd3f77d893..6c2338fab2 100644 --- a/packages/backend/migration/1672822262496-Flash.js +++ b/packages/backend/migration/1672822262496-Flash.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class Flash1672822262496 { name = 'Flash1672822262496' diff --git a/packages/backend/migration/1673336077243-PollChoiceLength.js b/packages/backend/migration/1673336077243-PollChoiceLength.js index 7bd65149d6..810c626e04 100644 --- a/packages/backend/migration/1673336077243-PollChoiceLength.js +++ b/packages/backend/migration/1673336077243-PollChoiceLength.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class PollChoiceLength1673336077243 { name = 'PollChoiceLength1673336077243' diff --git a/packages/backend/migration/1673500412259-Role.js b/packages/backend/migration/1673500412259-Role.js index 6bfb31e08e..a8acedf5b7 100644 --- a/packages/backend/migration/1673500412259-Role.js +++ b/packages/backend/migration/1673500412259-Role.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class Role1673500412259 { name = 'Role1673500412259' diff --git a/packages/backend/migration/1673515526953-RoleColor.js b/packages/backend/migration/1673515526953-RoleColor.js index b856e4183b..343eedf346 100644 --- a/packages/backend/migration/1673515526953-RoleColor.js +++ b/packages/backend/migration/1673515526953-RoleColor.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class RoleColor1673515526953 { name = 'RoleColor1673515526953' diff --git a/packages/backend/migration/1673522856499-RoleIroiro.js b/packages/backend/migration/1673522856499-RoleIroiro.js index 40635e50d8..a1e64d49fe 100644 --- a/packages/backend/migration/1673522856499-RoleIroiro.js +++ b/packages/backend/migration/1673522856499-RoleIroiro.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class RoleIroiro1673522856499 { name = 'RoleIroiro1673522856499' diff --git a/packages/backend/migration/1673524604156-RoleLastUsedAt.js b/packages/backend/migration/1673524604156-RoleLastUsedAt.js index 3bbb8000d8..786ef07f5e 100644 --- a/packages/backend/migration/1673524604156-RoleLastUsedAt.js +++ b/packages/backend/migration/1673524604156-RoleLastUsedAt.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class RoleLastUsedAt1673524604156 { name = 'RoleLastUsedAt1673524604156' diff --git a/packages/backend/migration/1673570377815-RoleConditional.js b/packages/backend/migration/1673570377815-RoleConditional.js index 354fd6c66a..11ae4f00c6 100644 --- a/packages/backend/migration/1673570377815-RoleConditional.js +++ b/packages/backend/migration/1673570377815-RoleConditional.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class RoleConditional1673570377815 { name = 'RoleConditional1673570377815' diff --git a/packages/backend/migration/1673575973645-MetaClean.js b/packages/backend/migration/1673575973645-MetaClean.js index 684d62e8e9..11be4c1cdd 100644 --- a/packages/backend/migration/1673575973645-MetaClean.js +++ b/packages/backend/migration/1673575973645-MetaClean.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class MetaClean1673575973645 { name = 'MetaClean1673575973645' diff --git a/packages/backend/migration/1673783015567-Policies.js b/packages/backend/migration/1673783015567-Policies.js index 8674306620..8b36921d41 100644 --- a/packages/backend/migration/1673783015567-Policies.js +++ b/packages/backend/migration/1673783015567-Policies.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class Policies1673783015567 { name = 'Policies1673783015567' diff --git a/packages/backend/migration/1673812883772-firstRetrievedAt.js b/packages/backend/migration/1673812883772-firstRetrievedAt.js index 4111cc4ad0..5603bbc7c4 100644 --- a/packages/backend/migration/1673812883772-firstRetrievedAt.js +++ b/packages/backend/migration/1673812883772-firstRetrievedAt.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class firstRetrievedAt1673812883772 { name = 'firstRetrievedAt1673812883772' diff --git a/packages/backend/migration/1674086433654-flashScriptLength.js b/packages/backend/migration/1674086433654-flashScriptLength.js index cdfb812ba0..a4d149fe15 100644 --- a/packages/backend/migration/1674086433654-flashScriptLength.js +++ b/packages/backend/migration/1674086433654-flashScriptLength.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class flashScriptLength1674086433654 { name = 'flashScriptLength1674086433654' diff --git a/packages/backend/migration/1674118260469-achievement.js b/packages/backend/migration/1674118260469-achievement.js index 072cf81ec3..131ab96f80 100644 --- a/packages/backend/migration/1674118260469-achievement.js +++ b/packages/backend/migration/1674118260469-achievement.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class achievement1674118260469 { name = 'achievement1674118260469' diff --git a/packages/backend/migration/1674255666603-loggedInDates.js b/packages/backend/migration/1674255666603-loggedInDates.js index a2a217da95..6d75ab6436 100644 --- a/packages/backend/migration/1674255666603-loggedInDates.js +++ b/packages/backend/migration/1674255666603-loggedInDates.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class loggedInDates1674255666603 { name = 'loggedInDates1674255666603' diff --git a/packages/backend/migration/1675053125067-fixforeignkeyreports.js b/packages/backend/migration/1675053125067-fixforeignkeyreports.js index 2ca383f563..ca5c10b11f 100644 --- a/packages/backend/migration/1675053125067-fixforeignkeyreports.js +++ b/packages/backend/migration/1675053125067-fixforeignkeyreports.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class fixforeignkeyreports1675053125067 { name = 'fixforeignkeyreports1675053125067' diff --git a/packages/backend/migration/1675404035646-cleanup.js b/packages/backend/migration/1675404035646-cleanup.js index 5cd5f5534a..09b22ee393 100644 --- a/packages/backend/migration/1675404035646-cleanup.js +++ b/packages/backend/migration/1675404035646-cleanup.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class cleanup1675404035646 { name = 'cleanup1675404035646' diff --git a/packages/backend/migration/1675557528704-role-icon-badge.js b/packages/backend/migration/1675557528704-role-icon-badge.js index 48684075d1..0ebca088e3 100644 --- a/packages/backend/migration/1675557528704-role-icon-badge.js +++ b/packages/backend/migration/1675557528704-role-icon-badge.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class roleIconBadge1675557528704 { name = 'roleIconBadge1675557528704' diff --git a/packages/backend/migration/1676434944993-drop-group.js b/packages/backend/migration/1676434944993-drop-group.js index 2df8a2d789..c856046eb9 100644 --- a/packages/backend/migration/1676434944993-drop-group.js +++ b/packages/backend/migration/1676434944993-drop-group.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class dropGroup1676434944993 { name = 'dropGroup1676434944993' diff --git a/packages/backend/migration/1676438468213-ad3.js b/packages/backend/migration/1676438468213-ad3.js index 83ca5828e3..18f56e8d36 100644 --- a/packages/backend/migration/1676438468213-ad3.js +++ b/packages/backend/migration/1676438468213-ad3.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class ad1676438468213 { name = 'ad1676438468213'; async up(queryRunner) { diff --git a/packages/backend/migration/1677054292210-ad4.js b/packages/backend/migration/1677054292210-ad4.js deleted file mode 100644 index 11c42dd354..0000000000 --- a/packages/backend/migration/1677054292210-ad4.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class ad1677054292210 { - name = 'ad1677054292210'; - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "ad" ADD "dayOfWeek" integer NOT NULL Default 0`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "dayOfWeek"`); - } -} diff --git a/packages/backend/migration/1677570181236-role-assignment-expires-at.js b/packages/backend/migration/1677570181236-role-assignment-expires-at.js index 6fe32ffeb0..3ac2edab0a 100644 --- a/packages/backend/migration/1677570181236-role-assignment-expires-at.js +++ b/packages/backend/migration/1677570181236-role-assignment-expires-at.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class roleAssignmentExpiresAt1677570181236 { name = 'roleAssignmentExpiresAt1677570181236' diff --git a/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js b/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js index 44c807499c..f1765dd146 100644 --- a/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js +++ b/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class perNoteReactionAcceptance1678164627293 { name = 'perNoteReactionAcceptance1678164627293' diff --git a/packages/backend/migration/1678426061773-tweak-varchar-length.js b/packages/backend/migration/1678426061773-tweak-varchar-length.js index 74c4fd6715..984c41dba6 100644 --- a/packages/backend/migration/1678426061773-tweak-varchar-length.js +++ b/packages/backend/migration/1678426061773-tweak-varchar-length.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class tweakVarcharLength1678426061773 { name = 'tweakVarcharLength1678426061773' diff --git a/packages/backend/migration/1678427401214-remove-unused.js b/packages/backend/migration/1678427401214-remove-unused.js index e398b3700c..ee643e7776 100644 --- a/packages/backend/migration/1678427401214-remove-unused.js +++ b/packages/backend/migration/1678427401214-remove-unused.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class removeUnused1678427401214 { name = 'removeUnused1678427401214' diff --git a/packages/backend/migration/1678602320354-role-display-order.js b/packages/backend/migration/1678602320354-role-display-order.js index d3cc9792ca..de8f6f1033 100644 --- a/packages/backend/migration/1678602320354-role-display-order.js +++ b/packages/backend/migration/1678602320354-role-display-order.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class roleDisplayOrder1678602320354 { name = 'roleDisplayOrder1678602320354' diff --git a/packages/backend/migration/1678694614599-sensitive-words.js b/packages/backend/migration/1678694614599-sensitive-words.js index 13361f597e..6d4c5730c7 100644 --- a/packages/backend/migration/1678694614599-sensitive-words.js +++ b/packages/backend/migration/1678694614599-sensitive-words.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class sensitiveWords1678694614599 { name = 'sensitiveWords1678694614599' diff --git a/packages/backend/migration/1678869617549-retention-date-key.js b/packages/backend/migration/1678869617549-retention-date-key.js index 1b995385b0..1a31b9a750 100644 --- a/packages/backend/migration/1678869617549-retention-date-key.js +++ b/packages/backend/migration/1678869617549-retention-date-key.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class retentionDateKey1678869617549 { name = 'retentionDateKey1678869617549' diff --git a/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js b/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js index 5d1218be12..656a921770 100644 --- a/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js +++ b/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class addPropsForCustomEmoji1678945242650 { name = 'addPropsForCustomEmoji1678945242650' diff --git a/packages/backend/migration/1678953978856-clip-favorite.js b/packages/backend/migration/1678953978856-clip-favorite.js index 9d706c4dae..aa5dc93a6e 100644 --- a/packages/backend/migration/1678953978856-clip-favorite.js +++ b/packages/backend/migration/1678953978856-clip-favorite.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class clipFavorite1678953978856 { name = 'clipFavorite1678953978856' diff --git a/packages/backend/migration/1679309757174-antenna-active.js b/packages/backend/migration/1679309757174-antenna-active.js index dadea25a7c..69e845c142 100644 --- a/packages/backend/migration/1679309757174-antenna-active.js +++ b/packages/backend/migration/1679309757174-antenna-active.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class antennaActive1679309757174 { name = 'antennaActive1679309757174' diff --git a/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js b/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js index f2a13100e2..42faab7466 100644 --- a/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js +++ b/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class enableChartsForRemoteUser1679639483253 { name = 'enableChartsForRemoteUser1679639483253' diff --git a/packages/backend/migration/1679651580149-cleanup.js b/packages/backend/migration/1679651580149-cleanup.js index efee339c46..1f00f3cc1f 100644 --- a/packages/backend/migration/1679651580149-cleanup.js +++ b/packages/backend/migration/1679651580149-cleanup.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class cleanup1679651580149 { name = 'cleanup1679651580149' diff --git a/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js b/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js index 67be10e6fd..0733339841 100644 --- a/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js +++ b/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class enableChartsForFederatedInstances1679652081809 { name = 'enableChartsForFederatedInstances1679652081809' diff --git a/packages/backend/migration/1680228513388-channelFavorite.js b/packages/backend/migration/1680228513388-channelFavorite.js index 866173305e..afc676959a 100644 --- a/packages/backend/migration/1680228513388-channelFavorite.js +++ b/packages/backend/migration/1680228513388-channelFavorite.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class channelFavorite1680228513388 { name = 'channelFavorite1680228513388' diff --git a/packages/backend/migration/1680238118084-channelNotePining.js b/packages/backend/migration/1680238118084-channelNotePining.js index 78bafc0237..126eae87ea 100644 --- a/packages/backend/migration/1680238118084-channelNotePining.js +++ b/packages/backend/migration/1680238118084-channelNotePining.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class channelNotePining1680238118084 { name = 'channelNotePining1680238118084' diff --git a/packages/backend/migration/1680491187535-cleanup.js b/packages/backend/migration/1680491187535-cleanup.js index f0b1bccdab..1e609ca060 100644 --- a/packages/backend/migration/1680491187535-cleanup.js +++ b/packages/backend/migration/1680491187535-cleanup.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class cleanup1680491187535 { name = 'cleanup1680491187535' diff --git a/packages/backend/migration/1680582195041-cleanup.js b/packages/backend/migration/1680582195041-cleanup.js index 83d04b6186..c587e456a5 100644 --- a/packages/backend/migration/1680582195041-cleanup.js +++ b/packages/backend/migration/1680582195041-cleanup.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class cleanup1680582195041 { name = 'cleanup1680582195041' @@ -11,6 +6,6 @@ export class cleanup1680582195041 { } async down(queryRunner) { - + } } diff --git a/packages/backend/migration/1680702787050-UserMemo.js b/packages/backend/migration/1680702787050-UserMemo.js index 3f7afe8657..7446bf8da5 100644 --- a/packages/backend/migration/1680702787050-UserMemo.js +++ b/packages/backend/migration/1680702787050-UserMemo.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class UserMemo1680702787050 { name = 'UserMemo1680702787050' diff --git a/packages/backend/migration/1680775031481-avatar-url-and-banner-url.js b/packages/backend/migration/1680775031481-avatar-url-and-banner-url.js index 49295e70eb..7c5fe7ac5e 100644 --- a/packages/backend/migration/1680775031481-avatar-url-and-banner-url.js +++ b/packages/backend/migration/1680775031481-avatar-url-and-banner-url.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class AvatarUrlAndBannerUrl1680775031481 { name = 'AvatarUrlAndBannerUrl1680775031481' diff --git a/packages/backend/migration/1680931179228-account-move.js b/packages/backend/migration/1680931179228-account-move.js index a8b5e4df68..821318d1bc 100644 --- a/packages/backend/migration/1680931179228-account-move.js +++ b/packages/backend/migration/1680931179228-account-move.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class AccountMove1680931179228 { name = 'AccountMove1680931179228' diff --git a/packages/backend/migration/1681400427971-serverRules.js b/packages/backend/migration/1681400427971-serverRules.js index 176783b50a..2364e8e1d2 100644 --- a/packages/backend/migration/1681400427971-serverRules.js +++ b/packages/backend/migration/1681400427971-serverRules.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class ServerRules1681400427971 { name = 'ServerRules1681400427971' diff --git a/packages/backend/migration/1681870960239-RoleTLSetting.js b/packages/backend/migration/1681870960239-RoleTLSetting.js index 2999051a3b..2280f44eaa 100644 --- a/packages/backend/migration/1681870960239-RoleTLSetting.js +++ b/packages/backend/migration/1681870960239-RoleTLSetting.js @@ -1,15 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class RoleTLSetting1681870960239 { name = 'RoleTLSetting1681870960239' async up(queryRunner) { await queryRunner.query(`ALTER TABLE "role" ADD "isExplorable" boolean NOT NULL DEFAULT false`); } - + async down(queryRunner) { await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "isExplorable"`); } diff --git a/packages/backend/migration/1682190963894-movedAt.js b/packages/backend/migration/1682190963894-movedAt.js index 852cf58969..1f8f030a5c 100644 --- a/packages/backend/migration/1682190963894-movedAt.js +++ b/packages/backend/migration/1682190963894-movedAt.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class MovedAt1682190963894 { name = 'MovedAt1682190963894' diff --git a/packages/backend/migration/1682754135458-preservedUsernames.js b/packages/backend/migration/1682754135458-preservedUsernames.js index 8aae3c2054..46a0826f43 100644 --- a/packages/backend/migration/1682754135458-preservedUsernames.js +++ b/packages/backend/migration/1682754135458-preservedUsernames.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class PreservedUsernames1682754135458 { name = 'PreservedUsernames1682754135458' diff --git a/packages/backend/migration/1682985520254-channelColor.js b/packages/backend/migration/1682985520254-channelColor.js index 3c7f3101a5..294b7372b2 100644 --- a/packages/backend/migration/1682985520254-channelColor.js +++ b/packages/backend/migration/1682985520254-channelColor.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class ChannelColor1682985520254 { name = 'ChannelColor1682985520254' diff --git a/packages/backend/migration/1683328299359-channelArchive.js b/packages/backend/migration/1683328299359-channelArchive.js index 10a87246de..83695ff537 100644 --- a/packages/backend/migration/1683328299359-channelArchive.js +++ b/packages/backend/migration/1683328299359-channelArchive.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class ChannelArchive1683328299359 { name = 'ChannelArchive1683328299359' diff --git a/packages/backend/migration/1683682889948-prevent-ai-larning.js b/packages/backend/migration/1683682889948-prevent-ai-larning.js index 167c9f71d2..9d1a19c10b 100644 --- a/packages/backend/migration/1683682889948-prevent-ai-larning.js +++ b/packages/backend/migration/1683682889948-prevent-ai-larning.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class PreventAiLarning1683682889948 { name = 'PreventAiLarning1683682889948' diff --git a/packages/backend/migration/1683683083083-public-reactions-default-true.js b/packages/backend/migration/1683683083083-public-reactions-default-true.js index f416e5ffa7..195ea02a5e 100644 --- a/packages/backend/migration/1683683083083-public-reactions-default-true.js +++ b/packages/backend/migration/1683683083083-public-reactions-default-true.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class PublicReactionsDefaultTrue1683683083083 { name = 'PublicReactionsDefaultTrue1683683083083' diff --git a/packages/backend/migration/1683789676867-fix-typo.js b/packages/backend/migration/1683789676867-fix-typo.js index d647d20e62..c0dbbf0050 100644 --- a/packages/backend/migration/1683789676867-fix-typo.js +++ b/packages/backend/migration/1683789676867-fix-typo.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class FixTypo1683789676867 { name = 'FixTypo1683789676867' diff --git a/packages/backend/migration/1683847157541-UserList.js b/packages/backend/migration/1683847157541-UserList.js index 14a52d64f8..b50a50eed8 100644 --- a/packages/backend/migration/1683847157541-UserList.js +++ b/packages/backend/migration/1683847157541-UserList.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class UserList1683847157541 { name = 'UserList1683847157541' diff --git a/packages/backend/migration/1683869758873-UserListFavorites.js b/packages/backend/migration/1683869758873-UserListFavorites.js index aae4056845..ac9c4c42b9 100644 --- a/packages/backend/migration/1683869758873-UserListFavorites.js +++ b/packages/backend/migration/1683869758873-UserListFavorites.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class UserListFavorites1683869758873 { name = 'UserListFavorites1683869758873' diff --git a/packages/backend/migration/1684206886988-remove-showTimelineReplies.js b/packages/backend/migration/1684206886988-remove-showTimelineReplies.js index 398f9f0803..690653bd7c 100644 --- a/packages/backend/migration/1684206886988-remove-showTimelineReplies.js +++ b/packages/backend/migration/1684206886988-remove-showTimelineReplies.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class RemoveShowTimelineReplies1684206886988 { name = 'RemoveShowTimelineReplies1684206886988' diff --git a/packages/backend/migration/1684386446061-emoji-improve.js b/packages/backend/migration/1684386446061-emoji-improve.js index e7e94769b8..40b0a2bc5e 100644 --- a/packages/backend/migration/1684386446061-emoji-improve.js +++ b/packages/backend/migration/1684386446061-emoji-improve.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class EmojiImprove1684386446061 { name = 'EmojiImprove1684386446061' diff --git a/packages/backend/migration/1685973839966-errorImageUrl.js b/packages/backend/migration/1685973839966-errorImageUrl.js index ca685ef088..fd5d467162 100644 --- a/packages/backend/migration/1685973839966-errorImageUrl.js +++ b/packages/backend/migration/1685973839966-errorImageUrl.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class ErrorImageUrl1685973839966 { name = 'ErrorImageUrl1685973839966' diff --git a/packages/backend/migration/1688280713783-add-meta-options.js b/packages/backend/migration/1688280713783-add-meta-options.js deleted file mode 100644 index 77d1934925..0000000000 --- a/packages/backend/migration/1688280713783-add-meta-options.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AddMetaOptions1688280713783 { - name = 'AddMetaOptions1688280713783' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "enableServerMachineStats" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "meta" ADD "enableIdenticonGeneration" boolean NOT NULL DEFAULT true`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableIdenticonGeneration"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableServerMachineStats"`); - } -} diff --git a/packages/backend/migration/1688720440658-refactor-invite-system.js b/packages/backend/migration/1688720440658-refactor-invite-system.js deleted file mode 100644 index ea192a1950..0000000000 --- a/packages/backend/migration/1688720440658-refactor-invite-system.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class RefactorInviteSystem1688720440658 { - name = 'RefactorInviteSystem1688720440658' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "expiresAt" TIMESTAMP WITH TIME ZONE`); - await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "usedAt" TIMESTAMP WITH TIME ZONE`); - await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "pendingUserId" character varying(32)`); - await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "createdById" character varying(32)`); - await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "usedById" character varying(32)`); - await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "UQ_b6f93f2f30bdbb9a5ebdc7c7189" UNIQUE ("usedById")`); - await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "FK_beba993576db0261a15364ea96e" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "FK_b6f93f2f30bdbb9a5ebdc7c7189" FOREIGN KEY ("usedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "FK_b6f93f2f30bdbb9a5ebdc7c7189"`); - await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "FK_beba993576db0261a15364ea96e"`); - await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "UQ_b6f93f2f30bdbb9a5ebdc7c7189"`); - await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "usedById"`); - await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "createdById"`); - await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "pendingUserId"`); - await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "usedAt"`); - await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "expiresAt"`); - } -} diff --git a/packages/backend/migration/1688880985544-add-index-to-relations.js b/packages/backend/migration/1688880985544-add-index-to-relations.js deleted file mode 100644 index c18903641c..0000000000 --- a/packages/backend/migration/1688880985544-add-index-to-relations.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AddIndexToRelations1688880985544 { - name = 'AddIndexToRelations1688880985544' - - async up(queryRunner) { - await queryRunner.query(`CREATE INDEX "IDX_beba993576db0261a15364ea96" ON "registration_ticket" ("createdById") `); - await queryRunner.query(`CREATE INDEX "IDX_b6f93f2f30bdbb9a5ebdc7c718" ON "registration_ticket" ("usedById") `); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_b6f93f2f30bdbb9a5ebdc7c718"`); - await queryRunner.query(`DROP INDEX "public"."IDX_beba993576db0261a15364ea96"`); - } -} diff --git a/packages/backend/migration/1689102832143-nsfw-cache.js b/packages/backend/migration/1689102832143-nsfw-cache.js deleted file mode 100644 index 90d453418b..0000000000 --- a/packages/backend/migration/1689102832143-nsfw-cache.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class NsfwCache1689102832143 { - name = 'NsfwCache1689102832143' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "cacheRemoteSensitiveFiles" boolean NOT NULL DEFAULT true`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "cacheRemoteSensitiveFiles"`); - } -} diff --git a/packages/backend/migration/1689325027964-UserBlacklistAnntena.js b/packages/backend/migration/1689325027964-UserBlacklistAnntena.js deleted file mode 100644 index 2dc7774493..0000000000 --- a/packages/backend/migration/1689325027964-UserBlacklistAnntena.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class UserBlacklistAnntena1689325027964 { - name = 'UserBlacklistAnntena1689325027964' - - async up(queryRunner) { - await queryRunner.query(`ALTER TYPE "antenna_src_enum" ADD VALUE 'users_blacklist' AFTER 'list'`); - } - - async down(queryRunner) { - } -} diff --git a/packages/backend/migration/1690417561185-fix-renote-muting.js b/packages/backend/migration/1690417561185-fix-renote-muting.js deleted file mode 100644 index d9604ca26c..0000000000 --- a/packages/backend/migration/1690417561185-fix-renote-muting.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class FixRenoteMuting1690417561185 { - name = 'FixRenoteMuting1690417561185' - - async up(queryRunner) { - await queryRunner.query(`DELETE FROM "renote_muting" WHERE "muteeId" NOT IN (SELECT "id" FROM "user")`); - await queryRunner.query(`DELETE FROM "renote_muting" WHERE "muterId" NOT IN (SELECT "id" FROM "user")`); - } - - async down(queryRunner) { - - } -} diff --git a/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js b/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js deleted file mode 100644 index 9bccdb3bb5..0000000000 --- a/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class ChangeCacheRemoteFilesDefault1690417561186 { - name = 'ChangeCacheRemoteFilesDefault1690417561186' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "cacheRemoteFiles" SET DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "cacheRemoteFiles" SET DEFAULT true`); - } -} diff --git a/packages/backend/migration/1690417561187-Fix.js b/packages/backend/migration/1690417561187-Fix.js deleted file mode 100644 index 7f1d62d68c..0000000000 --- a/packages/backend/migration/1690417561187-Fix.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Fix1690417561187 { - name = 'Fix1690417561187' - - async up(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_2cd3b2a6b4cf0b910b260afe08"`); - await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_createdAt"`); - await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`); - await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isRoot" IS 'Whether the User is the root.'`); - await queryRunner.query(`COMMENT ON COLUMN "ad"."startsAt" IS 'The expired date of the Ad.'`); - await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "lastUsedAt" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "mascotImageUrl" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }'`); - await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`); - await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`); - await queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b"`); - await queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b"`); - await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c"`); - await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c"`); - await queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6"`); - await queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6"`); - await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9"`); - await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9"`); - await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`); - await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "UQ_10c146e4b39b443ede016f6736d"`); - await queryRunner.query(`CREATE INDEX "IDX_3fcc2c589eaefc205e0714b99c" ON "ad" ("startsAt") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c71faf11f0a28a5c0bb506203c" ON "channel_favorite" ("userId", "channelId") `); - await queryRunner.query(`CREATE INDEX "IDX_f7b9d338207e40e768e4a5265a" ON "instance" ("firstRetrievedAt") `); - await queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `); - await queryRunner.query(`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `); - await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`); - await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9"`); - await queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6"`); - await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c"`); - await queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b"`); - await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`); - await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`); - await queryRunner.query(`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`); - await queryRunner.query(`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`); - await queryRunner.query(`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`); - await queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`); - await queryRunner.query(`DROP INDEX "public"."IDX_f7b9d338207e40e768e4a5265a"`); - await queryRunner.query(`DROP INDEX "public"."IDX_c71faf11f0a28a5c0bb506203c"`); - await queryRunner.query(`DROP INDEX "public"."IDX_3fcc2c589eaefc205e0714b99c"`); - await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "UQ_10c146e4b39b443ede016f6736d" UNIQUE ("userId")`); - await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9" UNIQUE ("userId")`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6" UNIQUE ("userId")`); - await queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c" UNIQUE ("noteId")`); - await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b" UNIQUE ("noteId")`); - await queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`); - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{admin,administrator,root,system,maintainer,host,mod,moderator,owner,superuser,staff,auth,i,me,everyone,all,mention,mentions,example,user,users,account,accounts,official,help,helps,support,supports,info,information,informations,announce,announces,announcement,announcements,notice,notification,notifications,dev,developer,developers,tech,misskey}'`); - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "mascotImageUrl" SET DEFAULT '/assets/ai.png'`); - await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "lastUsedAt" SET DEFAULT '2023-04-25 06:51:20.985478+00'`); - await queryRunner.query(`COMMENT ON COLUMN "ad"."startsAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isRoot" IS 'Whether the User is the admin.'`); - await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `); - await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `); - await queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_2cd3b2a6b4cf0b910b260afe08" ON "instance" ("firstRetrievedAt") `); - } -} diff --git a/packages/backend/migration/1690569881926-user-2fa-backup-codes.js b/packages/backend/migration/1690569881926-user-2fa-backup-codes.js deleted file mode 100644 index a3ef8dcf08..0000000000 --- a/packages/backend/migration/1690569881926-user-2fa-backup-codes.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class User2faBackupCodes1690569881926 { - name = 'User2faBackupCodes1690569881926' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "twoFactorBackupSecret" character varying array`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twoFactorBackupSecret"`); - } -} diff --git a/packages/backend/migration/1690782653311-SensitiveChannel.js b/packages/backend/migration/1690782653311-SensitiveChannel.js deleted file mode 100644 index afec1a2153..0000000000 --- a/packages/backend/migration/1690782653311-SensitiveChannel.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SensitiveChannel1690782653311 { - name = 'SensitiveChannel1690782653311' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "channel" - ADD "isSensitive" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "isSensitive"`); - } -} diff --git a/packages/backend/migration/1690796169261-play-visibility.js b/packages/backend/migration/1690796169261-play-visibility.js deleted file mode 100644 index 5e5843bfee..0000000000 --- a/packages/backend/migration/1690796169261-play-visibility.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class PlayVisibility1689102832143 { - name = 'PlayVisibility1690796169261' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "public"."flash" ADD "visibility" character varying(512) DEFAULT 'public'`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "public"."flash" DROP COLUMN "visibility"`, undefined); - } -} diff --git a/packages/backend/migration/1691649257651-refine-announcement.js b/packages/backend/migration/1691649257651-refine-announcement.js deleted file mode 100644 index ac621155d5..0000000000 --- a/packages/backend/migration/1691649257651-refine-announcement.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class RefineAnnouncement1691649257651 { - name = 'RefineAnnouncement1691649257651' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "announcement" ADD "display" character varying(256) NOT NULL DEFAULT 'normal'`); - await queryRunner.query(`ALTER TABLE "announcement" ADD "needConfirmationToRead" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "announcement" ADD "isActive" boolean NOT NULL DEFAULT true`); - await queryRunner.query(`ALTER TABLE "announcement" ADD "forExistingUsers" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "announcement" ADD "userId" character varying(32)`); - await queryRunner.query(`CREATE INDEX "IDX_bc1afcc8ef7e9400cdc3c0a87e" ON "announcement" ("isActive") `); - await queryRunner.query(`CREATE INDEX "IDX_da795d3a83187e8832005ba19d" ON "announcement" ("forExistingUsers") `); - await queryRunner.query(`CREATE INDEX "IDX_fd25dfe3da37df1715f11ba6ec" ON "announcement" ("userId") `); - await queryRunner.query(`ALTER TABLE "announcement" ADD CONSTRAINT "FK_fd25dfe3da37df1715f11ba6ec8" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "announcement" DROP CONSTRAINT "FK_fd25dfe3da37df1715f11ba6ec8"`); - await queryRunner.query(`DROP INDEX "public"."IDX_fd25dfe3da37df1715f11ba6ec"`); - await queryRunner.query(`DROP INDEX "public"."IDX_da795d3a83187e8832005ba19d"`); - await queryRunner.query(`DROP INDEX "public"."IDX_bc1afcc8ef7e9400cdc3c0a87e"`); - await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "forExistingUsers"`); - await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "isActive"`); - await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "needConfirmationToRead"`); - await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "display"`); - } -} diff --git a/packages/backend/migration/1691657412740-refine-announcement-2.js b/packages/backend/migration/1691657412740-refine-announcement-2.js deleted file mode 100644 index 67edf19659..0000000000 --- a/packages/backend/migration/1691657412740-refine-announcement-2.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class RefineAnnouncement21691657412740 { - name = 'RefineAnnouncement21691657412740' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "announcement" ADD "icon" character varying(256) NOT NULL DEFAULT 'info'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "icon"`); - } -} diff --git a/packages/backend/migration/1691959191872-passkey-support.js b/packages/backend/migration/1691959191872-passkey-support.js deleted file mode 100644 index 1da9bdb363..0000000000 --- a/packages/backend/migration/1691959191872-passkey-support.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class PasskeySupport1691959191872 { - name = 'PasskeySupport1691959191872' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_security_key" ADD "counter" bigint NOT NULL DEFAULT '0'`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."counter" IS 'The number of times the UserSecurityKey was validated.'`); - await queryRunner.query(`ALTER TABLE "user_security_key" ADD "credentialDeviceType" character varying(32)`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."credentialDeviceType" IS 'The type of Backup Eligibility in authenticator data'`); - await queryRunner.query(`ALTER TABLE "user_security_key" ADD "credentialBackedUp" boolean`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."credentialBackedUp" IS 'Whether or not the credential has been backed up'`); - await queryRunner.query(`ALTER TABLE "user_security_key" ADD "transports" character varying(32) array`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."transports" IS 'The type of the credential returned by the browser'`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."publicKey" IS 'The public key of the UserSecurityKey, hex-encoded.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."lastUsed" IS 'Timestamp of the last time the UserSecurityKey was used.'`); - await queryRunner.query(`ALTER TABLE "user_security_key" ALTER COLUMN "lastUsed" SET DEFAULT now()`); - await queryRunner.query(`UPDATE "user_security_key" SET "id" = REPLACE(REPLACE(REPLACE(REPLACE(ENCODE(DECODE("id", 'hex'), 'base64'), E'\\n', ''), '+', '-'), '/', '_'), '=', ''), "publicKey" = REPLACE(REPLACE(REPLACE(REPLACE(ENCODE(DECODE("publicKey", 'hex'), 'base64'), E'\\n', ''), '+', '-'), '/', '_'), '=', '')`); - await queryRunner.query(`ALTER TABLE "attestation_challenge" DROP CONSTRAINT "FK_f1a461a618fa1755692d0e0d592"`); - await queryRunner.query(`DROP INDEX "IDX_47efb914aed1f72dd39a306c7b"`); - await queryRunner.query(`DROP INDEX "IDX_f1a461a618fa1755692d0e0d59"`); - await queryRunner.query(`DROP TABLE "attestation_challenge"`); - } - - async down(queryRunner) { - await queryRunner.query(`CREATE TABLE "attestation_challenge" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "challenge" character varying(64) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "registrationChallenge" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_d0ba6786e093f1bcb497572a6b5" PRIMARY KEY ("id", "userId"))`); - await queryRunner.query(`CREATE INDEX "IDX_f1a461a618fa1755692d0e0d59" ON "attestation_challenge" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_47efb914aed1f72dd39a306c7b" ON "attestation_challenge" ("challenge") `); - await queryRunner.query(`ALTER TABLE "attestation_challenge" ADD CONSTRAINT "FK_f1a461a618fa1755692d0e0d592" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."challenge" IS 'Hex-encoded sha256 hash of the challenge.'`); - await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."createdAt" IS 'The date challenge was created for expiry purposes.'`); - await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."registrationChallenge" IS 'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.'`); - await queryRunner.query(`UPDATE "user_security_key" SET "id" = ENCODE(DECODE(REPLACE(REPLACE("id" || CASE WHEN LENGTH("id") % 4 = 2 THEN '==' WHEN LENGTH("id") % 4 = 3 THEN '=' ELSE '' END, '-', '+'), '_', '/'), 'base64'), 'hex'), "publicKey" = ENCODE(DECODE(REPLACE(REPLACE("publicKey" || CASE WHEN LENGTH("publicKey") % 4 = 2 THEN '==' WHEN LENGTH("publicKey") % 4 = 3 THEN '=' ELSE '' END, '-', '+'), '_', '/'), 'base64'), 'hex')`); - await queryRunner.query(`ALTER TABLE "user_security_key" ALTER COLUMN "lastUsed" DROP DEFAULT`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."lastUsed" IS 'The date of the last time the UserSecurityKey was successfully validated.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."publicKey" IS 'Variable-length public key used to verify attestations (hex-encoded).'`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."transports" IS 'The type of the credential returned by the browser'`); - await queryRunner.query(`ALTER TABLE "user_security_key" DROP COLUMN "transports"`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."credentialBackedUp" IS 'Whether or not the credential has been backed up'`); - await queryRunner.query(`ALTER TABLE "user_security_key" DROP COLUMN "credentialBackedUp"`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."credentialDeviceType" IS 'The type of Backup Eligibility in authenticator data'`); - await queryRunner.query(`ALTER TABLE "user_security_key" DROP COLUMN "credentialDeviceType"`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."counter" IS 'The number of times the UserSecurityKey was validated.'`); - await queryRunner.query(`ALTER TABLE "user_security_key" DROP COLUMN "counter"`); - } -} diff --git a/packages/backend/migration/1694850832075-server-icons-and-manifest.js b/packages/backend/migration/1694850832075-server-icons-and-manifest.js deleted file mode 100644 index 235bf05744..0000000000 --- a/packages/backend/migration/1694850832075-server-icons-and-manifest.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class ServerIconsAndManifest1694850832075 { - name = 'ServerIconsAndManifest1694850832075' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "app192IconUrl" character varying(1024)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "app512IconUrl" character varying(1024)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "manifestJsonOverride" character varying(8192) NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "manifestJsonOverride"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "app512IconUrl"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "app192IconUrl"`); - } -} diff --git a/packages/backend/migration/1694915420864-clipped-count.js b/packages/backend/migration/1694915420864-clipped-count.js deleted file mode 100644 index 6d70aaecf1..0000000000 --- a/packages/backend/migration/1694915420864-clipped-count.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class ClippedCount1694915420864 { - name = 'ClippedCount1694915420864' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" ADD "clippedCount" smallint NOT NULL DEFAULT '0'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "clippedCount"`); - } -} diff --git a/packages/backend/migration/1695260774117-verified-links.js b/packages/backend/migration/1695260774117-verified-links.js deleted file mode 100644 index 64c8a9ad8f..0000000000 --- a/packages/backend/migration/1695260774117-verified-links.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class VerifiedLinks1695260774117 { - name = 'VerifiedLinks1695260774117' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "verifiedLinks" character varying array NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "verifiedLinks"`); - } -} diff --git a/packages/backend/migration/1695288787870-following-notify.js b/packages/backend/migration/1695288787870-following-notify.js deleted file mode 100644 index b3f78d5f2a..0000000000 --- a/packages/backend/migration/1695288787870-following-notify.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class FollowingNotify1695288787870 { - name = 'FollowingNotify1695288787870' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "following" ADD "notify" character varying(32)`); - await queryRunner.query(`CREATE INDEX "IDX_5108098457488634a4768e1d12" ON "following" ("notify") `); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_5108098457488634a4768e1d12"`); - await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "notify"`); - } -} diff --git a/packages/backend/migration/1695440131671-short-name.js b/packages/backend/migration/1695440131671-short-name.js deleted file mode 100644 index fdc256caf8..0000000000 --- a/packages/backend/migration/1695440131671-short-name.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class ShortName1695440131671 { - name = 'ShortName1695440131671' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "shortName" character varying(64)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "shortName"`); - } -} diff --git a/packages/backend/migration/1695605508898-mutingNotificationTypes.js b/packages/backend/migration/1695605508898-mutingNotificationTypes.js deleted file mode 100644 index 67d4243142..0000000000 --- a/packages/backend/migration/1695605508898-mutingNotificationTypes.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class MutingNotificationTypes1695605508898 { - name = 'MutingNotificationTypes1695605508898' - - async up(queryRunner) { - await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`); - await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test', 'pollVote', 'groupInvited')`); - await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`); - await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`); - await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`); - } - - async down(queryRunner) { - await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app')`); - await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`); - await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`); - await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`); - await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`); - } -} diff --git a/packages/backend/migration/1695901659683-note-updated-at.js b/packages/backend/migration/1695901659683-note-updated-at.js deleted file mode 100644 index e828fb1a6f..0000000000 --- a/packages/backend/migration/1695901659683-note-updated-at.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class NoteUpdatedAt1695901659683 { - name = 'NoteUpdatedAt1695901659683' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`); - } -} diff --git a/packages/backend/migration/1695944637565-notificationRecieveConfig.js b/packages/backend/migration/1695944637565-notificationRecieveConfig.js deleted file mode 100644 index 04a40993c0..0000000000 --- a/packages/backend/migration/1695944637565-notificationRecieveConfig.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class NotificationRecieveConfig1695944637565 { - name = 'NotificationRecieveConfig1695944637565' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutingNotificationTypes"`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "notificationRecieveConfig" jsonb NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "notificationRecieveConfig"`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutingNotificationTypes" "public"."user_profile_notificationrecieveconfig_enum" array NOT NULL DEFAULT '{}'`); - } -} diff --git a/packages/backend/migration/1696003580220-AddSomeUrls.js b/packages/backend/migration/1696003580220-AddSomeUrls.js deleted file mode 100644 index 213e39e7af..0000000000 --- a/packages/backend/migration/1696003580220-AddSomeUrls.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AddSomeUrls1696003580220 { - name = 'AddSomeUrls1696003580220' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "impressumUrl" character varying(1024)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "privacyPolicyUrl" character varying(1024)`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "impressumUrl"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "privacyPolicyUrl"`); - } -} diff --git a/packages/backend/migration/1696222183852-withReplies.js b/packages/backend/migration/1696222183852-withReplies.js deleted file mode 100644 index 84a5511d17..0000000000 --- a/packages/backend/migration/1696222183852-withReplies.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class WithReplies1696222183852 { - name = 'WithReplies1696222183852' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "following" ADD "withReplies" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "user_list_joining" ADD "withReplies" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`); - await queryRunner.query(`ALTER TABLE "user_list_joining" DROP COLUMN "withReplies"`); - await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "withReplies"`); - } -} diff --git a/packages/backend/migration/1696323464251-user-list-membership.js b/packages/backend/migration/1696323464251-user-list-membership.js deleted file mode 100644 index dc1d438dd7..0000000000 --- a/packages/backend/migration/1696323464251-user-list-membership.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class UserListMembership1696323464251 { - name = 'UserListMembership1696323464251' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_list_joining" RENAME TO "user_list_membership"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_list_membership" RENAME TO "user_list_joining"`); - } -} diff --git a/packages/backend/migration/1696331570827-hibernation.js b/packages/backend/migration/1696331570827-hibernation.js deleted file mode 100644 index 1487ece77c..0000000000 --- a/packages/backend/migration/1696331570827-hibernation.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Hibernation1696331570827 { - name = 'Hibernation1696331570827' - - async up(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`); - await queryRunner.query(`ALTER TABLE "user" ADD "isHibernated" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "following" ADD "isFollowerHibernated" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`CREATE INDEX "IDX_ce62b50d882d4e9dee10ad0d2f" ON "following" ("followeeId", "followerHost", "isFollowerHibernated") `); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_ce62b50d882d4e9dee10ad0d2f"`); - await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "isFollowerHibernated"`); - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isHibernated"`); - await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `); - } -} diff --git a/packages/backend/migration/1696332072038-clean.js b/packages/backend/migration/1696332072038-clean.js deleted file mode 100644 index 92a6810d6a..0000000000 --- a/packages/backend/migration/1696332072038-clean.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Clean1696332072038 { - name = 'Clean1696332072038' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_d844bfc6f3f523a05189076efaa"`); - await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_605472305f26818cc93d1baaa74"`); - await queryRunner.query(`DROP INDEX "public"."IDX_d844bfc6f3f523a05189076efa"`); - await queryRunner.query(`DROP INDEX "public"."IDX_605472305f26818cc93d1baaa7"`); - await queryRunner.query(`DROP INDEX "public"."IDX_90f7da835e4c10aca6853621e1"`); - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }'`); - await queryRunner.query(`COMMENT ON COLUMN "user_list_membership"."createdAt" IS 'The created date of the UserListMembership.'`); - await queryRunner.query(`CREATE INDEX "IDX_021015e6683570ae9f6b0c62be" ON "user_list_membership" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_cddcaf418dc4d392ecfcca842a" ON "user_list_membership" ("userListId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e4f3094c43f2d665e6030b0337" ON "user_list_membership" ("userId", "userListId") `); - await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_021015e6683570ae9f6b0c62bee" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_cddcaf418dc4d392ecfcca842a7" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_cddcaf418dc4d392ecfcca842a7"`); - await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_021015e6683570ae9f6b0c62bee"`); - await queryRunner.query(`DROP INDEX "public"."IDX_e4f3094c43f2d665e6030b0337"`); - await queryRunner.query(`DROP INDEX "public"."IDX_cddcaf418dc4d392ecfcca842a"`); - await queryRunner.query(`DROP INDEX "public"."IDX_021015e6683570ae9f6b0c62be"`); - await queryRunner.query(`COMMENT ON COLUMN "user_list_membership"."createdAt" IS 'The created date of the UserListJoining.'`); - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{admin,administrator,root,system,maintainer,host,mod,moderator,owner,superuser,staff,auth,i,me,everyone,all,mention,mentions,example,user,users,account,accounts,official,help,helps,support,supports,info,information,informations,announce,announces,announcement,announcements,notice,notification,notifications,dev,developer,developers,tech,misskey}'`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_90f7da835e4c10aca6853621e1" ON "user_list_membership" ("userId", "userListId") `); - await queryRunner.query(`CREATE INDEX "IDX_605472305f26818cc93d1baaa7" ON "user_list_membership" ("userListId") `); - await queryRunner.query(`CREATE INDEX "IDX_d844bfc6f3f523a05189076efa" ON "user_list_membership" ("userId") `); - await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_605472305f26818cc93d1baaa74" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_d844bfc6f3f523a05189076efaa" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } -} diff --git a/packages/backend/migration/1696373953614-meta-cache-settings.js b/packages/backend/migration/1696373953614-meta-cache-settings.js deleted file mode 100644 index cbbe471d45..0000000000 --- a/packages/backend/migration/1696373953614-meta-cache-settings.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class MetaCacheSettings1696373953614 { - name = 'MetaCacheSettings1696373953614' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "perLocalUserUserTimelineCacheMax" integer NOT NULL DEFAULT '300'`); - await queryRunner.query(`ALTER TABLE "meta" ADD "perRemoteUserUserTimelineCacheMax" integer NOT NULL DEFAULT '100'`); - await queryRunner.query(`ALTER TABLE "meta" ADD "perUserHomeTimelineCacheMax" integer NOT NULL DEFAULT '300'`); - await queryRunner.query(`ALTER TABLE "meta" ADD "perUserListTimelineCacheMax" integer NOT NULL DEFAULT '300'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perUserListTimelineCacheMax"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perUserHomeTimelineCacheMax"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perRemoteUserUserTimelineCacheMax"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perLocalUserUserTimelineCacheMax"`); - } -} diff --git a/packages/backend/migration/1696388600237-revert-note-edit.js b/packages/backend/migration/1696388600237-revert-note-edit.js deleted file mode 100644 index d353c851db..0000000000 --- a/packages/backend/migration/1696388600237-revert-note-edit.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class RevertNoteEdit1696388600237 { - name = 'RevertNoteEdit1696388600237' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`); - } -} diff --git a/packages/backend/migration/1696405744672-clean-up.js b/packages/backend/migration/1696405744672-clean-up.js deleted file mode 100644 index 4e1ee6cd61..0000000000 --- a/packages/backend/migration/1696405744672-clean-up.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class CleanUp1696405744672 { - name = 'CleanUp1696405744672' - - async up(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_e7c0567f5261063592f022e9b5"`); - await queryRunner.query(`DROP INDEX "public"."IDX_25dfc71b0369b003a4cd434d0b"`); - } - - async down(queryRunner) { - await queryRunner.query(`CREATE INDEX "IDX_25dfc71b0369b003a4cd434d0b" ON "note" ("attachedFileTypes") `); - await queryRunner.query(`CREATE INDEX "IDX_e7c0567f5261063592f022e9b5" ON "note" ("createdAt") `); - } -} diff --git a/packages/backend/migration/1696569742153-clean-up.js b/packages/backend/migration/1696569742153-clean-up.js deleted file mode 100644 index b7c981bab2..0000000000 --- a/packages/backend/migration/1696569742153-clean-up.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class CleanUp1696569742153 { - name = 'CleanUp1696569742153' - - async up(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_01f4581f114e0ebd2bbb876f0b"`); - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "score"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" ADD "score" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt") `); - } -} diff --git a/packages/backend/migration/1696581429196-clean-up.js b/packages/backend/migration/1696581429196-clean-up.js deleted file mode 100644 index b6723f3430..0000000000 --- a/packages/backend/migration/1696581429196-clean-up.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class CleanUp1696581429196 { - name = 'CleanUp1696581429196' - - async up(queryRunner) { - await queryRunner.query(`DROP TABLE IF EXISTS "muted_note"`); - } - - async down(queryRunner) { - } -} diff --git a/packages/backend/migration/1696743032098-AdsOnStream.js b/packages/backend/migration/1696743032098-AdsOnStream.js deleted file mode 100644 index 43b9f83e66..0000000000 --- a/packages/backend/migration/1696743032098-AdsOnStream.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AdsOnStream1696743032098 { - name = 'AdsOnStream1696743032098' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "notesPerOneAd" integer NOT NULL DEFAULT '0'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "notesPerOneAd"`); - } -} diff --git a/packages/backend/migration/1696807733453-userListUserId.js b/packages/backend/migration/1696807733453-userListUserId.js deleted file mode 100644 index 8f0ae2cd87..0000000000 --- a/packages/backend/migration/1696807733453-userListUserId.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class UserListUserId1696807733453 { - name = 'UserListUserId1696807733453' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_list_membership" ADD "userListUserId" character varying(32) NOT NULL DEFAULT ''`); - const memberships = await queryRunner.query(`SELECT "id", "userListId" FROM "user_list_membership"`); - for(let i = 0; i < memberships.length; i++) { - const userList = await queryRunner.query(`SELECT "userId" FROM "user_list" WHERE "id" = $1`, [memberships[i].userListId]); - await queryRunner.query(`UPDATE "user_list_membership" SET "userListUserId" = $1 WHERE "id" = $2`, [userList[0].userId, memberships[i].id]); - } - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_list_membership" DROP COLUMN "userListUserId"`); - } -} diff --git a/packages/backend/migration/1696808725134-userListUserId-2.js b/packages/backend/migration/1696808725134-userListUserId-2.js deleted file mode 100644 index cc504e761c..0000000000 --- a/packages/backend/migration/1696808725134-userListUserId-2.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class UserListUserId21696808725134 { - name = 'UserListUserId21696808725134' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_list_membership" ALTER COLUMN "userListUserId" DROP DEFAULT`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_list_membership" ALTER COLUMN "userListUserId" SET DEFAULT ''`); - } -} diff --git a/packages/backend/migration/1697247230117-InstanceSilence.js b/packages/backend/migration/1697247230117-InstanceSilence.js deleted file mode 100644 index 309d817087..0000000000 --- a/packages/backend/migration/1697247230117-InstanceSilence.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class InstanceSilence1697247230117 { - name = 'InstanceSilence1697247230117' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "silencedHosts" character varying(1024) array NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`); - } -} diff --git a/packages/backend/migration/1697420555911-deleteCreatedAt.js b/packages/backend/migration/1697420555911-deleteCreatedAt.js deleted file mode 100644 index 407a5f449a..0000000000 --- a/packages/backend/migration/1697420555911-deleteCreatedAt.js +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class DeleteCreatedAt1697420555911 { - name = 'DeleteCreatedAt1697420555911' - - async up(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_02878d441ceae15ce060b73daf"`); - await queryRunner.query(`DROP INDEX "public"."IDX_c8dfad3b72196dd1d6b5db168a"`); - await queryRunner.query(`DROP INDEX "public"."IDX_e11e649824a45d8ed01d597fd9"`); - await queryRunner.query(`DROP INDEX "public"."IDX_db2098070b2b5a523c58181f74"`); - await queryRunner.query(`DROP INDEX "public"."IDX_048a757923ed8b157e9895da53"`); - await queryRunner.query(`DROP INDEX "public"."IDX_1129c2ef687fc272df040bafaa"`); - await queryRunner.query(`DROP INDEX "public"."IDX_118ec703e596086fc4515acb39"`); - await queryRunner.query(`DROP INDEX "public"."IDX_b9a354f7941c1e779f3b33aea6"`); - await queryRunner.query(`DROP INDEX "public"."IDX_71cb7b435b7c0d4843317e7e16"`); - await queryRunner.query(`DROP INDEX "public"."IDX_11e71f2511589dcc8a4d3214f9"`); - await queryRunner.query(`DROP INDEX "public"."IDX_735a5544f9249d412255f47f95"`); - await queryRunner.query(`DROP INDEX "public"."IDX_582f8fab771a9040a12961f3e7"`); - await queryRunner.query(`DROP INDEX "public"."IDX_8f1a239bd077c8864a20c62c2c"`); - await queryRunner.query(`DROP INDEX "public"."IDX_f86d57fbca33c7a4e6897490cc"`); - await queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`); - await queryRunner.query(`DROP INDEX "public"."IDX_fbb4297c927a9b85e9cefa2eb1"`); - await queryRunner.query(`DROP INDEX "public"."IDX_0fb627e1c2f753262a74f0562d"`); - await queryRunner.query(`DROP INDEX "public"."IDX_149d2e44785707548c82999b01"`); - await queryRunner.query(`ALTER TABLE "drive_folder" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "app" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "announcement_read" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "user_list" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "auth_session" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "blocking" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "channel_following" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "channel_favorite" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "clip" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "clip_favorite" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "follow_request" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "gallery_post" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "gallery_like" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "moderation_log" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "muting" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "renote_muting" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "note_favorite" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "note_reaction" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "note_thread_muting" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "page_like" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "password_reset_request" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "poll_vote" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "promo_read" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "registry_item" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "signin" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "sw_subscription" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "user_list_favorite" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "user_list_membership" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "user_note_pining" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "user_pending" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "webhook" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "role_assignment" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "flash" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "flash_like" DROP COLUMN "createdAt"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "flash_like" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "flash" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "role_assignment" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "role" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "webhook" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_pending" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_note_pining" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_list_membership" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_list_favorite" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "sw_subscription" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "signin" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "registry_item" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "promo_read" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "poll_vote" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "password_reset_request" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "page_like" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "page" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "note_thread_muting" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "note_reaction" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "note_favorite" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "renote_muting" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "muting" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "moderation_log" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "gallery_like" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "gallery_post" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "follow_request" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "following" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "clip_favorite" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "note" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "clip" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "channel_favorite" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "channel_following" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "channel" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "blocking" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "auth_session" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "antenna" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_list" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "announcement_read" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "announcement" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "ad" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "access_token" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "app" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "user" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "drive_file" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "drive_folder" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`CREATE INDEX "IDX_149d2e44785707548c82999b01" ON "flash" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_0fb627e1c2f753262a74f0562d" ON "poll_vote" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_fbb4297c927a9b85e9cefa2eb1" ON "page" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_f86d57fbca33c7a4e6897490cc" ON "muting" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_8f1a239bd077c8864a20c62c2c" ON "gallery_post" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_582f8fab771a9040a12961f3e7" ON "following" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_735a5544f9249d412255f47f95" ON "channel_favorite" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_11e71f2511589dcc8a4d3214f9" ON "channel_following" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_71cb7b435b7c0d4843317e7e16" ON "channel" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_b9a354f7941c1e779f3b33aea6" ON "blocking" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_118ec703e596086fc4515acb39" ON "announcement" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_1129c2ef687fc272df040bafaa" ON "ad" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_048a757923ed8b157e9895da53" ON "app" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_db2098070b2b5a523c58181f74" ON "abuse_user_report" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_e11e649824a45d8ed01d597fd9" ON "user" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_c8dfad3b72196dd1d6b5db168a" ON "drive_file" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_02878d441ceae15ce060b73daf" ON "drive_folder" ("createdAt") `); - } -} diff --git a/packages/backend/migration/1697436246389-antenna-localOnly.js b/packages/backend/migration/1697436246389-antenna-localOnly.js deleted file mode 100644 index d7c0ca6510..0000000000 --- a/packages/backend/migration/1697436246389-antenna-localOnly.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AntennaLocalOnly1697436246389 { - name = 'AntennaLocalOnly1697436246389' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" ADD "localOnly" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "localOnly"`); - } -} diff --git a/packages/backend/migration/1697441463087-FollowRequestWithReplies.js b/packages/backend/migration/1697441463087-FollowRequestWithReplies.js deleted file mode 100644 index 58b61aff63..0000000000 --- a/packages/backend/migration/1697441463087-FollowRequestWithReplies.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - - -export class FollowRequestWithReplies1697441463087 { - name = 'FollowRequestWithReplies1697441463087' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "follow_request" ADD "withReplies" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "follow_request" DROP COLUMN "withReplies"`); - } -} diff --git a/packages/backend/migration/1697673894459-note-reactionAndUserPairCache.js b/packages/backend/migration/1697673894459-note-reactionAndUserPairCache.js deleted file mode 100644 index fab07fd3f4..0000000000 --- a/packages/backend/migration/1697673894459-note-reactionAndUserPairCache.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - - -export class NoteReactionAndUserPairCache1697673894459 { - name = 'NoteReactionAndUserPairCache1697673894459' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" ADD "reactionAndUserPairCache" character varying(1024) array NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "reactionAndUserPairCache"`); - } -} diff --git a/packages/backend/migration/1697847397844-avatar-decoration.js b/packages/backend/migration/1697847397844-avatar-decoration.js deleted file mode 100644 index 32ee47e968..0000000000 --- a/packages/backend/migration/1697847397844-avatar-decoration.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AvatarDecoration1697847397844 { - name = 'AvatarDecoration1697847397844' - - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "avatar_decoration" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE, "url" character varying(1024) NOT NULL, "name" character varying(256) NOT NULL, "description" character varying(2048) NOT NULL, "roleIdsThatCanBeUsedThisDecoration" character varying(128) array NOT NULL DEFAULT '{}', CONSTRAINT "PK_b6de9296f6097078e1dc53f7603" PRIMARY KEY ("id"))`); - await queryRunner.query(`ALTER TABLE "user" ADD "avatarDecorations" character varying(512) array NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarDecorations"`); - await queryRunner.query(`DROP TABLE "avatar_decoration"`); - } -} diff --git a/packages/backend/migration/1697941908548-avatar-decoration2.js b/packages/backend/migration/1697941908548-avatar-decoration2.js deleted file mode 100644 index 58344e2bb6..0000000000 --- a/packages/backend/migration/1697941908548-avatar-decoration2.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AvatarDecoration21697941908548 { - name = 'AvatarDecoration21697941908548' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarDecorations"`); - await queryRunner.query(`ALTER TABLE "user" ADD "avatarDecorations" jsonb NOT NULL DEFAULT '[]'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarDecorations"`); - await queryRunner.query(`ALTER TABLE "user" ADD "avatarDecorations" character varying(512) array NOT NULL DEFAULT '{}'`); - } -} diff --git a/packages/backend/migration/1698041201306-enable-ftt.js b/packages/backend/migration/1698041201306-enable-ftt.js deleted file mode 100644 index c67dda6f5f..0000000000 --- a/packages/backend/migration/1698041201306-enable-ftt.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class EnableFtt1698041201306 { - name = 'EnableFtt1698041201306' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "enableFanoutTimeline" boolean NOT NULL DEFAULT true`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableFanoutTimeline"`); - } -} diff --git a/packages/backend/migration/1698840138000-add-allow-renote-to-external.js b/packages/backend/migration/1698840138000-add-allow-renote-to-external.js deleted file mode 100644 index 8ce35b0f69..0000000000 --- a/packages/backend/migration/1698840138000-add-allow-renote-to-external.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AddAllowRenoteToExternal1698840138000 { - name = 'AddAllowRenoteToExternal1698840138000' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "channel" ADD "allowRenoteToExternal" boolean NOT NULL DEFAULT true`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "allowRenoteToExternal"`); - } -} diff --git a/packages/backend/migration/1699141698112-announcement-silence.js b/packages/backend/migration/1699141698112-announcement-silence.js deleted file mode 100644 index f462d30b51..0000000000 --- a/packages/backend/migration/1699141698112-announcement-silence.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AnnouncementSilence1699141698112 { - name = 'AnnouncementSilence1699141698112' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "announcement" ADD "silence" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`CREATE INDEX "IDX_7b8d9225168e962f94ea517e00" ON "announcement" ("silence") `); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_7b8d9225168e962f94ea517e00"`); - await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "silence"`); - } -} diff --git a/packages/backend/migration/1700096812223-enableFanoutTimelineDbFallback.js b/packages/backend/migration/1700096812223-enableFanoutTimelineDbFallback.js deleted file mode 100644 index 2ab93624ce..0000000000 --- a/packages/backend/migration/1700096812223-enableFanoutTimelineDbFallback.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class EnableFanoutTimelineDbFallback1700096812223 { - name = 'EnableFanoutTimelineDbFallback1700096812223' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "enableFanoutTimelineDbFallback" boolean NOT NULL DEFAULT true`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableFanoutTimelineDbFallback"`); - } -} diff --git a/packages/backend/migration/1700303245007-supportVerifyMailApi.js b/packages/backend/migration/1700303245007-supportVerifyMailApi.js deleted file mode 100644 index 58ff7a69c4..0000000000 --- a/packages/backend/migration/1700303245007-supportVerifyMailApi.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SupportVerifyMailApi1700303245007 { - name = 'SupportVerifyMailApi1700303245007' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "verifymailAuthKey" character varying(1024)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "enableVerifymailApi" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableVerifymailApi"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "verifymailAuthKey"`); - } -} diff --git a/packages/backend/migration/1700383825690-hard-mute.js b/packages/backend/migration/1700383825690-hard-mute.js deleted file mode 100644 index 92c3ada4a1..0000000000 --- a/packages/backend/migration/1700383825690-hard-mute.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class HardMute1700383825690 { - name = 'HardMute1700383825690' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`); - } -} diff --git a/packages/backend/migration/1700902349231-add-bday-index.js b/packages/backend/migration/1700902349231-add-bday-index.js deleted file mode 100644 index c58165c70e..0000000000 --- a/packages/backend/migration/1700902349231-add-bday-index.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AddBdayIndex1700902349231 { - name = 'AddBdayIndex1700902349231' - - async up(queryRunner) { - await queryRunner.query(`CREATE INDEX "IDX_de22cd2b445eee31ae51cdbe99" ON "user_profile" (SUBSTR("birthday", 6, 5))`); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_de22cd2b445eee31ae51cdbe99"`); - } -} diff --git a/packages/backend/migration/1702718871541-ffVisibility.js b/packages/backend/migration/1702718871541-ffVisibility.js deleted file mode 100644 index 164af00f25..0000000000 --- a/packages/backend/migration/1702718871541-ffVisibility.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class ffVisibility1702718871541 { - constructor() { - this.name = 'ffVisibility1702718871541'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TYPE "public"."user_profile_followingvisibility_enum" AS ENUM('public', 'followers', 'private')`); - await queryRunner.query(`CREATE CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followingvisibility_enum") WITH INOUT AS ASSIGNMENT`); - await queryRunner.query(`CREATE TYPE "public"."user_profile_followersVisibility_enum" AS ENUM('public', 'followers', 'private')`); - await queryRunner.query(`CREATE CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followersVisibility_enum") WITH INOUT AS ASSIGNMENT`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "followingVisibility" "public"."user_profile_followingvisibility_enum" NOT NULL DEFAULT 'public'`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "followersVisibility" "public"."user_profile_followersVisibility_enum" NOT NULL DEFAULT 'public'`); - await queryRunner.query(`UPDATE "user_profile" SET "followingVisibility" = "ffVisibility"`); - await queryRunner.query(`UPDATE "user_profile" SET "followersVisibility" = "ffVisibility"`); - await queryRunner.query(`DROP CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followersVisibility_enum")`); - await queryRunner.query(`DROP CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followingvisibility_enum")`); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "ffVisibility"`); - await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum"`); - } - async down(queryRunner) { - await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum" AS ENUM('public', 'followers', 'private')`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "ffVisibility" "public"."user_profile_ffvisibility_enum" NOT NULL DEFAULT 'public'`); - - await queryRunner.query(`CREATE CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum") WITH INOUT AS ASSIGNMENT`); - await queryRunner.query(`UPDATE "user_profile" SET "ffVisibility" = "followingVisibility"`); - await queryRunner.query(`DROP CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum")`); - - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followersVisibility"`); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followingVisibility"`); - await queryRunner.query(`DROP TYPE "public"."user_profile_followersVisibility_enum"`); - await queryRunner.query(`DROP TYPE "public"."user_profile_followingvisibility_enum"`); - } -} diff --git a/packages/backend/migration/1703209889304-bannedEmailDomains.js b/packages/backend/migration/1703209889304-bannedEmailDomains.js deleted file mode 100644 index 2fdd4e1183..0000000000 --- a/packages/backend/migration/1703209889304-bannedEmailDomains.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class bannedEmailDomains1703209889304 { - constructor() { - this.name = 'bannedEmailDomains1703209889304'; - } - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "bannedEmailDomains" character varying(1024) array NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "bannedEmailDomains"`); - } -} diff --git a/packages/backend/migration/1703658526000-supportTrueMailApi.js b/packages/backend/migration/1703658526000-supportTrueMailApi.js deleted file mode 100644 index fb62653e40..0000000000 --- a/packages/backend/migration/1703658526000-supportTrueMailApi.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SupportTrueMailApi1703658526000 { - name = 'SupportTrueMailApi1703658526000' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "truemailInstance" character varying(1024)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "truemailAuthKey" character varying(1024)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "enableTruemailApi" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTruemailApi"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "truemailInstance"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "truemailAuthKey"`); - } -} diff --git a/packages/backend/migration/1704373210054-support-mcaptcha.js b/packages/backend/migration/1704373210054-support-mcaptcha.js deleted file mode 100644 index 50b4801e14..0000000000 --- a/packages/backend/migration/1704373210054-support-mcaptcha.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SupportMcaptcha1704373210054 { - name = 'SupportMcaptcha1704373210054' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "enableMcaptcha" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSitekey" character varying(1024)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSecretKey" character varying(1024)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaInstanceUrl" character varying(1024)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaInstanceUrl"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSecretKey"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSitekey"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableMcaptcha"`); - } -} diff --git a/packages/backend/migration/1704959805077-bubble-game-record.js b/packages/backend/migration/1704959805077-bubble-game-record.js deleted file mode 100644 index 6c4d7ab1a9..0000000000 --- a/packages/backend/migration/1704959805077-bubble-game-record.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class BubbleGameRecord1704959805077 { - name = 'BubbleGameRecord1704959805077' - - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "bubble_game_record" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "seededAt" TIMESTAMP WITH TIME ZONE NOT NULL, "seed" character varying(1024) NOT NULL, "gameVersion" integer NOT NULL, "gameMode" character varying(128) NOT NULL, "score" integer NOT NULL, "logs" jsonb NOT NULL DEFAULT '[]', "isVerified" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_a75395fe404b392e2893b50d7ea" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_75276757070d21fdfaf4c05290" ON "bubble_game_record" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_4ae7053179014915d1432d3f40" ON "bubble_game_record" ("seededAt") `); - await queryRunner.query(`CREATE INDEX "IDX_26d4ee490b5a487142d35466ee" ON "bubble_game_record" ("score") `); - await queryRunner.query(`ALTER TABLE "bubble_game_record" ADD CONSTRAINT "FK_75276757070d21fdfaf4c052909" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "bubble_game_record" DROP CONSTRAINT "FK_75276757070d21fdfaf4c052909"`); - await queryRunner.query(`DROP INDEX "public"."IDX_26d4ee490b5a487142d35466ee"`); - await queryRunner.query(`DROP INDEX "public"."IDX_4ae7053179014915d1432d3f40"`); - await queryRunner.query(`DROP INDEX "public"."IDX_75276757070d21fdfaf4c05290"`); - await queryRunner.query(`DROP TABLE "bubble_game_record"`); - } -} diff --git a/packages/backend/migration/1705222772858-optimize-note-index-for-array-column.js b/packages/backend/migration/1705222772858-optimize-note-index-for-array-column.js deleted file mode 100644 index fe0a5a2bcf..0000000000 --- a/packages/backend/migration/1705222772858-optimize-note-index-for-array-column.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class OptimizeNoteIndexForArrayColumns1705222772858 { - name = 'OptimizeNoteIndexForArrayColumns1705222772858' - - async up(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_796a8c03959361f97dc2be1d5c"`); - await queryRunner.query(`DROP INDEX "public"."IDX_54ebcb6d27222913b908d56fd8"`); - await queryRunner.query(`DROP INDEX "public"."IDX_88937d94d7443d9a99a76fa5c0"`); - await queryRunner.query(`DROP INDEX "public"."IDX_51c063b6a133a9cb87145450f5"`); - await queryRunner.query(`CREATE INDEX "IDX_NOTE_FILE_IDS" ON "note" using gin ("fileIds")`) - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_NOTE_FILE_IDS"`) - await queryRunner.query(`CREATE INDEX "IDX_51c063b6a133a9cb87145450f5" ON "note" ("fileIds") `); - await queryRunner.query(`CREATE INDEX "IDX_88937d94d7443d9a99a76fa5c0" ON "note" ("tags") `); - await queryRunner.query(`CREATE INDEX "IDX_54ebcb6d27222913b908d56fd8" ON "note" ("mentions") `); - await queryRunner.query(`CREATE INDEX "IDX_796a8c03959361f97dc2be1d5c" ON "note" ("visibleUserIds") `); - } -} diff --git a/packages/backend/migration/1705475608437-reversi.js b/packages/backend/migration/1705475608437-reversi.js deleted file mode 100644 index 9921728457..0000000000 --- a/packages/backend/migration/1705475608437-reversi.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Reversi1705475608437 { - name = 'Reversi1705475608437' - - async up(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_b46ec40746efceac604142be1c"`); - await queryRunner.query(`DROP INDEX "public"."IDX_b604d92d6c7aec38627f6eaf16"`); - await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "reversi_matching" DROP COLUMN "createdAt"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_matching" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "reversi_game" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`CREATE INDEX "IDX_b604d92d6c7aec38627f6eaf16" ON "reversi_matching" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt") `); - } -} diff --git a/packages/backend/migration/1705654039457-reversi-2.js b/packages/backend/migration/1705654039457-reversi-2.js deleted file mode 100644 index 6685dca73b..0000000000 --- a/packages/backend/migration/1705654039457-reversi-2.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Reversi21705654039457 { - name = 'Reversi21705654039457' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user1Accepted" TO "user1Ready"`); - await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user2Accepted" TO "user2Ready"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user1Ready" TO "user1Accepted"`); - await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user2Ready" TO "user2Accepted"`); - } -} diff --git a/packages/backend/migration/1705793785675-reversi-3.js b/packages/backend/migration/1705793785675-reversi-3.js deleted file mode 100644 index 94b1e4fac9..0000000000 --- a/packages/backend/migration/1705793785675-reversi-3.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Reversi31705793785675 { - name = 'Reversi31705793785675' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "surrendered" TO "surrenderedUserId"`); - await queryRunner.query(`ALTER TABLE "reversi_game" ADD "timeoutUserId" character varying(32)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "timeoutUserId"`); - await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "surrenderedUserId" TO "surrendered"`); - } -} diff --git a/packages/backend/migration/1705794768153-reversi-4.js b/packages/backend/migration/1705794768153-reversi-4.js deleted file mode 100644 index 95119cabba..0000000000 --- a/packages/backend/migration/1705794768153-reversi-4.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Reversi41705794768153 { - name = 'Reversi41705794768153' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" ADD "endedAt" TIMESTAMP WITH TIME ZONE`); - await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."endedAt" IS 'The ended date of the ReversiGame.'`); - } - - async down(queryRunner) { - await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."endedAt" IS 'The ended date of the ReversiGame.'`); - await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "endedAt"`); - } -} diff --git a/packages/backend/migration/1705798904141-reversi-5.js b/packages/backend/migration/1705798904141-reversi-5.js deleted file mode 100644 index f1a1a42d46..0000000000 --- a/packages/backend/migration/1705798904141-reversi-5.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Reversi51705798904141 { - name = 'Reversi51705798904141' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" ADD "timeLimitForEachTurn" smallint NOT NULL DEFAULT '90'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "timeLimitForEachTurn"`); - } -} diff --git a/packages/backend/migration/1706081514499-reversi-6.js b/packages/backend/migration/1706081514499-reversi-6.js deleted file mode 100644 index 0d9e5cbbf2..0000000000 --- a/packages/backend/migration/1706081514499-reversi-6.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Reversi61706081514499 { - name = 'Reversi61706081514499' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" ADD "noIrregularRules" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "noIrregularRules"`); - } -} diff --git a/packages/backend/migration/1706791962000-fix-meta-disableRegistration.js b/packages/backend/migration/1706791962000-fix-meta-disableRegistration.js deleted file mode 100644 index 1c45f3756d..0000000000 --- a/packages/backend/migration/1706791962000-fix-meta-disableRegistration.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class FixMetaDisableRegistration1706791962000 { - name = 'FixMetaDisableRegistration1706791962000' - - async up(queryRunner) { - await queryRunner.query(`alter table meta alter column "disableRegistration" set default true;`); - } - - async down(queryRunner) { - await queryRunner.query(`alter table meta alter column "disableRegistration" set default false;`); - } -} diff --git a/packages/backend/migration/1707429690000-prohibited-words.js b/packages/backend/migration/1707429690000-prohibited-words.js deleted file mode 100644 index 44e96cb160..0000000000 --- a/packages/backend/migration/1707429690000-prohibited-words.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class prohibitedWords1707429690000 { - name = 'prohibitedWords1707429690000' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "prohibitedWords" character varying(1024) array NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "prohibitedWords"`); - } -} diff --git a/packages/backend/migration/1707808106310-MakeRepositoryUrlNullable.js b/packages/backend/migration/1707808106310-MakeRepositoryUrlNullable.js deleted file mode 100644 index 335b14976c..0000000000 --- a/packages/backend/migration/1707808106310-MakeRepositoryUrlNullable.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class MakeRepositoryUrlNullable1707808106310 { - name = 'MakeRepositoryUrlNullable1707808106310' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" DROP NOT NULL`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET NOT NULL`); - } -} diff --git a/packages/backend/migration/1708266695091-repositoryUrl-from-syuilo-to-misskey-dev.js b/packages/backend/migration/1708266695091-repositoryUrl-from-syuilo-to-misskey-dev.js deleted file mode 100644 index e4dbaa16d0..0000000000 --- a/packages/backend/migration/1708266695091-repositoryUrl-from-syuilo-to-misskey-dev.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class RepositoryUrlFromSyuiloToMisskeyDev1708266695091 { - name = 'RepositoryUrlFromSyuiloToMisskeyDev1708266695091' - - async up(queryRunner) { - await queryRunner.query(`UPDATE "meta" SET "repositoryUrl" = 'https://github.com/misskey-dev/misskey' WHERE "repositoryUrl" = 'https://github.com/syuilo/misskey'`); - } - - async down(queryRunner) { - // no valid down migration - } -} diff --git a/packages/backend/migration/1708399372194-per-instance-mod-note.js b/packages/backend/migration/1708399372194-per-instance-mod-note.js deleted file mode 100644 index 339a4d7af9..0000000000 --- a/packages/backend/migration/1708399372194-per-instance-mod-note.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class PerInstanceModNote1708399372194 { - name = 'PerInstanceModNote1708399372194' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" ADD "moderationNote" character varying(16384) NOT NULL DEFAULT ''`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "moderationNote"`); - } -} diff --git a/packages/backend/migration/1709126576000-optimize-emoji-index.js b/packages/backend/migration/1709126576000-optimize-emoji-index.js deleted file mode 100644 index e4184895d0..0000000000 --- a/packages/backend/migration/1709126576000-optimize-emoji-index.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class OptimizeEmojiIndex1709126576000 { - name = 'OptimizeEmojiIndex1709126576000' - - async up(queryRunner) { - await queryRunner.query(`CREATE INDEX "IDX_EMOJI_ROLE_IDS" ON "emoji" using gin ("roleIdsThatCanBeUsedThisEmojiAsReaction")`) - await queryRunner.query(`CREATE INDEX "IDX_EMOJI_CATEGORY" ON "emoji" ("category")`) - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_EMOJI_CATEGORY"`) - await queryRunner.query(`DROP INDEX "IDX_EMOJI_ROLE_IDS"`) - } -} diff --git a/packages/backend/migration/1710512074000-url-preview-meta.js b/packages/backend/migration/1710512074000-url-preview-meta.js deleted file mode 100644 index 8af521bbf4..0000000000 --- a/packages/backend/migration/1710512074000-url-preview-meta.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class UrlPreviewMeta1710512074000 { - name = 'UrlPreviewMeta1710512074000' - - async up(queryRunner) { - await queryRunner.query(` - alter table meta - rename column "summalyProxy" to "urlPreviewSummaryProxyUrl"; - alter table meta - add "urlPreviewEnabled" boolean default true not null; - alter table meta - add "urlPreviewTimeout" integer default 10000 not null; - alter table meta - add "urlPreviewMaximumContentLength" bigint default 10485760 not null; - alter table meta - add "urlPreviewRequireContentLength" boolean default false not null; - alter table meta - add "urlPreviewUserAgent" varchar(1024) default null; - `); - } - - async down(queryRunner) { - await queryRunner.query(` - alter table meta - rename column "urlPreviewSummaryProxyUrl" to "summalyProxy"; - alter table meta - drop column "urlPreviewEnabled"; - alter table meta - drop column "urlPreviewTimeout"; - alter table meta - drop column "urlPreviewMaximumContentLength"; - alter table meta - drop column "urlPreviewRequireContentLength"; - alter table meta - drop column "urlPreviewUserAgent"; - `); - } -} diff --git a/packages/backend/migration/1710919614510-antenna-exclude-bots.js b/packages/backend/migration/1710919614510-antenna-exclude-bots.js deleted file mode 100644 index fac84317cc..0000000000 --- a/packages/backend/migration/1710919614510-antenna-exclude-bots.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AntennaExcludeBots1710919614510 { - name = 'AntennaExcludeBots1710919614510' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" ADD "excludeBots" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "excludeBots"`); - } -} diff --git a/packages/backend/migration/1713656541000-abuse-report-notification.js b/packages/backend/migration/1713656541000-abuse-report-notification.js deleted file mode 100644 index 4a754f81e2..0000000000 --- a/packages/backend/migration/1713656541000-abuse-report-notification.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AbuseReportNotification1713656541000 { - name = 'AbuseReportNotification1713656541000' - - async up(queryRunner) { - await queryRunner.query(` - CREATE TABLE "system_webhook" ( - "id" varchar(32) NOT NULL, - "isActive" boolean NOT NULL DEFAULT true, - "updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - "latestSentAt" timestamp with time zone NULL DEFAULT NULL, - "latestStatus" integer NULL DEFAULT NULL, - "name" varchar(255) NOT NULL, - "on" varchar(128) [] NOT NULL DEFAULT '{}'::character varying[], - "url" varchar(1024) NOT NULL, - "secret" varchar(1024) NOT NULL, - CONSTRAINT "PK_system_webhook_id" PRIMARY KEY ("id") - ); - CREATE INDEX "IDX_system_webhook_isActive" ON "system_webhook" ("isActive"); - CREATE INDEX "IDX_system_webhook_on" ON "system_webhook" USING gin ("on"); - - CREATE TABLE "abuse_report_notification_recipient" ( - "id" varchar(32) NOT NULL, - "isActive" boolean NOT NULL DEFAULT true, - "updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - "name" varchar(255) NOT NULL, - "method" varchar(64) NOT NULL, - "userId" varchar(32) NULL DEFAULT NULL, - "systemWebhookId" varchar(32) NULL DEFAULT NULL, - CONSTRAINT "PK_abuse_report_notification_recipient_id" PRIMARY KEY ("id"), - CONSTRAINT "FK_abuse_report_notification_recipient_userId1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION, - CONSTRAINT "FK_abuse_report_notification_recipient_userId2" FOREIGN KEY ("userId") REFERENCES "user_profile"("userId") ON DELETE CASCADE ON UPDATE NO ACTION, - CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId" FOREIGN KEY ("systemWebhookId") REFERENCES "system_webhook"("id") ON DELETE CASCADE ON UPDATE NO ACTION - ); - CREATE INDEX "IDX_abuse_report_notification_recipient_isActive" ON "abuse_report_notification_recipient" ("isActive"); - CREATE INDEX "IDX_abuse_report_notification_recipient_method" ON "abuse_report_notification_recipient" ("method"); - CREATE INDEX "IDX_abuse_report_notification_recipient_userId" ON "abuse_report_notification_recipient" ("userId"); - CREATE INDEX "IDX_abuse_report_notification_recipient_systemWebhookId" ON "abuse_report_notification_recipient" ("systemWebhookId"); - `); - } - - async down(queryRunner) { - await queryRunner.query(` - ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId1"; - ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId2"; - ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId"; - DROP INDEX "IDX_abuse_report_notification_recipient_isActive"; - DROP INDEX "IDX_abuse_report_notification_recipient_method"; - DROP INDEX "IDX_abuse_report_notification_recipient_userId"; - DROP INDEX "IDX_abuse_report_notification_recipient_systemWebhookId"; - DROP TABLE "abuse_report_notification_recipient"; - - DROP INDEX "IDX_system_webhook_isActive"; - DROP INDEX "IDX_system_webhook_on"; - DROP TABLE "system_webhook"; - `); - } -} diff --git a/packages/backend/migration/1716129964060-ChannelIdDenormalizedForMiPoll.js b/packages/backend/migration/1716129964060-ChannelIdDenormalizedForMiPoll.js deleted file mode 100644 index f736378c04..0000000000 --- a/packages/backend/migration/1716129964060-ChannelIdDenormalizedForMiPoll.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class ChannelIdDenormalizedForMiPoll1716129964060 { - name = 'ChannelIdDenormalizedForMiPoll1716129964060' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "poll" ADD "channelId" character varying(32)`); - await queryRunner.query(`COMMENT ON COLUMN "poll"."channelId" IS '[Denormalized]'`); - await queryRunner.query(`CREATE INDEX "IDX_c1240fcc9675946ea5d6c2860e" ON "poll" ("channelId") `); - await queryRunner.query(`UPDATE "poll" SET "channelId" = "note"."channelId" FROM "note" WHERE "poll"."noteId" = "note"."id"`); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_c1240fcc9675946ea5d6c2860e"`); - await queryRunner.query(`COMMENT ON COLUMN "poll"."channelId" IS '[Denormalized]'`); - await queryRunner.query(`ALTER TABLE "poll" DROP COLUMN "channelId"`); - } -} diff --git a/packages/backend/migration/1716197366117-MediaSilenceForHosts.js b/packages/backend/migration/1716197366117-MediaSilenceForHosts.js deleted file mode 100644 index 10bb7f0255..0000000000 --- a/packages/backend/migration/1716197366117-MediaSilenceForHosts.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class MediaSilenceForHosts1716197366117 { - name = 'MediaSilenceForHosts1716197366117' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "mediaSilencedHosts" character varying(1024) array NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mediaSilencedHosts"`); - } -} diff --git a/packages/backend/migration/1716345015347-NotRespondingSince.js b/packages/backend/migration/1716345015347-NotRespondingSince.js deleted file mode 100644 index fc4ee6639a..0000000000 --- a/packages/backend/migration/1716345015347-NotRespondingSince.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class NotRespondingSince1716345015347 { - name = 'NotRespondingSince1716345015347' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" ADD "notRespondingSince" TIMESTAMP WITH TIME ZONE`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "notRespondingSince"`); - } -} diff --git a/packages/backend/migration/1716447138870-SuspensionStateInsteadOfIsSspended.js b/packages/backend/migration/1716447138870-SuspensionStateInsteadOfIsSspended.js deleted file mode 100644 index 4808a9a3db..0000000000 --- a/packages/backend/migration/1716447138870-SuspensionStateInsteadOfIsSspended.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SuspensionStateInsteadOfIsSspended1716345771510 { - name = 'SuspensionStateInsteadOfIsSspended1716345771510' - - async up(queryRunner) { - await queryRunner.query(`CREATE TYPE "public"."instance_suspensionstate_enum" AS ENUM('none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding')`); - - await queryRunner.query(`DROP INDEX "public"."IDX_34500da2e38ac393f7bb6b299c"`); - - await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "isSuspended" TO "suspensionState"`); - - await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`); - - await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE "public"."instance_suspensionstate_enum" USING ( - CASE "suspensionState" - WHEN TRUE THEN 'manuallySuspended'::instance_suspensionstate_enum - ELSE 'none'::instance_suspensionstate_enum - END - )`); - - await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT 'none'`); - - await queryRunner.query(`CREATE INDEX "IDX_3ede46f507c87ad698051d56a8" ON "instance" ("suspensionState") `); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_3ede46f507c87ad698051d56a8"`); - - await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`); - - await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE boolean USING ( - CASE "suspensionState" - WHEN 'none'::instance_suspensionstate_enum THEN FALSE - ELSE TRUE - END - )`); - - await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT false`); - - await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "suspensionState" TO "isSuspended"`); - - await queryRunner.query(`CREATE INDEX "IDX_34500da2e38ac393f7bb6b299c" ON "instance" ("isSuspended") `); - - await queryRunner.query(`DROP TYPE "public"."instance_suspensionstate_enum"`); - } -} diff --git a/packages/backend/migration/1716450883149-RemoveAntennaNotify.js b/packages/backend/migration/1716450883149-RemoveAntennaNotify.js deleted file mode 100644 index b5a2441855..0000000000 --- a/packages/backend/migration/1716450883149-RemoveAntennaNotify.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class RemoveAntennaNotify1716450883149 { - name = 'RemoveAntennaNotify1716450883149' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "notify"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" ADD "notify" boolean NOT NULL`); - } -} diff --git a/packages/backend/migration/1717117195275-inquiryUrl.js b/packages/backend/migration/1717117195275-inquiryUrl.js deleted file mode 100644 index 29ca31af14..0000000000 --- a/packages/backend/migration/1717117195275-inquiryUrl.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class InquiryUrl1717117195275 { - name = 'InquiryUrl1717117195275' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "inquiryUrl" character varying(1024)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "inquiryUrl"`); - } -} diff --git a/packages/backend/migration/1721666053703-fixDriveUrl.js b/packages/backend/migration/1721666053703-fixDriveUrl.js deleted file mode 100644 index d8512fb835..0000000000 --- a/packages/backend/migration/1721666053703-fixDriveUrl.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class FixDriveUrl1721666053703 { - name = 'FixDriveUrl1721666053703' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "url" TYPE character varying(1024), ALTER COLUMN "url" SET NOT NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`); - await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "uri" TYPE character varying(1024)`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`); - await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "src" TYPE character varying(1024)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "src" TYPE character varying(512)`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`); - await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "uri" TYPE character varying(512)`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`); - await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "url" TYPE character varying(512), ALTER COLUMN "url" SET NOT NULL`); - } -} diff --git a/packages/backend/migration/1723944246767-followedMessage.js b/packages/backend/migration/1723944246767-followedMessage.js deleted file mode 100644 index fc9ad1cb85..0000000000 --- a/packages/backend/migration/1723944246767-followedMessage.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class FollowedMessage1723944246767 { - name = 'FollowedMessage1723944246767'; - - async up(queryRunner) { - await queryRunner.query('ALTER TABLE "user_profile" ADD "followedMessage" character varying(256)'); - } - - async down(queryRunner) { - await queryRunner.query('ALTER TABLE "user_profile" DROP COLUMN "followedMessage"'); - } -} diff --git a/packages/backend/migration/1726804538569-reactions-buffering.js b/packages/backend/migration/1726804538569-reactions-buffering.js deleted file mode 100644 index bc19e9cc8a..0000000000 --- a/packages/backend/migration/1726804538569-reactions-buffering.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class ReactionsBuffering1726804538569 { - name = 'ReactionsBuffering1726804538569' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "enableReactionsBuffering" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableReactionsBuffering"`); - } -} diff --git a/packages/backend/migration/1727318020265-enableStatsForFederatedInstances.js b/packages/backend/migration/1727318020265-enableStatsForFederatedInstances.js deleted file mode 100644 index 4ff520172b..0000000000 --- a/packages/backend/migration/1727318020265-enableStatsForFederatedInstances.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class EnableStatsForFederatedInstances1727318020265 { - name = 'EnableStatsForFederatedInstances1727318020265' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "enableStatsForFederatedInstances" boolean NOT NULL DEFAULT true`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableStatsForFederatedInstances"`); - } -} diff --git a/packages/backend/migration/1727491883993-user-score.js b/packages/backend/migration/1727491883993-user-score.js deleted file mode 100644 index 7292d5363c..0000000000 --- a/packages/backend/migration/1727491883993-user-score.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class UserScore1727491883993 { - name = 'UserScore1727491883993' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" ADD "score" integer NOT NULL DEFAULT '0'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "score"`); - } -} diff --git a/packages/backend/migration/1727512908322-meta-federation.js b/packages/backend/migration/1727512908322-meta-federation.js deleted file mode 100644 index 52c24df4f7..0000000000 --- a/packages/backend/migration/1727512908322-meta-federation.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class MetaFederation1727512908322 { - name = 'MetaFederation1727512908322' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "federation" character varying(128) NOT NULL DEFAULT 'all'`); - await queryRunner.query(`ALTER TABLE "meta" ADD "federationHosts" character varying(1024) array NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federationHosts"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federation"`); - } -} diff --git a/packages/backend/migration/1728085812127-refine-abuse-user-report.js b/packages/backend/migration/1728085812127-refine-abuse-user-report.js deleted file mode 100644 index 57cbfdcf6d..0000000000 --- a/packages/backend/migration/1728085812127-refine-abuse-user-report.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class RefineAbuseUserReport1728085812127 { - name = 'RefineAbuseUserReport1728085812127' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "resolvedAs" character varying(128)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "resolvedAs"`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "moderationNote"`); - } -} diff --git a/packages/backend/migration/1728550878802-testcaptcha.js b/packages/backend/migration/1728550878802-testcaptcha.js deleted file mode 100644 index d8d987c0c1..0000000000 --- a/packages/backend/migration/1728550878802-testcaptcha.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Testcaptcha1728550878802 { - name = 'Testcaptcha1728550878802' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "enableTestcaptcha" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTestcaptcha"`); - } -} diff --git a/packages/backend/migration/1728634286056-prohibitedWordsForNameOfUser.js b/packages/backend/migration/1728634286056-prohibitedWordsForNameOfUser.js deleted file mode 100644 index 36e698d120..0000000000 --- a/packages/backend/migration/1728634286056-prohibitedWordsForNameOfUser.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class ProhibitedWordsForNameOfUser1728634286056 { - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "prohibitedWordsForNameOfUser" character varying(1024) array NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "prohibitedWordsForNameOfUser"`); - } -} diff --git a/packages/backend/migration/1729333924409-signinRequiredForShowContents.js b/packages/backend/migration/1729333924409-signinRequiredForShowContents.js deleted file mode 100644 index 5d4d1fcce2..0000000000 --- a/packages/backend/migration/1729333924409-signinRequiredForShowContents.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SigninRequiredForShowContents1729333924409 { - name = 'SigninRequiredForShowContents1729333924409' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" ADD "requireSigninToViewContents" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "requireSigninToViewContents"`); - } -} diff --git a/packages/backend/migration/1729486255072-makeNotesHiddenBefore.js b/packages/backend/migration/1729486255072-makeNotesHiddenBefore.js deleted file mode 100644 index 5fe4886b04..0000000000 --- a/packages/backend/migration/1729486255072-makeNotesHiddenBefore.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class MakeNotesHiddenBefore1729486255072 { - name = 'MakeNotesHiddenBefore1729486255072' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" ADD "makeNotesFollowersOnlyBefore" integer`); - await queryRunner.query(`ALTER TABLE "user" ADD "makeNotesHiddenBefore" integer`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "makeNotesHiddenBefore"`); - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "makeNotesFollowersOnlyBefore"`); - } -} diff --git a/packages/backend/migration/1736230492103-addAntennaHideNotesInSensitiveChannel.js b/packages/backend/migration/1736230492103-addAntennaHideNotesInSensitiveChannel.js deleted file mode 100644 index 74225de96a..0000000000 --- a/packages/backend/migration/1736230492103-addAntennaHideNotesInSensitiveChannel.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AddAntennaHideNotesInSensitiveChannel1736230492103 { - name = 'AddAntennaHideNotesInSensitiveChannel1736230492103' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" ADD "hideNotesInSensitiveChannel" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "hideNotesInSensitiveChannel"`); - } -} diff --git a/packages/backend/migration/1739006797620-GoogleAnalytics.js b/packages/backend/migration/1739006797620-GoogleAnalytics.js deleted file mode 100644 index 5871bf098a..0000000000 --- a/packages/backend/migration/1739006797620-GoogleAnalytics.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class GoogleAnalytics1739006797620 { - name = 'GoogleAnalytics1739006797620' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "googleAnalyticsMeasurementId" character varying(64)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "googleAnalyticsMeasurementId"`); - } -} diff --git a/packages/backend/migration/1740121393164-system-accounts.js b/packages/backend/migration/1740121393164-system-accounts.js deleted file mode 100644 index 9490cb2b64..0000000000 --- a/packages/backend/migration/1740121393164-system-accounts.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SystemAccounts1740121393164 { - name = 'SystemAccounts1740121393164' - - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "system_account" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "type" character varying(256) NOT NULL, CONSTRAINT "PK_edb56f4aaf9ddd50ee556da97ba" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_41a3c87a37aea616ee459369e1" ON "system_account" ("userId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c362033aee0ea51011386a5a7e" ON "system_account" ("type") `); - await queryRunner.query(`ALTER TABLE "system_account" ADD CONSTRAINT "FK_41a3c87a37aea616ee459369e12" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - - const instanceActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'instance.actor'`); - if (instanceActor.length > 0) { - await queryRunner.query(`INSERT INTO "system_account" ("id", "userId", "type") VALUES ('${instanceActor[0].id}', '${instanceActor[0].id}', 'actor')`); - } - - const relayActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'relay.actor'`); - if (relayActor.length > 0) { - await queryRunner.query(`INSERT INTO "system_account" ("id", "userId", "type") VALUES ('${relayActor[0].id}', '${relayActor[0].id}', 'relay')`); - } - - const meta = await queryRunner.query(`SELECT "proxyAccountId" FROM "meta" ORDER BY "id" DESC LIMIT 1`); - if (!meta && meta.length >= 1 && meta[0].proxyAccountId) { - await queryRunner.query(`INSERT INTO "system_account" ("id", "userId", "type") VALUES ('${meta[0].proxyAccountId}', '${meta[0].proxyAccountId}', 'proxy')`); - } - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "system_account" DROP CONSTRAINT "FK_41a3c87a37aea616ee459369e12"`); - await queryRunner.query(`DROP INDEX "public"."IDX_c362033aee0ea51011386a5a7e"`); - await queryRunner.query(`DROP INDEX "public"."IDX_41a3c87a37aea616ee459369e1"`); - await queryRunner.query(`DROP TABLE "system_account"`); - } -} diff --git a/packages/backend/migration/1740129169650-system-accounts-2.js b/packages/backend/migration/1740129169650-system-accounts-2.js deleted file mode 100644 index c03f0337ab..0000000000 --- a/packages/backend/migration/1740129169650-system-accounts-2.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SystemAccounts21740129169650 { - name = 'SystemAccounts21740129169650' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP CONSTRAINT "FK_ab1bc0c1e209daa77b8e8d212ad"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyAccountId"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "proxyAccountId" character varying(32)`); - const proxyAccountId = await queryRunner.query(`SELECT "userId" FROM "system_account" WHERE "type" = 'proxy' ORDER BY "id" DESC LIMIT 1`); - if (proxyAccountId && proxyAccountId.length >= 1) { - await queryRunner.query(`UPDATE "meta" SET "proxyAccountId" = '${proxyAccountId[0].userId}'`); - } - await queryRunner.query(`ALTER TABLE "meta" ADD CONSTRAINT "FK_ab1bc0c1e209daa77b8e8d212ad" FOREIGN KEY ("proxyAccountId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - } -} diff --git a/packages/backend/migration/1740133121105-system-accounts-3.js b/packages/backend/migration/1740133121105-system-accounts-3.js deleted file mode 100644 index a1f8c996f5..0000000000 --- a/packages/backend/migration/1740133121105-system-accounts-3.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SystemAccounts31740133121105 { - name = 'SystemAccounts31740133121105' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "rootUserId" character varying(32)`); - await queryRunner.query(`ALTER TABLE "meta" ADD CONSTRAINT "FK_c80e4079d632f95eac06a9d28cc" FOREIGN KEY ("rootUserId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - - const users = await queryRunner.query(`SELECT "id" FROM "user" WHERE "isRoot" = true LIMIT 1`); - if (users.length > 0) { - await queryRunner.query(`UPDATE "meta" SET "rootUserId" = $1`, [users[0].id]); - } - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP CONSTRAINT "FK_c80e4079d632f95eac06a9d28cc"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "rootUserId"`); - } -} diff --git a/packages/backend/migration/1740993126937-system-accounts-4.js b/packages/backend/migration/1740993126937-system-accounts-4.js deleted file mode 100644 index 83654aca80..0000000000 --- a/packages/backend/migration/1740993126937-system-accounts-4.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SystemAccounts41740993126937 { - name = 'SystemAccounts41740993126937' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isRoot"`); - } - - async down(queryRunner) { - // down 実行時は isRoot = true のユーザーが存在しなくなるため手動で対応する必要あり - await queryRunner.query(`ALTER TABLE "user" ADD "isRoot" boolean NOT NULL DEFAULT false`); - } -} diff --git a/packages/backend/migration/1741279404074-system-accounts-fixup.js b/packages/backend/migration/1741279404074-system-accounts-fixup.js deleted file mode 100644 index 31cab7f5ae..0000000000 --- a/packages/backend/migration/1741279404074-system-accounts-fixup.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SystemAccounts1741279404074 { - name = 'SystemAccounts1741279404074' - - async up(queryRunner) { - const instanceActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'instance.actor' AND "host" IS NULL AND "id" NOT IN (SELECT "userId" FROM "system_account" WHERE "type" = 'actor')`); - if (instanceActor.length > 0) { - console.warn('instance.actor was incorrect, updating...'); - await queryRunner.query(`UPDATE "system_account" SET "id" = '${instanceActor[0].id}', "userId" = '${instanceActor[0].id}' WHERE "type" = 'actor'`); - } - - const relayActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'relay.actor' AND "host" IS NULL AND "id" NOT IN (SELECT "userId" FROM "system_account" WHERE "type" = 'relay')`); - if (relayActor.length > 0) { - console.warn('relay.actor was incorrect, updating...'); - await queryRunner.query(`UPDATE "system_account" SET "id" = '${relayActor[0].id}', "userId" = '${relayActor[0].id}' WHERE "type" = 'relay'`); - } - } - - async down(queryRunner) { - // fixup migration, no down migration - } -} diff --git a/packages/backend/migration/1741424411879-user-featured-fixup.js b/packages/backend/migration/1741424411879-user-featured-fixup.js deleted file mode 100644 index 5643a328f0..0000000000 --- a/packages/backend/migration/1741424411879-user-featured-fixup.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class UserFeaturedFixup1741424411879 { - name = 'UserFeaturedFixup1741424411879' - - async up(queryRunner) { - await queryRunner.query(`CREATE OR REPLACE FUNCTION pg_temp.extract_ap_id(text) RETURNS text AS $$ - SELECT - CASE - WHEN $1 ~ '^https?://' THEN $1 - WHEN $1 LIKE '{%' THEN COALESCE(jsonb_extract_path_text($1::jsonb, 'id'), null) - ELSE null - END; - $$ LANGUAGE sql IMMUTABLE;`); - - // "host" is NOT NULL is not needed but just in case add it to prevent overwriting irreplaceable data - await queryRunner.query(`UPDATE "user" SET "featured" = pg_temp.extract_ap_id("featured") WHERE "host" IS NOT NULL`); - } - - async down(queryRunner) { - // fixup migration, no down migration - } -} diff --git a/packages/backend/migration/1742203321812-chat.js b/packages/backend/migration/1742203321812-chat.js deleted file mode 100644 index 3d8f7276b5..0000000000 --- a/packages/backend/migration/1742203321812-chat.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Chat1742203321812 { - name = 'Chat1742203321812' - - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "chat_room" ("id" character varying(32) NOT NULL, "name" character varying(256) NOT NULL, "ownerId" character varying(32) NOT NULL, CONSTRAINT "PK_8aa3a52cf74c96469f0ef9fbe3e" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_f0d8ad64243fa2ca2800da0dfd" ON "chat_room" ("ownerId") `); - await queryRunner.query(`CREATE TABLE "chat_message" ("id" character varying(32) NOT NULL, "fromUserId" character varying(32) NOT NULL, "toUserId" character varying(32), "toRoomId" character varying(32), "text" character varying(4096), "uri" character varying(512), "reads" character varying(32) array NOT NULL DEFAULT '{}', "fileId" character varying(32), "reactions" character varying(1024) array NOT NULL DEFAULT '{}', CONSTRAINT "PK_3cc0d85193aade457d3077dd06b" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_79a26e7a4d9afa5e4fc05f134e" ON "chat_message" ("fromUserId") `); - await queryRunner.query(`CREATE INDEX "IDX_25e097b51d7622c249452c6f75" ON "chat_message" ("toUserId") `); - await queryRunner.query(`CREATE INDEX "IDX_f006b8a76efd1abf9f221c175c" ON "chat_message" ("toRoomId") `); - await queryRunner.query(`CREATE TABLE "chat_room_membership" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "roomId" character varying(32) NOT NULL, CONSTRAINT "PK_2bd59c741e571b283c048beb69a" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_d99c5279460fb77ef58c596ce5" ON "chat_room_membership" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_c25143ebab714e930aeca1c0e8" ON "chat_room_membership" ("roomId") `); - await queryRunner.query(`ALTER TABLE "chat_room" ADD CONSTRAINT "FK_f0d8ad64243fa2ca2800da0dfd6" FOREIGN KEY ("ownerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_79a26e7a4d9afa5e4fc05f134ed" FOREIGN KEY ("fromUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_25e097b51d7622c249452c6f757" FOREIGN KEY ("toUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_f006b8a76efd1abf9f221c175ce" FOREIGN KEY ("toRoomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_fd0f9a4879430239715ad4f8e2a" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD CONSTRAINT "FK_d99c5279460fb77ef58c596ce51" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD CONSTRAINT "FK_c25143ebab714e930aeca1c0e8d" FOREIGN KEY ("roomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP CONSTRAINT "FK_c25143ebab714e930aeca1c0e8d"`); - await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP CONSTRAINT "FK_d99c5279460fb77ef58c596ce51"`); - await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_fd0f9a4879430239715ad4f8e2a"`); - await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_f006b8a76efd1abf9f221c175ce"`); - await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_25e097b51d7622c249452c6f757"`); - await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_79a26e7a4d9afa5e4fc05f134ed"`); - await queryRunner.query(`ALTER TABLE "chat_room" DROP CONSTRAINT "FK_f0d8ad64243fa2ca2800da0dfd6"`); - await queryRunner.query(`DROP INDEX "public"."IDX_c25143ebab714e930aeca1c0e8"`); - await queryRunner.query(`DROP INDEX "public"."IDX_d99c5279460fb77ef58c596ce5"`); - await queryRunner.query(`DROP TABLE "chat_room_membership"`); - await queryRunner.query(`DROP INDEX "public"."IDX_f006b8a76efd1abf9f221c175c"`); - await queryRunner.query(`DROP INDEX "public"."IDX_25e097b51d7622c249452c6f75"`); - await queryRunner.query(`DROP INDEX "public"."IDX_79a26e7a4d9afa5e4fc05f134e"`); - await queryRunner.query(`DROP TABLE "chat_message"`); - await queryRunner.query(`DROP INDEX "public"."IDX_f0d8ad64243fa2ca2800da0dfd"`); - await queryRunner.query(`DROP TABLE "chat_room"`); - } -} diff --git a/packages/backend/migration/1742608337548-chat-2.js b/packages/backend/migration/1742608337548-chat-2.js deleted file mode 100644 index 9f74a263d6..0000000000 --- a/packages/backend/migration/1742608337548-chat-2.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Chat21742608337548 { - name = 'Chat21742608337548' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" ADD "chatScope" character varying(128) NOT NULL DEFAULT 'mutual'`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_185b6b5afa707b5d36d1ce3144" ON "chat_room_membership" ("userId", "roomId") `); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_185b6b5afa707b5d36d1ce3144"`); - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "chatScope"`); - } -} diff --git a/packages/backend/migration/1742617546147-chat-3.js b/packages/backend/migration/1742617546147-chat-3.js deleted file mode 100644 index 116b9a738b..0000000000 --- a/packages/backend/migration/1742617546147-chat-3.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Chat31742617546147 { - name = 'Chat31742617546147' - - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "chat_approval" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "otherId" character varying(32) NOT NULL, CONSTRAINT "PK_fbbb95d60acf5c85388345b5f5d" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_530257863e1381a7f2f1d3282f" ON "chat_approval" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_b1d46037f23d170da5c05fdf75" ON "chat_approval" ("otherId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_12c4768a2f706fc267f2078903" ON "chat_approval" ("userId", "otherId") `); - await queryRunner.query(`ALTER TABLE "chat_approval" ADD CONSTRAINT "FK_530257863e1381a7f2f1d3282fe" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "chat_approval" ADD CONSTRAINT "FK_b1d46037f23d170da5c05fdf755" FOREIGN KEY ("otherId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "chat_approval" DROP CONSTRAINT "FK_b1d46037f23d170da5c05fdf755"`); - await queryRunner.query(`ALTER TABLE "chat_approval" DROP CONSTRAINT "FK_530257863e1381a7f2f1d3282fe"`); - await queryRunner.query(`DROP INDEX "public"."IDX_12c4768a2f706fc267f2078903"`); - await queryRunner.query(`DROP INDEX "public"."IDX_b1d46037f23d170da5c05fdf75"`); - await queryRunner.query(`DROP INDEX "public"."IDX_530257863e1381a7f2f1d3282f"`); - await queryRunner.query(`DROP TABLE "chat_approval"`); - } -} diff --git a/packages/backend/migration/1742707840715-chat-4.js b/packages/backend/migration/1742707840715-chat-4.js deleted file mode 100644 index 953a53d880..0000000000 --- a/packages/backend/migration/1742707840715-chat-4.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Chat41742707840715 { - name = 'Chat41742707840715' - - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "chat_room_invitation" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "roomId" character varying(32) NOT NULL, CONSTRAINT "PK_9d489521a312dd28225672de2dc" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_8552bb38e7ed038c5bdd398a38" ON "chat_room_invitation" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_5f265075b215fc390a57523b12" ON "chat_room_invitation" ("roomId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_044f2a7962b8ee5bbfaa02e8a3" ON "chat_room_invitation" ("userId", "roomId") `); - await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD CONSTRAINT "FK_8552bb38e7ed038c5bdd398a384" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD CONSTRAINT "FK_5f265075b215fc390a57523b12a" FOREIGN KEY ("roomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP CONSTRAINT "FK_5f265075b215fc390a57523b12a"`); - await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP CONSTRAINT "FK_8552bb38e7ed038c5bdd398a384"`); - await queryRunner.query(`DROP INDEX "public"."IDX_044f2a7962b8ee5bbfaa02e8a3"`); - await queryRunner.query(`DROP INDEX "public"."IDX_5f265075b215fc390a57523b12"`); - await queryRunner.query(`DROP INDEX "public"."IDX_8552bb38e7ed038c5bdd398a38"`); - await queryRunner.query(`DROP TABLE "chat_room_invitation"`); - } -} diff --git a/packages/backend/migration/1742721896936-chat-5.js b/packages/backend/migration/1742721896936-chat-5.js deleted file mode 100644 index 00db787cb7..0000000000 --- a/packages/backend/migration/1742721896936-chat-5.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Chat51742721896936 { - name = 'Chat51742721896936' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD "ignored" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP COLUMN "ignored"`); - } -} diff --git a/packages/backend/migration/1742795111958-chat-6.js b/packages/backend/migration/1742795111958-chat-6.js deleted file mode 100644 index 9a5dc3e32f..0000000000 --- a/packages/backend/migration/1742795111958-chat-6.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Chat61742795111958 { - name = 'Chat61742795111958' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "chat_room" ADD "description" character varying(2048) NOT NULL DEFAULT ''`); - await queryRunner.query(`ALTER TABLE "chat_room" ADD "isArchived" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD "isMuted" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP COLUMN "isMuted"`); - await queryRunner.query(`ALTER TABLE "chat_room" DROP COLUMN "isArchived"`); - await queryRunner.query(`ALTER TABLE "chat_room" DROP COLUMN "description"`); - } -} diff --git a/packages/backend/migration/1743403874305-DeliverSuspendedSoftware.js b/packages/backend/migration/1743403874305-DeliverSuspendedSoftware.js deleted file mode 100644 index 19983a72bd..0000000000 --- a/packages/backend/migration/1743403874305-DeliverSuspendedSoftware.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class DeliverSuspendedSoftware1743403874305 { - name = 'DeliverSuspendedSoftware1743403874305' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "deliverSuspendedSoftware" jsonb NOT NULL DEFAULT '[]'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deliverSuspendedSoftware"`); - } -} diff --git a/packages/backend/migration/1743558299182-RoleCopyOnMoveAccount.js b/packages/backend/migration/1743558299182-RoleCopyOnMoveAccount.js deleted file mode 100644 index ff4f7a051b..0000000000 --- a/packages/backend/migration/1743558299182-RoleCopyOnMoveAccount.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class RoleCopyOnMoveAccount1743558299182 { - name = 'RoleCopyOnMoveAccount1743558299182' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "role" ADD "preserveAssignmentOnMoveAccount" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "preserveAssignmentOnMoveAccount"`); - } -} diff --git a/packages/backend/migration/1744075766000-excludeNotesInSensitiveChannel.js b/packages/backend/migration/1744075766000-excludeNotesInSensitiveChannel.js deleted file mode 100644 index 1e8faafbc4..0000000000 --- a/packages/backend/migration/1744075766000-excludeNotesInSensitiveChannel.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class ExcludeNotesInSensitiveChannel1744075766000 { - name = 'ExcludeNotesInSensitiveChannel1744075766000' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" RENAME COLUMN "hideNotesInSensitiveChannel" TO "excludeNotesInSensitiveChannel"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" RENAME COLUMN "excludeNotesInSensitiveChannel" TO "hideNotesInSensitiveChannel"`); - } -} diff --git a/packages/backend/migration/1745378064470-composite-note-index.js b/packages/backend/migration/1745378064470-composite-note-index.js deleted file mode 100644 index 12108a6b3c..0000000000 --- a/packages/backend/migration/1745378064470-composite-note-index.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { isConcurrentIndexMigrationEnabled } from "./js/migration-config.js"; - -export class CompositeNoteIndex1745378064470 { - name = 'CompositeNoteIndex1745378064470'; - transaction = isConcurrentIndexMigrationEnabled() ? false : undefined; - - async up(queryRunner) { - const concurrently = isConcurrentIndexMigrationEnabled(); - - if (concurrently) { - const hasValidIndex = await queryRunner.query(`SELECT indisvalid FROM pg_index INNER JOIN pg_class ON pg_index.indexrelid = pg_class.oid WHERE pg_class.relname = 'IDX_724b311e6f883751f261ebe378'`); - if (hasValidIndex.length === 0 || hasValidIndex[0].indisvalid !== true) { - await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`); - await queryRunner.query(`CREATE INDEX CONCURRENTLY "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`); - } - } else { - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`); - } - - await queryRunner.query(`DROP INDEX IF EXISTS "IDX_5b87d9d19127bd5d92026017a7"`); - // Flush all cached Linear Scan Plans and redo statistics for composite index - // this is important for Postgres to learn that even in highly complex queries, using this index first can reduce the result set significantly - await queryRunner.query(`ANALYZE "user", "note"`); - } - - async down(queryRunner) { - const mayConcurrently = isConcurrentIndexMigrationEnabled() ? 'CONCURRENTLY' : ''; - await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`); - await queryRunner.query(`CREATE INDEX ${mayConcurrently} "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId")`); - } -} diff --git a/packages/backend/migration/1746330901644-visibleUserGeneratedContentsForNonLoggedInVisitors.js b/packages/backend/migration/1746330901644-visibleUserGeneratedContentsForNonLoggedInVisitors.js deleted file mode 100644 index 115698a420..0000000000 --- a/packages/backend/migration/1746330901644-visibleUserGeneratedContentsForNonLoggedInVisitors.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class VisibleUserGeneratedContentsForNonLoggedInVisitors1746330901644 { - name = 'VisibleUserGeneratedContentsForNonLoggedInVisitors1746330901644' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "ugcVisibilityForVisitor" character varying(128) NOT NULL DEFAULT 'local'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "ugcVisibilityForVisitor"`); - } -} diff --git a/packages/backend/migration/1746422049376-singleUserMode.js b/packages/backend/migration/1746422049376-singleUserMode.js deleted file mode 100644 index 9a79d46d5b..0000000000 --- a/packages/backend/migration/1746422049376-singleUserMode.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SingleUserMode1746422049376 { - name = 'SingleUserMode1746422049376' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "singleUserMode" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "singleUserMode"`); - } -} diff --git a/packages/backend/migration/1746949539915-migrateSomeConfigFileSettingsToMeta.js b/packages/backend/migration/1746949539915-migrateSomeConfigFileSettingsToMeta.js deleted file mode 100644 index 3243f43b91..0000000000 --- a/packages/backend/migration/1746949539915-migrateSomeConfigFileSettingsToMeta.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import {loadConfig} from "./js/migration-config.js"; - -export class MigrateSomeConfigFileSettingsToMeta1746949539915 { - name = 'MigrateSomeConfigFileSettingsToMeta1746949539915' - - async up(queryRunner) { - const config = loadConfig(); - // $1 cannot be used in ALTER TABLE queries - await queryRunner.query(`ALTER TABLE "meta" ADD "proxyRemoteFiles" boolean NOT NULL DEFAULT ${config.proxyRemoteFiles}`); - await queryRunner.query(`ALTER TABLE "meta" ADD "signToActivityPubGet" boolean NOT NULL DEFAULT ${config.signToActivityPubGet}`); - await queryRunner.query(`ALTER TABLE "meta" ADD "allowExternalApRedirect" boolean NOT NULL DEFAULT ${!config.disallowExternalApRedirect}`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "allowExternalApRedirect"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "signToActivityPubGet"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyRemoteFiles"`); - } -} diff --git a/packages/backend/migration/1748310233000-addUrlPreviewAllowRedirect.js b/packages/backend/migration/1748310233000-addUrlPreviewAllowRedirect.js deleted file mode 100644 index a895d0a941..0000000000 --- a/packages/backend/migration/1748310233000-addUrlPreviewAllowRedirect.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AddUrlPreviewAllowRedirect1748310233000 { - name = 'AddUrlPreviewAllowRedirect1748310233000' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "urlPreviewAllowRedirect" boolean NOT NULL DEFAULT true`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "urlPreviewAllowRedirect"`); - } -} diff --git a/packages/backend/migration/js/migration-config.js b/packages/backend/migration/js/migration-config.js deleted file mode 100644 index 853735661b..0000000000 --- a/packages/backend/migration/js/migration-config.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { path as configYamlPath } from '../../built/config.js'; -import * as yaml from 'js-yaml'; -import fs from "node:fs"; - -export function isConcurrentIndexMigrationEnabled() { - return process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1'; -} - -let loadedConfigCache = undefined; - -function loadConfigInternal() { - const config = yaml.load(fs.readFileSync(configYamlPath, 'utf-8')); - - return { - disallowExternalApRedirect: Boolean(config.disallowExternalApRedirect ?? false), - proxyRemoteFiles: Boolean(config.proxyRemoteFiles ?? false), - signToActivityPubGet: Boolean(config.signToActivityPubGet ?? true), - } -} - -export function loadConfig() { - if (loadedConfigCache === undefined) { - loadedConfigCache = loadConfigInternal(); - } - return loadedConfigCache; -} diff --git a/packages/backend/ormconfig.js b/packages/backend/ormconfig.js index f979c36ad7..229e5bf1fe 100644 --- a/packages/backend/ormconfig.js +++ b/packages/backend/ormconfig.js @@ -1,7 +1,6 @@ import { DataSource } from 'typeorm'; import { loadConfig } from './built/config.js'; import { entities } from './built/postgres.js'; -import { isConcurrentIndexMigrationEnabled } from "./migration/js/migration-config.js"; const config = loadConfig(); @@ -15,5 +14,4 @@ export default new DataSource({ extra: config.db.extra, entities: entities, migrations: ['migration/*.js'], - migrationsTransactionMode: isConcurrentIndexMigrationEnabled() ? 'each' : 'all', }); diff --git a/packages/backend/package.json b/packages/backend/package.json index 157b6ca6f3..30da661c6b 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -3,244 +3,224 @@ "main": "./index.js", "private": true, "type": "module", - "engines": { - "node": "^20.10.0 || ^22.0.0" - }, "scripts": { - "start": "node ./built/boot/entry.js", - "start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js", + "start": "node ./built/index.js", + "start:test": "NODE_ENV=test node ./built/index.js", "migrate": "pnpm typeorm migration:run -d ormconfig.js", - "revert": "pnpm typeorm migration:revert -d ormconfig.js", - "check:connect": "node ./scripts/check_connect.js", - "build": "swc src -d built -D --strip-leading-paths", - "build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths", - "watch:swc": "swc src -d built -D -w --strip-leading-paths", + "check:connect": "node ./check_connect.js", + "build": "swc src -d built -D", + "watch:swc": "swc src -d built -D -w", "build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json", - "watch": "node ./scripts/watch.mjs", - "restart": "pnpm build && pnpm start", - "dev": "node ./scripts/dev.mjs", - "typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit", - "eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"", + "watch": "node watch.mjs", + "typecheck": "tsc --noEmit", + "eslint": "eslint --quiet \"src/**/*.ts\"", "lint": "pnpm typecheck && pnpm eslint", - "jest": "cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.unit.cjs", - "jest:e2e": "cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.e2e.cjs", - "jest:fed": "node ./jest.js --forceExit --config jest.config.fed.cjs", - "jest-and-coverage": "cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.unit.cjs", - "jest-and-coverage:e2e": "cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.e2e.cjs", - "jest-clear": "cross-env NODE_ENV=test node ./jest.js --clearCache", + "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit", + "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit", + "jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache", "test": "pnpm jest", - "test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e", - "test:fed": "pnpm jest:fed", - "test-and-coverage": "pnpm jest-and-coverage", - "test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e", - "generate-api-json": "node ./scripts/generate_api_json.js" + "test-and-coverage": "pnpm jest-and-coverage" }, "optionalDependencies": { "@swc/core-android-arm64": "1.3.11", - "@swc/core-darwin-arm64": "1.11.29", - "@swc/core-darwin-x64": "1.11.29", - "@swc/core-freebsd-x64": "1.3.11", - "@swc/core-linux-arm-gnueabihf": "1.11.29", - "@swc/core-linux-arm64-gnu": "1.11.29", - "@swc/core-linux-arm64-musl": "1.11.29", - "@swc/core-linux-x64-gnu": "1.11.29", - "@swc/core-linux-x64-musl": "1.11.29", - "@swc/core-win32-arm64-msvc": "1.11.29", - "@swc/core-win32-ia32-msvc": "1.11.29", - "@swc/core-win32-x64-msvc": "1.11.29", - "@tensorflow/tfjs": "4.22.0", - "@tensorflow/tfjs-node": "4.22.0", - "bufferutil": "4.0.9", - "slacc-android-arm-eabi": "0.0.10", - "slacc-android-arm64": "0.0.10", - "slacc-darwin-arm64": "0.0.10", - "slacc-darwin-universal": "0.0.10", - "slacc-darwin-x64": "0.0.10", - "slacc-freebsd-x64": "0.0.10", - "slacc-linux-arm-gnueabihf": "0.0.10", - "slacc-linux-arm64-gnu": "0.0.10", - "slacc-linux-arm64-musl": "0.0.10", - "slacc-linux-x64-gnu": "0.0.10", - "slacc-linux-x64-musl": "0.0.10", - "slacc-win32-arm64-msvc": "0.0.10", - "slacc-win32-x64-msvc": "0.0.10", - "utf-8-validate": "6.0.5" + "@swc/core-darwin-arm64": "1.3.56", + "@swc/core-darwin-x64": "1.3.56", + "@swc/core-linux-arm-gnueabihf": "1.3.56", + "@swc/core-linux-arm64-gnu": "1.3.56", + "@swc/core-linux-arm64-musl": "1.3.56", + "@swc/core-linux-x64-gnu": "1.3.56", + "@swc/core-linux-x64-musl": "1.3.56", + "@swc/core-win32-arm64-msvc": "1.3.56", + "@swc/core-win32-ia32-msvc": "1.3.56", + "@swc/core-win32-x64-msvc": "1.3.56", + "@tensorflow/tfjs": "4.4.0", + "@tensorflow/tfjs-node": "4.4.0", + "bufferutil": "^4.0.7", + "slacc-android-arm-eabi": "0.0.9", + "slacc-android-arm64": "0.0.9", + "slacc-darwin-arm64": "0.0.9", + "slacc-darwin-universal": "0.0.9", + "slacc-darwin-x64": "0.0.9", + "slacc-freebsd-x64": "0.0.9", + "slacc-linux-arm-gnueabihf": "0.0.9", + "slacc-linux-arm64-gnu": "0.0.9", + "slacc-linux-arm64-musl": "0.0.9", + "slacc-linux-x64-gnu": "0.0.9", + "slacc-win32-arm64-msvc": "0.0.9", + "slacc-win32-x64-msvc": "0.0.9", + "utf-8-validate": "^6.0.3" }, "dependencies": { - "@aws-sdk/client-s3": "3.817.0", - "@aws-sdk/lib-storage": "3.817.0", - "@discordapp/twemoji": "15.1.0", - "@fastify/accepts": "5.0.2", - "@fastify/cookie": "11.0.2", - "@fastify/cors": "10.1.0", - "@fastify/express": "4.0.2", - "@fastify/http-proxy": "10.0.2", - "@fastify/multipart": "9.0.3", - "@fastify/static": "8.2.0", - "@fastify/view": "10.0.2", - "@misskey-dev/sharp-read-bmp": "1.2.0", - "@misskey-dev/summaly": "5.2.1", - "@napi-rs/canvas": "0.1.70", - "@nestjs/common": "11.1.2", - "@nestjs/core": "11.1.2", - "@nestjs/testing": "11.1.2", + "@aws-sdk/client-s3": "3.321.1", + "@aws-sdk/lib-storage": "3.321.1", + "@aws-sdk/node-http-handler": "3.321.1", + "@bull-board/api": "5.5.3", + "@bull-board/fastify": "5.5.3", + "@bull-board/ui": "5.5.3", + "@discordapp/twemoji": "14.1.2", + "@fastify/accepts": "4.2.0", + "@fastify/cookie": "8.3.0", + "@fastify/cors": "8.3.0", + "@fastify/express": "^2.3.0", + "@fastify/http-proxy": "9.2.1", + "@fastify/multipart": "7.7.0", + "@fastify/static": "6.10.2", + "@fastify/view": "7.4.1", + "@nestjs/common": "10.0.3", + "@nestjs/core": "10.0.3", + "@nestjs/testing": "10.0.3", "@peertube/http-signature": "1.7.0", - "@sentry/node": "8.55.0", - "@sentry/profiling-node": "8.55.0", - "@simplewebauthn/server": "12.0.0", - "@sinonjs/fake-timers": "11.3.1", - "@smithy/node-http-handler": "2.5.0", - "@swc/cli": "0.7.7", - "@swc/core": "1.11.29", - "@twemoji/parser": "15.1.1", - "@types/redis-info": "3.0.3", + "@sinonjs/fake-timers": "10.3.0", + "@swc/cli": "0.1.62", + "@swc/core": "1.3.66", "accepts": "1.3.8", - "ajv": "8.17.1", - "archiver": "7.0.1", - "async-mutex": "0.5.0", + "ajv": "8.12.0", + "archiver": "5.3.1", + "autwh": "0.1.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", - "body-parser": "1.20.3", - "bullmq": "5.53.0", + "body-parser": "^1.20.2", + "bullmq": "4.1.0", "cacheable-lookup": "7.0.0", - "cbor": "9.0.2", - "chalk": "5.4.1", - "chalk-template": "1.1.0", - "chokidar": "4.0.3", + "cbor": "9.0.0", + "chalk": "5.2.0", + "chalk-template": "0.4.0", + "chokidar": "3.5.3", "cli-highlight": "2.1.11", "color-convert": "2.0.1", "content-disposition": "0.5.4", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", - "fastify": "5.3.3", - "fastify-raw-body": "5.0.0", + "escape-regexp": "0.0.1", + "fastify": "4.18.0", "feed": "4.2.2", - "file-type": "19.6.0", - "fluent-ffmpeg": "2.1.3", - "form-data": "4.0.2", - "got": "14.4.7", - "happy-dom": "16.8.1", + "file-type": "18.5.0", + "fluent-ffmpeg": "2.1.2", + "form-data": "4.0.0", + "got": "13.0.0", + "happy-dom": "9.20.3", "hpagent": "1.2.0", - "htmlescape": "1.1.1", - "http-link-header": "1.1.3", - "ioredis": "5.6.1", - "ip-cidr": "4.0.2", - "ipaddr.js": "2.2.0", - "is-svg": "5.1.0", + "http-link-header": "^1.1.0", + "ioredis": "5.3.2", + "ip-cidr": "3.1.0", + "ipaddr.js": "2.1.0", + "is-svg": "4.3.2", "js-yaml": "4.1.0", - "jsdom": "26.1.0", + "jsdom": "22.1.0", "json5": "2.2.3", - "jsonld": "8.3.3", - "jsrsasign": "11.1.0", - "juice": "11.0.1", - "meilisearch": "0.50.0", - "mfm-js": "0.24.0", - "microformats-parser": "2.0.2", + "jsonld": "8.2.0", + "jsrsasign": "10.8.6", + "meilisearch": "0.33.0", + "mfm-js": "0.23.3", "mime-types": "2.1.35", "misskey-js": "workspace:*", - "misskey-reversi": "workspace:*", "ms": "3.0.0-canary.1", - "nanoid": "5.1.5", "nested-property": "4.0.0", - "node-fetch": "3.3.2", - "nodemailer": "6.10.1", - "nsfwjs": "4.2.0", - "oauth": "0.10.2", - "oauth2orize": "1.12.0", - "oauth2orize-pkce": "0.1.2", + "node-fetch": "3.3.1", + "nodemailer": "6.9.3", + "nsfwjs": "2.4.2", + "oauth": "0.10.0", + "oauth2orize": "^1.11.1", + "oauth2orize-pkce": "^0.1.2", "os-utils": "0.0.14", - "otpauth": "9.4.0", - "parse5": "7.3.0", - "pg": "8.16.0", - "pkce-challenge": "4.1.0", + "otpauth": "9.1.2", + "parse5": "7.1.2", + "pg": "8.11.0", + "pkce-challenge": "^4.0.1", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", - "pug": "3.0.3", - "qrcode": "1.5.4", + "pug": "3.0.2", + "punycode": "2.3.0", + "pureimage": "0.3.17", + "qrcode": "1.5.3", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.22.1", - "redis-info": "3.1.0", + "re2": "1.19.1", "redis-lock": "0.1.4", - "reflect-metadata": "0.2.2", + "reflect-metadata": "0.1.13", "rename": "1.0.4", "rss-parser": "3.13.0", - "rxjs": "7.8.2", - "sanitize-html": "2.17.0", - "secure-json-parse": "3.0.2", - "sharp": "0.33.5", - "semver": "7.7.2", - "slacc": "0.0.10", + "rxjs": "7.8.1", + "s-age": "1.1.2", + "sanitize-html": "2.11.0", + "semver": "7.5.3", + "sharp": "0.32.1", + "sharp-read-bmp": "github:misskey-dev/sharp-read-bmp", + "slacc": "0.0.9", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "systeminformation": "5.27.1", + "summaly": "github:misskey-dev/summaly", + "systeminformation": "5.18.4", "tinycolor2": "1.6.0", - "tmp": "0.2.3", - "tsc-alias": "1.8.16", + "tmp": "0.2.1", + "tsc-alias": "1.8.6", "tsconfig-paths": "4.2.0", - "typeorm": "0.3.24", - "typescript": "5.8.3", - "ulid": "2.4.0", + "twemoji-parser": "14.0.0", + "typeorm": "0.3.17", + "typescript": "5.1.3", + "ulid": "2.3.0", + "unzipper": "0.10.14", + "uuid": "9.0.0", "vary": "1.1.2", - "web-push": "3.6.7", - "ws": "8.18.2", + "web-push": "3.6.3", + "ws": "8.13.0", "xev": "3.0.2" }, "devDependencies": { - "@jest/globals": "29.7.0", - "@nestjs/platform-express": "10.4.18", - "@sentry/vue": "9.22.0", - "@simplewebauthn/types": "12.0.0", - "@swc/jest": "0.2.38", - "@types/accepts": "1.3.7", - "@types/archiver": "6.0.3", - "@types/bcryptjs": "2.4.6", - "@types/body-parser": "1.19.5", - "@types/color-convert": "2.0.4", - "@types/content-disposition": "0.5.8", - "@types/fluent-ffmpeg": "2.1.27", - "@types/htmlescape": "1.1.3", - "@types/http-link-header": "1.0.7", - "@types/jest": "29.5.14", - "@types/js-yaml": "4.0.9", - "@types/jsdom": "21.1.7", - "@types/jsonld": "1.5.15", - "@types/jsrsasign": "10.5.15", - "@types/mime-types": "2.1.4", - "@types/ms": "0.7.34", - "@types/node": "22.15.21", - "@types/nodemailer": "6.4.17", - "@types/oauth": "0.9.6", - "@types/oauth2orize": "1.11.5", - "@types/oauth2orize-pkce": "0.1.2", - "@types/pg": "8.15.2", - "@types/pug": "2.0.10", - "@types/qrcode": "1.5.5", - "@types/random-seed": "0.3.5", - "@types/ratelimiter": "3.4.6", - "@types/rename": "1.0.7", - "@types/sanitize-html": "2.16.0", - "@types/semver": "7.7.0", - "@types/simple-oauth2": "5.0.7", - "@types/sinonjs__fake-timers": "8.1.5", - "@types/supertest": "6.0.3", - "@types/tinycolor2": "1.4.6", - "@types/tmp": "0.2.6", - "@types/vary": "1.1.3", - "@types/web-push": "3.6.4", - "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "aws-sdk-client-mock": "4.1.0", + "@jest/globals": "29.5.0", + "@swc/jest": "0.2.26", + "@types/accepts": "1.3.5", + "@types/archiver": "5.3.2", + "@types/bcryptjs": "2.4.2", + "@types/body-parser": "^1.19.2", + "@types/cbor": "6.0.0", + "@types/color-convert": "2.0.0", + "@types/content-disposition": "0.5.5", + "@types/escape-regexp": "0.0.1", + "@types/fluent-ffmpeg": "2.1.21", + "@types/http-link-header": "^1.0.3", + "@types/jest": "29.5.2", + "@types/js-yaml": "4.0.5", + "@types/jsdom": "21.1.1", + "@types/jsonld": "1.5.9", + "@types/jsrsasign": "10.5.8", + "@types/mime-types": "2.1.1", + "@types/ms": "^0.7.31", + "@types/node": "20.3.1", + "@types/node-fetch": "3.0.3", + "@types/nodemailer": "6.4.8", + "@types/oauth": "0.9.1", + "@types/oauth2orize": "^1.11.0", + "@types/pg": "8.10.2", + "@types/pug": "2.0.6", + "@types/punycode": "2.1.0", + "@types/qrcode": "1.5.0", + "@types/random-seed": "0.3.3", + "@types/ratelimiter": "3.4.4", + "@types/redis": "4.0.11", + "@types/rename": "1.0.4", + "@types/sanitize-html": "2.9.0", + "@types/semver": "7.5.0", + "@types/sharp": "0.32.0", + "@types/simple-oauth2": "^5.0.4", + "@types/sinonjs__fake-timers": "8.1.2", + "@types/tinycolor2": "1.4.3", + "@types/tmp": "0.2.3", + "@types/unzipper": "0.10.6", + "@types/uuid": "9.0.2", + "@types/vary": "1.1.0", + "@types/web-push": "3.3.2", + "@types/websocket": "1.0.5", + "@types/ws": "8.5.5", + "@typescript-eslint/eslint-plugin": "5.60.0", + "@typescript-eslint/parser": "5.60.0", + "aws-sdk-client-mock": "2.1.1", "cross-env": "7.0.3", - "eslint-plugin-import": "2.31.0", - "execa": "8.0.1", - "fkill": "9.0.0", - "jest": "29.7.0", - "jest-mock": "29.7.0", - "nodemon": "3.1.10", - "pid-port": "1.0.2", - "simple-oauth2": "5.1.0", - "supertest": "7.1.1" + "eslint": "8.43.0", + "eslint-plugin-import": "2.27.5", + "execa": "6.1.0", + "jest": "29.5.0", + "jest-mock": "29.5.0", + "simple-oauth2": "^5.0.0" } } diff --git a/packages/backend/scripts/check_connect.js b/packages/backend/scripts/check_connect.js deleted file mode 100644 index 96c4549ccb..0000000000 --- a/packages/backend/scripts/check_connect.js +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import Redis from 'ioredis'; -import { loadConfig } from '../built/config.js'; -import { createPostgresDataSource } from '../built/postgres.js'; - -const config = loadConfig(); - -async function connectToPostgres() { - const source = createPostgresDataSource(config); - await source.initialize(); - await source.destroy(); -} - -async function connectToRedis(redisOptions) { - return await new Promise(async (resolve, reject) => { - const redis = new Redis({ - ...redisOptions, - lazyConnect: true, - reconnectOnError: false, - showFriendlyErrorStack: true, - }); - redis.on('error', e => reject(e)); - - try { - await redis.connect(); - resolve(); - - } catch (e) { - reject(e); - - } finally { - redis.disconnect(false); - } - }); -} - -// If not all of these are defined, the default one gets reused. -// so we use a Set to only try connecting once to each **uniq** redis. -const promises = Array - .from(new Set([ - config.redis, - config.redisForPubsub, - config.redisForJobQueue, - config.redisForTimelines, - config.redisForReactions, - ])) - .map(connectToRedis) - .concat([ - connectToPostgres() - ]); - -await Promise.all(promises); diff --git a/packages/backend/scripts/dev.mjs b/packages/backend/scripts/dev.mjs deleted file mode 100644 index a3e0558abd..0000000000 --- a/packages/backend/scripts/dev.mjs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { execa, execaNode } from 'execa'; - -/** @type {import('execa').ExecaChildProcess | undefined} */ -let backendProcess; - -async function execBuildAssets() { - await execa('pnpm', ['run', 'build-assets'], { - cwd: '../../', - stdout: process.stdout, - stderr: process.stderr, - }) -} - -function execStart() { - // pnpm run start を呼び出したいが、windowsだとプロセスグループ単位でのkillが出来ずゾンビプロセス化するので - // 上記と同等の動きをするコマンドで子・孫プロセスを作らないようにしたい - backendProcess = execaNode('./built/boot/entry.js', [], { - stdout: process.stdout, - stderr: process.stderr, - env: { - 'NODE_ENV': 'development', - }, - }); -} - -async function killProc() { - if (backendProcess) { - backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す - backendProcess.kill(); - await new Promise(resolve => backendProcess.on('exit', resolve)); - backendProcess = undefined; - } -} - -(async () => { - execaNode( - './node_modules/nodemon/bin/nodemon.js', - [ - '-w', 'src', - '-e', 'ts,js,mjs,cjs,json', - '--exec', 'pnpm', 'run', 'build', - ], - { - stdio: [process.stdin, process.stdout, process.stderr, 'ipc'], - serialization: "json", - }) - .on('message', async (message) => { - if (message.type === 'exit') { - // かならずbuild->build-assetsの順番で呼び出したいので、 - // 少々トリッキーだがnodemonからのexitイベントを利用してbuild-assets->startを行う。 - // pnpm restartをbuildが終わる前にbuild-assetsが動いてしまうので、バラバラに呼び出す必要がある - - await killProc(); - await execBuildAssets(); - execStart(); - } - }) -})(); diff --git a/packages/backend/scripts/generate_api_json.js b/packages/backend/scripts/generate_api_json.js deleted file mode 100644 index 798e243004..0000000000 --- a/packages/backend/scripts/generate_api_json.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { execa } from 'execa'; -import { writeFileSync, existsSync } from "node:fs"; - -async function main() { - if (!process.argv.includes('--no-build')) { - await execa('pnpm', ['run', 'build'], { - stdout: process.stdout, - stderr: process.stderr, - }); - } - - if (!existsSync('./built')) { - throw new Error('`built` directory does not exist.'); - } - - /** @type {import('../src/config.js')} */ - const { loadConfig } = await import('../built/config.js'); - - /** @type {import('../src/server/api/openapi/gen-spec.js')} */ - const { genOpenapiSpec } = await import('../built/server/api/openapi/gen-spec.js'); - - const config = loadConfig(); - const spec = genOpenapiSpec(config, true); - - writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8'); -} - -main().catch(e => { - console.error(e); - process.exit(1); -}); diff --git a/packages/backend/src/@types/hcaptcha.d.ts b/packages/backend/src/@types/hcaptcha.d.ts index e11dda4662..afed587560 100644 --- a/packages/backend/src/@types/hcaptcha.d.ts +++ b/packages/backend/src/@types/hcaptcha.d.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - declare module 'hcaptcha' { interface IVerifyResponse { success: boolean; diff --git a/packages/backend/src/@types/http-signature.d.ts b/packages/backend/src/@types/http-signature.d.ts index 75b62e55f0..f2f9bfcc31 100644 --- a/packages/backend/src/@types/http-signature.d.ts +++ b/packages/backend/src/@types/http-signature.d.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - declare module '@peertube/http-signature' { import type { IncomingMessage, ClientRequest } from 'node:http'; diff --git a/packages/backend/src/@types/oauth2orize-pkce.d.ts b/packages/backend/src/@types/oauth2orize-pkce.d.ts new file mode 100644 index 0000000000..aa45ad2c04 --- /dev/null +++ b/packages/backend/src/@types/oauth2orize-pkce.d.ts @@ -0,0 +1,5 @@ +declare module 'oauth2orize-pkce' { + export default { + extensions(): any; + }; +} diff --git a/packages/backend/src/@types/os-utils.d.ts b/packages/backend/src/@types/os-utils.d.ts index 8943edddd1..390df17d39 100644 --- a/packages/backend/src/@types/os-utils.d.ts +++ b/packages/backend/src/@types/os-utils.d.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - declare module 'os-utils' { type FreeCommandCallback = (usedmem: number) => void; diff --git a/packages/backend/src/@types/package.json.d.ts b/packages/backend/src/@types/package.json.d.ts index 52a2b356db..abe5fae687 100644 --- a/packages/backend/src/@types/package.json.d.ts +++ b/packages/backend/src/@types/package.json.d.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - declare module '*/package.json' { interface IRepository { type: string; diff --git a/packages/backend/src/@types/probe-image-size.d.ts b/packages/backend/src/@types/probe-image-size.d.ts index 538836475c..416e819acb 100644 --- a/packages/backend/src/@types/probe-image-size.d.ts +++ b/packages/backend/src/@types/probe-image-size.d.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - declare module 'probe-image-size' { import type { ReadStream } from 'node:fs'; diff --git a/packages/backend/src/@types/redis-lock.d.ts b/packages/backend/src/@types/redis-lock.d.ts index b037cde5ee..9242656a98 100644 --- a/packages/backend/src/@types/redis-lock.d.ts +++ b/packages/backend/src/@types/redis-lock.d.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - declare module 'redis-lock' { import type Redis from 'ioredis'; diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 435bd8dd45..406e3192bb 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -1,19 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - +import { setTimeout } from 'node:timers/promises'; import { Global, Inject, Module } from '@nestjs/common'; import * as Redis from 'ioredis'; import { DataSource } from 'typeorm'; import { MeiliSearch } from 'meilisearch'; -import { MiMeta } from '@/models/Meta.js'; import { DI } from './di-symbols.js'; import { Config, loadConfig } from './config.js'; import { createPostgresDataSource } from './postgres.js'; import { RepositoryModule } from './models/RepositoryModule.js'; -import { allSettled } from './misc/promise-tracker.js'; -import { GlobalEvents } from './core/GlobalEventService.js'; import type { Provider, OnApplicationShutdown } from '@nestjs/common'; const $config: Provider = { @@ -24,13 +17,8 @@ const $config: Provider = { const $db: Provider = { provide: DI.db, useFactory: async (config) => { - try { - const db = createPostgresDataSource(config); - return await db.initialize(); - } catch (e) { - console.log(e); - throw e; - } + const db = createPostgresDataSource(config); + return await db.initialize(); }, inject: [DI.config], }; @@ -38,13 +26,9 @@ const $db: Provider = { const $meilisearch: Provider = { provide: DI.meilisearch, useFactory: (config: Config) => { - if (config.fulltextSearch?.provider === 'meilisearch') { - if (!config.meilisearch) { - throw new Error('MeiliSearch is enabled but no configuration is provided'); - } - + if (config.meilisearch) { return new MeiliSearch({ - host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`, + host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`, apiKey: config.meilisearch.apiKey, }); } else { @@ -57,7 +41,14 @@ const $meilisearch: Provider = { const $redis: Provider = { provide: DI.redis, useFactory: (config: Config) => { - return new Redis.Redis(config.redis); + return new Redis.Redis({ + port: config.redis.port, + host: config.redis.host, + family: config.redis.family == null ? 0 : config.redis.family, + password: config.redis.pass, + keyPrefix: `${config.redis.prefix}:`, + db: config.redis.db ?? 0, + }); }, inject: [DI.config], }; @@ -65,7 +56,14 @@ const $redis: Provider = { const $redisForPub: Provider = { provide: DI.redisForPub, useFactory: (config: Config) => { - const redis = new Redis.Redis(config.redisForPubsub); + const redis = new Redis.Redis({ + port: config.redisForPubsub.port, + host: config.redisForPubsub.host, + family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family, + password: config.redisForPubsub.pass, + keyPrefix: `${config.redisForPubsub.prefix}:`, + db: config.redisForPubsub.db ?? 0, + }); return redis; }, inject: [DI.config], @@ -74,91 +72,25 @@ const $redisForPub: Provider = { const $redisForSub: Provider = { provide: DI.redisForSub, useFactory: (config: Config) => { - const redis = new Redis.Redis(config.redisForPubsub); + const redis = new Redis.Redis({ + port: config.redisForPubsub.port, + host: config.redisForPubsub.host, + family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family, + password: config.redisForPubsub.pass, + keyPrefix: `${config.redisForPubsub.prefix}:`, + db: config.redisForPubsub.db ?? 0, + }); redis.subscribe(config.host); return redis; }, inject: [DI.config], }; -const $redisForTimelines: Provider = { - provide: DI.redisForTimelines, - useFactory: (config: Config) => { - return new Redis.Redis(config.redisForTimelines); - }, - inject: [DI.config], -}; - -const $redisForReactions: Provider = { - provide: DI.redisForReactions, - useFactory: (config: Config) => { - return new Redis.Redis(config.redisForReactions); - }, - inject: [DI.config], -}; - -const $meta: Provider = { - provide: DI.meta, - useFactory: async (db: DataSource, redisForSub: Redis.Redis) => { - const meta = await db.transaction(async transactionalEntityManager => { - // 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する - const metas = await transactionalEntityManager.find(MiMeta, { - order: { - id: 'DESC', - }, - }); - - const meta = metas[0]; - - if (meta) { - return meta; - } else { - // metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う - const saved = await transactionalEntityManager - .upsert( - MiMeta, - { - id: 'x', - }, - ['id'], - ) - .then((x) => transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0])); - - return saved; - } - }); - - async function onMessage(_: string, data: string): Promise { - const obj = JSON.parse(data); - - if (obj.channel === 'internal') { - const { type, body } = obj.message as GlobalEvents['internal']['payload']; - switch (type) { - case 'metaUpdated': { - for (const key in body.after) { - (meta as any)[key] = (body.after as any)[key]; - } - meta.rootUser = null; // joinなカラムは通常取ってこないので - break; - } - default: - break; - } - } - } - - redisForSub.on('message', onMessage); - - return meta; - }, - inject: [DI.db, DI.redisForSub], -}; - @Global() @Module({ imports: [RepositoryModule], - providers: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions], - exports: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, RepositoryModule], + providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub], + exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, RepositoryModule], }) export class GlobalModule implements OnApplicationShutdown { constructor( @@ -166,21 +98,22 @@ export class GlobalModule implements OnApplicationShutdown { @Inject(DI.redis) private redisClient: Redis.Redis, @Inject(DI.redisForPub) private redisForPub: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis, - @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, - @Inject(DI.redisForReactions) private redisForReactions: Redis.Redis, - ) { } + ) {} public async dispose(): Promise { - // Wait for all potential DB queries - await allSettled(); - // And then disconnect from DB + if (process.env.NODE_ENV === 'test') { + // XXX: + // Shutting down the existing connections causes errors on Jest as + // Misskey has asynchronous postgres/redis connections that are not + // awaited. + // Let's wait for some random time for them to finish. + await setTimeout(5000); + } await Promise.all([ this.db.destroy(), this.redisClient.disconnect(), this.redisForPub.disconnect(), this.redisForSub.disconnect(), - this.redisForTimelines.disconnect(), - this.redisForReactions.disconnect(), ]); } diff --git a/packages/backend/src/MainModule.ts b/packages/backend/src/MainModule.ts index f86a0be93c..fc568e883e 100644 --- a/packages/backend/src/MainModule.ts +++ b/packages/backend/src/MainModule.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Module } from '@nestjs/common'; import { ServerModule } from '@/server/ServerModule.js'; import { GlobalModule } from '@/GlobalModule.js'; diff --git a/packages/backend/src/NestLogger.ts b/packages/backend/src/NestLogger.ts index d0be19664f..448098b831 100644 --- a/packages/backend/src/NestLogger.ts +++ b/packages/backend/src/NestLogger.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { LoggerService } from '@nestjs/common'; import Logger from '@/logger.js'; const logger = new Logger('core', 'cyan'); -const nestLogger = logger.createSubLogger('nest', 'green'); +const nestLogger = logger.createSubLogger('nest', 'green', false); export class NestLogger implements LoggerService { /** diff --git a/packages/backend/src/boot/common.ts b/packages/backend/src/boot/common.ts index 268c07582d..3995545d7f 100644 --- a/packages/backend/src/boot/common.ts +++ b/packages/backend/src/boot/common.ts @@ -1,13 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { NestFactory } from '@nestjs/core'; import { ChartManagementService } from '@/core/chart/ChartManagementService.js'; import { QueueProcessorService } from '@/queue/QueueProcessorService.js'; import { NestLogger } from '@/NestLogger.js'; import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js'; +import { JanitorService } from '@/daemons/JanitorService.js'; import { QueueStatsService } from '@/daemons/QueueStatsService.js'; import { ServerStatsService } from '@/daemons/ServerStatsService.js'; import { ServerService } from '@/server/ServerService.js'; @@ -17,12 +13,14 @@ export async function server() { const app = await NestFactory.createApplicationContext(MainModule, { logger: new NestLogger(), }); + app.enableShutdownHooks(); const serverService = app.get(ServerService); await serverService.launch(); if (process.env.NODE_ENV !== 'test') { app.get(ChartManagementService).start(); + app.get(JanitorService).start(); app.get(QueueStatsService).start(); app.get(ServerStatsService).start(); } @@ -34,6 +32,7 @@ export async function jobQueue() { const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, { logger: new NestLogger(), }); + jobQueue.enableShutdownHooks(); jobQueue.get(QueueProcessorService).start(); jobQueue.get(ChartManagementService).start(); diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/index.ts similarity index 71% rename from packages/backend/src/boot/entry.ts rename to packages/backend/src/boot/index.ts index da585ad68d..f4daf30690 100644 --- a/packages/backend/src/boot/entry.ts +++ b/packages/backend/src/boot/index.ts @@ -1,7 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ /** * Misskey Entry Point! @@ -15,7 +11,6 @@ import Logger from '@/logger.js'; import { envOption } from '../env.js'; import { masterMain } from './master.js'; import { workerMain } from './worker.js'; -import { readyRef } from './ready.js'; import 'reflect-metadata'; @@ -25,7 +20,7 @@ Error.stackTraceLimit = Infinity; EventEmitter.defaultMaxListeners = 128; const logger = new Logger('core', 'cyan'); -const clusterLogger = logger.createSubLogger('cluster', 'orange'); +const clusterLogger = logger.createSubLogger('cluster', 'orange', false); const ev = new Xev(); //#region Events @@ -68,25 +63,17 @@ process.on('exit', code => { //#endregion -if (!envOption.disableClustering) { - if (cluster.isPrimary) { - logger.info(`Start main process... pid: ${process.pid}`); - await masterMain(); - ev.mount(); - } else if (cluster.isWorker) { - logger.info(`Start worker process... pid: ${process.pid}`); - await workerMain(); - } else { - throw new Error('Unknown process type'); - } -} else { - // 非clusterの場合はMasterのみが起動するため、Workerの処理は行わない(cluster.isWorker === trueの状態でこのブロックに来ることはない) - logger.info(`Start main process... pid: ${process.pid}`); +if (cluster.isPrimary || envOption.disableClustering) { await masterMain(); - ev.mount(); + + if (cluster.isPrimary) { + ev.mount(); + } } -readyRef.value = true; +if (cluster.isWorker || envOption.disableClustering) { + await workerMain(); +} // ユニットテスト時にMisskeyが子プロセスで起動された時のため // それ以外のときは process.send は使えないので弾く diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index d1fb3858db..f5d936fadf 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; @@ -10,8 +5,7 @@ import * as os from 'node:os'; import cluster from 'node:cluster'; import chalk from 'chalk'; import chalkTemplate from 'chalk-template'; -import * as Sentry from '@sentry/node'; -import { nodeProfilingIntegration } from '@sentry/profiling-node'; +import semver from 'semver'; import Logger from '@/logger.js'; import { loadConfig } from '@/config.js'; import type { Config } from '@/config.js'; @@ -25,7 +19,7 @@ const _dirname = dirname(_filename); const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); const logger = new Logger('core', 'cyan'); -const bootLogger = logger.createSubLogger('boot', 'magenta'); +const bootLogger = logger.createSubLogger('boot', 'magenta', false); const themeColor = chalk.hex('#86b300'); @@ -37,7 +31,7 @@ function greet() { console.log(themeColor(' | |_|___ ___| |_ ___ _ _ ')); console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |')); console.log(themeColor(' |_|_|_|_|___|___|_,_|___|_ |')); - console.log(' ' + chalk.gray(v) + themeColor(' |___|\n'.substring(v.length))); + console.log(' ' + chalk.gray(v) + themeColor(' |___|\n'.substr(v.length))); //#endregion console.log(' Misskey is an open-source decentralized microblogging platform.'); @@ -65,69 +59,26 @@ export async function masterMain() { showNodejsVersion(); config = loadConfigBoot(); //await connectDb(); - if (config.pidFile) fs.writeFileSync(config.pidFile, process.pid.toString()); } catch (e) { bootLogger.error('Fatal error occurred during initialization', null, true); process.exit(1); } + if (envOption.onlyServer) { + await server(); + } else if (envOption.onlyQueue) { + await jobQueue(); + } else { + await server(); + } + bootLogger.succ('Misskey initialized'); - if (config.sentryForBackend) { - Sentry.init({ - integrations: [ - ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []), - ], - - // Performance Monitoring - tracesSampleRate: 1.0, // Capture 100% of the transactions - - // Set sampling rate for profiling - this is relative to tracesSampleRate - profilesSampleRate: 1.0, - - maxBreadcrumbs: 0, - - ...config.sentryForBackend.options, - }); - } - - bootLogger.info( - `mode: [disableClustering: ${envOption.disableClustering}, onlyServer: ${envOption.onlyServer}, onlyQueue: ${envOption.onlyQueue}]`, - ); - if (!envOption.disableClustering) { - // clusterモジュール有効時 - - if (envOption.onlyServer) { - // onlyServer かつ enableCluster な場合、メインプロセスはforkのみに制限する(listenしない)。 - // ワーカープロセス側でlistenすると、メインプロセスでポートへの着信を受け入れてワーカープロセスへの分配を行う動作をする。 - // そのため、メインプロセスでも直接listenするとポートの競合が発生して起動に失敗してしまう。 - // see: https://nodejs.org/api/cluster.html#cluster - } else if (envOption.onlyQueue) { - await jobQueue(); - } else { - await server(); - } - await spawnWorkers(config.clusterLimit); - } else { - // clusterモジュール無効時 - - if (envOption.onlyServer) { - await server(); - } else if (envOption.onlyQueue) { - await jobQueue(); - } else { - await server(); - await jobQueue(); - } } - if (envOption.onlyQueue) { - bootLogger.succ('Queue started', null, true); - } else { - bootLogger.succ(config.socket ? `Now listening on socket ${config.socket} on ${config.url}` : `Now listening on port ${config.port} on ${config.url}`, null, true); - } + bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); } function showEnvironment(): void { @@ -145,6 +96,12 @@ function showNodejsVersion(): void { const nodejsLogger = bootLogger.createSubLogger('nodejs'); nodejsLogger.info(`Version ${process.version} detected.`); + + const minVersion = fs.readFileSync(`${_dirname}/../../../../.node-version`, 'utf-8').trim(); + if (semver.lt(process.version, minVersion)) { + nodejsLogger.error(`At least Node.js ${minVersion} required!`); + process.exit(1); + } } function loadConfigBoot(): Config { diff --git a/packages/backend/src/boot/ready.ts b/packages/backend/src/boot/ready.ts deleted file mode 100644 index 591ae5cb58..0000000000 --- a/packages/backend/src/boot/ready.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const readyRef = { value: false }; diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts index 5d4a15b29f..ab75aaa572 100644 --- a/packages/backend/src/boot/worker.ts +++ b/packages/backend/src/boot/worker.ts @@ -1,39 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import cluster from 'node:cluster'; -import * as Sentry from '@sentry/node'; -import { nodeProfilingIntegration } from '@sentry/profiling-node'; import { envOption } from '@/env.js'; -import { loadConfig } from '@/config.js'; import { jobQueue, server } from './common.js'; /** * Init worker process */ export async function workerMain() { - const config = loadConfig(); - - if (config.sentryForBackend) { - Sentry.init({ - integrations: [ - ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []), - ], - - // Performance Monitoring - tracesSampleRate: 1.0, // Capture 100% of the transactions - - // Set sampling rate for profiling - this is relative to tracesSampleRate - profilesSampleRate: 1.0, - - maxBreadcrumbs: 0, - - ...config.sentryForBackend.options, - }); - } - if (envOption.onlyServer) { await server(); } else if (envOption.onlyQueue) { diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 7cdf8df0c0..9d1945e4d4 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -1,40 +1,27 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only +/** + * Config loader */ import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import * as yaml from 'js-yaml'; -import type * as Sentry from '@sentry/node'; -import type * as SentryVue from '@sentry/vue'; -import type { RedisOptions } from 'ioredis'; - -type RedisOptionsSource = Partial & { - host: string; - port: number; - family?: number; - pass: string; - db?: number; - prefix?: string; -}; /** - * 設定ファイルの型 + * ユーザーが設定する必要のある情報 */ -type Source = { - url?: string; - port?: number; - socket?: string; - chmodSocket?: string; +export type Source = { + repository_url?: string; + feedback_url?: string; + url: string; + port: number; disableHsts?: boolean; db: { host: string; port: number; - db?: string; - user?: string; - pass?: string; + db: string; + user: string; + pass: string; disableCache?: boolean; extra?: { [x: string]: string }; }; @@ -46,13 +33,29 @@ type Source = { user: string; pass: string; }[]; - redis: RedisOptionsSource; - redisForPubsub?: RedisOptionsSource; - redisForJobQueue?: RedisOptionsSource; - redisForTimelines?: RedisOptionsSource; - redisForReactions?: RedisOptionsSource; - fulltextSearch?: { - provider?: FulltextSearchProvider; + redis: { + host: string; + port: number; + family?: number; + pass: string; + db?: number; + prefix?: string; + }; + redisForPubsub?: { + host: string; + port: number; + family?: number; + pass: string; + db?: number; + prefix?: string; + }; + redisForJobQueue?: { + host: string; + port: number; + family?: number; + pass: string; + db?: number; + prefix?: string; }; meilisearch?: { host: string; @@ -60,19 +63,7 @@ type Source = { apiKey: string; ssl?: boolean; index: string; - scope?: 'local' | 'global' | string[]; }; - sentryForBackend?: { options: Partial; enableNodeProfiling: boolean; }; - sentryForFrontend?: { - options: Partial & { dsn: string }; - vueIntegration?: SentryVue.VueIntegrationOptions | null; - browserTracingIntegration?: Parameters[0] | null; - replayIntegration?: Parameters[0] | null; - }; - - publishTarballInsteadOfProvideRepositoryUrl?: boolean; - - setupPassword?: string; proxy?: string; proxySmtp?: string; @@ -82,102 +73,35 @@ type Source = { maxFileSize?: number; + accesslog?: string; + clusterLimit?: number; id: string; - outgoingAddress?: string; outgoingAddressFamily?: 'ipv4' | 'ipv6' | 'dual'; deliverJobConcurrency?: number; inboxJobConcurrency?: number; - relationshipJobConcurrency?: number; + relashionshipJobConcurrency?: number; deliverJobPerSec?: number; inboxJobPerSec?: number; - relationshipJobPerSec?: number; + relashionshipJobPerSec?: number; deliverJobMaxAttempts?: number; inboxJobMaxAttempts?: number; mediaProxy?: string; + proxyRemoteFiles?: boolean; videoThumbnailGenerator?: string; - perChannelMaxNoteCacheCount?: number; - perUserNotificationsMaxCount?: number; - deactivateAntennaThreshold?: number; - pidFile: string; - - logging?: { - sql?: { - disableQueryTruncation?: boolean, - enableQueryParamLogging?: boolean, - } - } - // BEGIN comfy.social - noteFilterPlugin?: string; - // END comfy.social + signToActivityPubGet?: boolean; }; -export type Config = { - url: string; - port: number; - socket: string | undefined; - chmodSocket: string | undefined; - disableHsts: boolean | undefined; - db: { - host: string; - port: number; - db: string; - user: string; - pass: string; - disableCache?: boolean; - extra?: { [x: string]: string }; - }; - dbReplications: boolean | undefined; - dbSlaves: { - host: string; - port: number; - db: string; - user: string; - pass: string; - }[] | undefined; - fulltextSearch?: { - provider?: FulltextSearchProvider; - }; - meilisearch: { - host: string; - port: string; - apiKey: string; - ssl?: boolean; - index: string; - scope?: 'local' | 'global' | string[]; - } | undefined; - proxy: string | undefined; - proxySmtp: string | undefined; - proxyBypassHosts: string[] | undefined; - allowedPrivateNetworks: string[] | undefined; - maxFileSize: number; - clusterLimit: number | undefined; - id: string; - outgoingAddress: string | undefined; - outgoingAddressFamily: 'ipv4' | 'ipv6' | 'dual' | undefined; - deliverJobConcurrency: number | undefined; - inboxJobConcurrency: number | undefined; - relationshipJobConcurrency: number | undefined; - deliverJobPerSec: number | undefined; - inboxJobPerSec: number | undefined; - relationshipJobPerSec: number | undefined; - deliverJobMaxAttempts: number | undefined; - inboxJobMaxAttempts: number | undefined; - logging?: { - sql?: { - disableQueryTruncation?: boolean, - enableQueryParamLogging?: boolean, - } - } - +/** + * Misskeyが自動的に(ユーザーが設定した情報から推論して)設定する情報 + */ +export type Mixin = { version: string; - publishTarballInsteadOfProvideRepositoryUrl: boolean; - setupPassword: string | undefined; host: string; hostname: string; scheme: string; @@ -187,36 +111,16 @@ export type Config = { authUrl: string; driveUrl: string; userAgent: string; - frontendEntry: string; - frontendManifestExists: boolean; - frontendEmbedEntry: string; - frontendEmbedManifestExists: boolean; + clientEntry: string; + clientManifestExists: boolean; mediaProxy: string; externalMediaProxyEnabled: boolean; videoThumbnailGenerator: string | null; - redis: RedisOptions & RedisOptionsSource; - redisForPubsub: RedisOptions & RedisOptionsSource; - redisForJobQueue: RedisOptions & RedisOptionsSource; - redisForTimelines: RedisOptions & RedisOptionsSource; - redisForReactions: RedisOptions & RedisOptionsSource; - sentryForBackend: { options: Partial; enableNodeProfiling: boolean; } | undefined; - sentryForFrontend: { - options: Partial & { dsn: string }; - vueIntegration?: SentryVue.VueIntegrationOptions | null; - browserTracingIntegration?: Parameters[0] | null; - replayIntegration?: Parameters[0] | null; - } | undefined; - perChannelMaxNoteCacheCount: number; - perUserNotificationsMaxCount: number; - deactivateAntennaThreshold: number; - pidFile: string; - - // BEGIN comfy.social - noteFilterPlugin?: string; - // END comfy.social + redisForPubsub: NonNullable; + redisForJobQueue: NonNullable; }; -export type FulltextSearchProvider = 'sqlLike' | 'sqlPgroonga' | 'meilisearch'; +export type Config = Source & Mixin; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -229,108 +133,57 @@ const dir = `${_dirname}/../../../.config`; /** * Path of configuration file */ -export const path = process.env.MISSKEY_CONFIG_YML +const path = process.env.MISSKEY_CONFIG_YML ? resolve(dir, process.env.MISSKEY_CONFIG_YML) : process.env.NODE_ENV === 'test' ? resolve(dir, 'test.yml') : resolve(dir, 'default.yml'); -export function loadConfig(): Config { +export function loadConfig() { const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8')); - - const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json'); - const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json'); - const frontendManifest = frontendManifestExists ? - JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8')) + const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json'); + const clientManifest = clientManifestExists ? + JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8')) : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; - const frontendEmbedManifest = frontendEmbedManifestExists ? - JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8')) - : { 'src/boot.ts': { file: 'src/boot.ts' } }; - const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; - const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? ''); - const version = meta.version; - const host = url.host; - const hostname = url.hostname; - const scheme = url.protocol.replace(/:$/, ''); - const wsScheme = scheme.replace('http', 'ws'); + const mixin = {} as Mixin; - const dbDb = config.db.db ?? process.env.DATABASE_DB ?? ''; - const dbUser = config.db.user ?? process.env.DATABASE_USER ?? ''; - const dbPass = config.db.pass ?? process.env.DATABASE_PASSWORD ?? ''; + const url = tryCreateUrl(config.url); + + config.url = url.origin; + + config.port = config.port ?? parseInt(process.env.PORT ?? '', 10); + + mixin.version = meta.version; + mixin.host = url.host; + mixin.hostname = url.hostname; + mixin.scheme = url.protocol.replace(/:$/, ''); + mixin.wsScheme = mixin.scheme.replace('http', 'ws'); + mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`; + mixin.apiUrl = `${mixin.scheme}://${mixin.host}/api`; + mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; + mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; + mixin.userAgent = `Misskey/${meta.version} (${config.url})`; + mixin.clientEntry = clientManifest['src/_boot_.ts']; + mixin.clientManifestExists = clientManifestExists; const externalMediaProxy = config.mediaProxy ? config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy : null; - const internalMediaProxy = `${scheme}://${host}/proxy`; - const redis = convertRedisOptions(config.redis, host); + const internalMediaProxy = `${mixin.scheme}://${mixin.host}/proxy`; + mixin.mediaProxy = externalMediaProxy ?? internalMediaProxy; + mixin.externalMediaProxyEnabled = externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy; - return { - version, - publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl, - setupPassword: config.setupPassword, - url: url.origin, - port: config.port ?? parseInt(process.env.PORT ?? '', 10), - socket: config.socket, - chmodSocket: config.chmodSocket, - disableHsts: config.disableHsts, - host, - hostname, - scheme, - wsScheme, - wsUrl: `${wsScheme}://${host}`, - apiUrl: `${scheme}://${host}/api`, - authUrl: `${scheme}://${host}/auth`, - driveUrl: `${scheme}://${host}/files`, - db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass }, - dbReplications: config.dbReplications, - dbSlaves: config.dbSlaves, - fulltextSearch: config.fulltextSearch, - meilisearch: config.meilisearch, - redis, - redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis, - redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis, - redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis, - redisForReactions: config.redisForReactions ? convertRedisOptions(config.redisForReactions, host) : redis, - sentryForBackend: config.sentryForBackend, - sentryForFrontend: config.sentryForFrontend, - id: config.id, - proxy: config.proxy, - proxySmtp: config.proxySmtp, - proxyBypassHosts: config.proxyBypassHosts, - allowedPrivateNetworks: config.allowedPrivateNetworks, - maxFileSize: config.maxFileSize ?? 262144000, - clusterLimit: config.clusterLimit, - outgoingAddress: config.outgoingAddress, - outgoingAddressFamily: config.outgoingAddressFamily, - deliverJobConcurrency: config.deliverJobConcurrency, - inboxJobConcurrency: config.inboxJobConcurrency, - relationshipJobConcurrency: config.relationshipJobConcurrency, - deliverJobPerSec: config.deliverJobPerSec, - inboxJobPerSec: config.inboxJobPerSec, - relationshipJobPerSec: config.relationshipJobPerSec, - deliverJobMaxAttempts: config.deliverJobMaxAttempts, - inboxJobMaxAttempts: config.inboxJobMaxAttempts, - mediaProxy: externalMediaProxy ?? internalMediaProxy, - externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy, - videoThumbnailGenerator: config.videoThumbnailGenerator ? - config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator - : null, - userAgent: `Misskey/${version} (${config.url})`, - frontendEntry: frontendManifest['src/_boot_.ts'], - frontendManifestExists: frontendManifestExists, - frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'], - frontendEmbedManifestExists: frontendEmbedManifestExists, - perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000, - perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500, - deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7), - pidFile: config.pidFile, - logging: config.logging, - // BEGIN comfy.social - noteFilterPlugin: config.noteFilterPlugin, - // END comfy.social - }; + mixin.videoThumbnailGenerator = config.videoThumbnailGenerator ? + config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator + : null; + + if (!config.redis.prefix) config.redis.prefix = mixin.host; + if (config.redisForPubsub == null) config.redisForPubsub = config.redis; + if (config.redisForJobQueue == null) config.redisForJobQueue = config.redis; + + return Object.assign(config, mixin); } function tryCreateUrl(url: string) { @@ -340,14 +193,3 @@ function tryCreateUrl(url: string) { throw new Error(`url="${url}" is not a valid URL.`); } } - -function convertRedisOptions(options: RedisOptionsSource, host: string): RedisOptions & RedisOptionsSource { - return { - ...options, - password: options.pass, - prefix: options.prefix ?? host, - family: options.family ?? 0, - keyPrefix: `${options.prefix ?? host}:`, - db: options.db ?? 0, - }; -} diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index 1ca0397206..ee1a9a3093 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -1,15 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const MAX_NOTE_TEXT_LENGTH = 3000; export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days -export const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; - //#region hard limits // If you change DB_* values, you must also change the DB schema. @@ -26,18 +19,6 @@ export const DB_MAX_NOTE_TEXT_LENGTH = 8192; export const DB_MAX_IMAGE_COMMENT_LENGTH = 512; //#endregion -export const FILE_TYPE_IMAGE = [ - 'image/png', - 'image/gif', - 'image/jpeg', - 'image/webp', - 'image/avif', - 'image/apng', - 'image/bmp', - 'image/tiff', - 'image/x-icon', -]; - // ブラウザで直接表示することを許可するファイルの種類のリスト // ここに含まれないものは application/octet-stream としてレスポンスされる // SVGはXSSを生むので許可しない diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts deleted file mode 100644 index 9bca795479..0000000000 --- a/packages/backend/src/core/AbuseReportNotificationService.ts +++ /dev/null @@ -1,432 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common'; -import { Brackets, In, IsNull, Not } from 'typeorm'; -import * as Redis from 'ioredis'; -import sanitizeHtml from 'sanitize-html'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js'; -import type { - AbuseReportNotificationRecipientRepository, - MiAbuseReportNotificationRecipient, - MiAbuseUserReport, - MiMeta, - MiUser, -} from '@/models/_.js'; -import { EmailService } from '@/core/EmailService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { IdService } from './IdService.js'; - -@Injectable() -export class AbuseReportNotificationService implements OnApplicationShutdown { - constructor( - @Inject(DI.meta) - private meta: MiMeta, - - @Inject(DI.abuseReportNotificationRecipientRepository) - private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository, - - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - - private idService: IdService, - private roleService: RoleService, - private systemWebhookService: SystemWebhookService, - private emailService: EmailService, - private moderationLogService: ModerationLogService, - private globalEventService: GlobalEventService, - private userEntityService: UserEntityService, - ) { - this.redisForSub.on('message', this.onMessage); - } - - /** - * 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する. - * 通知先ユーザは{@link getModeratorIds}の取得結果に依る. - * - * @see RoleService.getModeratorIds - * @see GlobalEventService.publishAdminStream - */ - @bindThis - public async notifyAdminStream(abuseReports: MiAbuseUserReport[]) { - if (abuseReports.length <= 0) { - return; - } - - const moderatorIds = await this.roleService.getModeratorIds({ - includeAdmins: true, - excludeExpire: true, - }); - - for (const moderatorId of moderatorIds) { - for (const abuseReport of abuseReports) { - this.globalEventService.publishAdminStream( - moderatorId, - 'newAbuseUserReport', - { - id: abuseReport.id, - targetUserId: abuseReport.targetUserId, - reporterId: abuseReport.reporterId, - comment: abuseReport.comment, - }, - ); - } - } - } - - /** - * Mailを用いて{@link abuseReports}の内容を管理者各位に通知する. - * メールアドレスの送信先は以下の通り. - * - モデレータ権限所有者ユーザ(設定画面からメールアドレスの設定を行っているユーザに限る) - * - metaテーブルに設定されているメールアドレス - * - * @see EmailService.sendEmail - */ - @bindThis - public async notifyMail(abuseReports: MiAbuseUserReport[]) { - if (abuseReports.length <= 0) { - return; - } - - const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it - .filter(it => it.isActive && it.userProfile?.emailVerified) - .map(it => it.userProfile?.email) - .filter(x => x != null), - ); - - recipientEMailAddresses.push( - ...(this.meta.email ? [this.meta.email] : []), - ); - - if (recipientEMailAddresses.length <= 0) { - return; - } - - for (const mailAddress of recipientEMailAddresses) { - await Promise.all( - abuseReports.map(it => { - // TODO: 送信処理はJobQueue化したい - return this.emailService.sendEmail( - mailAddress, - 'New Abuse Report', - sanitizeHtml(it.comment), - sanitizeHtml(it.comment), - ); - }), - ); - } - } - - /** - * SystemWebhookを用いて{@link abuseReports}の内容を管理者各位に通知する. - * ここではJobQueueへのエンキューのみを行うため、即時実行されない. - * - * @see SystemWebhookService.enqueueSystemWebhook - */ - @bindThis - public async notifySystemWebhook( - abuseReports: MiAbuseUserReport[], - type: 'abuseReport' | 'abuseReportResolved', - ) { - if (abuseReports.length <= 0) { - return; - } - - const usersMap = await this.userEntityService.packMany( - [ - ...new Set([ - ...abuseReports.map(it => it.reporter ?? it.reporterId), - ...abuseReports.map(it => it.targetUser ?? it.targetUserId), - ...abuseReports.map(it => it.assignee ?? it.assigneeId), - ].filter(x => x != null)), - ], - null, - { schema: 'UserLite' }, - ).then(it => new Map(it.map(it => [it.id, it]))); - const convertedReports = abuseReports.map(it => { - return { - ...it, - reporter: usersMap.get(it.reporterId) ?? null, - targetUser: usersMap.get(it.targetUserId) ?? null, - assignee: it.assigneeId ? (usersMap.get(it.assigneeId) ?? null) : null, - }; - }); - - const inactiveRecipients = await this.fetchWebhookRecipients() - .then(it => it.filter(it => !it.isActive)); - const withoutWebhookIds = inactiveRecipients - .map(it => it.systemWebhookId) - .filter(x => x != null); - return Promise.all( - convertedReports.map(it => { - return this.systemWebhookService.enqueueSystemWebhook( - type, - it, - { - excludes: withoutWebhookIds, - }, - ); - }), - ); - } - - /** - * 通報の通知先一覧を取得する. - * - * @param {Object} [params] クエリの取得条件 - * @param {Object} [params.method] 取得する通知先の通知方法 - * @param {Object} [opts] 動作時の詳細なオプション - * @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true) - * @param {boolean} [opts.joinUser] 通知先のユーザ情報をJOINするかどうか(default: false) - * @param {boolean} [opts.joinSystemWebhook] 通知先のSystemWebhook情報をJOINするかどうか(default: false) - * @see removeUnauthorizedRecipientUsers - */ - @bindThis - public async fetchRecipients( - params?: { - ids?: MiAbuseReportNotificationRecipient['id'][], - method?: RecipientMethod[], - }, - opts?: { - removeUnauthorized?: boolean, - joinUser?: boolean, - joinSystemWebhook?: boolean, - }, - ): Promise { - const query = this.abuseReportNotificationRecipientRepository.createQueryBuilder('recipient'); - - if (opts?.joinUser) { - query.innerJoinAndSelect('user', 'user', 'recipient.userId = user.id'); - query.innerJoinAndSelect('recipient.userProfile', 'userProfile'); - } - - if (opts?.joinSystemWebhook) { - query.innerJoinAndSelect('recipient.systemWebhook', 'systemWebhook'); - } - - if (params?.ids) { - query.andWhere({ id: In(params.ids) }); - } - - if (params?.method) { - query.andWhere(new Brackets(qb => { - if (params.method?.includes('email')) { - qb.orWhere({ method: 'email', userId: Not(IsNull()) }); - } - if (params.method?.includes('webhook')) { - qb.orWhere({ method: 'webhook', userId: IsNull() }); - } - })); - } - - const recipients = await query.getMany(); - if (recipients.length <= 0) { - return []; - } - - // アサイン有効期限切れはイベントで拾えないので、このタイミングでチェック及び削除(オプション) - return (opts?.removeUnauthorized ?? true) - ? await this.removeUnauthorizedRecipientUsers(recipients) - : recipients; - } - - /** - * EMailの通知先一覧を取得する. - * リレーション先の{@link MiUser}および{@link MiUserProfile}も同時に取得する. - * - * @param {Object} [opts] - * @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true) - * @see removeUnauthorizedRecipientUsers - */ - @bindThis - public async fetchEMailRecipients(opts?: { - removeUnauthorized?: boolean - }): Promise { - return this.fetchRecipients({ method: ['email'] }, { joinUser: true, ...opts }); - } - - /** - * Webhookの通知先一覧を取得する. - * リレーション先の{@link MiSystemWebhook}も同時に取得する. - */ - @bindThis - public fetchWebhookRecipients(): Promise { - return this.fetchRecipients({ method: ['webhook'] }, { joinSystemWebhook: true }); - } - - /** - * 通知先を作成する. - */ - @bindThis - public async createRecipient( - params: { - isActive: MiAbuseReportNotificationRecipient['isActive']; - name: MiAbuseReportNotificationRecipient['name']; - method: MiAbuseReportNotificationRecipient['method']; - userId: MiAbuseReportNotificationRecipient['userId']; - systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId']; - }, - updater: MiUser, - ): Promise { - const id = this.idService.gen(); - await this.abuseReportNotificationRecipientRepository.insert({ - ...params, - id, - }); - - const created = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: id }); - - this.moderationLogService - .log(updater, 'createAbuseReportNotificationRecipient', { - recipientId: id, - recipient: created, - }); - - return created; - } - - /** - * 通知先を更新する. - */ - @bindThis - public async updateRecipient( - params: { - id: MiAbuseReportNotificationRecipient['id']; - isActive: MiAbuseReportNotificationRecipient['isActive']; - name: MiAbuseReportNotificationRecipient['name']; - method: MiAbuseReportNotificationRecipient['method']; - userId: MiAbuseReportNotificationRecipient['userId']; - systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId']; - }, - updater: MiUser, - ): Promise { - const beforeEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id }); - - await this.abuseReportNotificationRecipientRepository.update(params.id, { - isActive: params.isActive, - updatedAt: new Date(), - name: params.name, - method: params.method, - userId: params.userId, - systemWebhookId: params.systemWebhookId, - }); - - const afterEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id }); - - this.moderationLogService - .log(updater, 'updateAbuseReportNotificationRecipient', { - recipientId: params.id, - before: beforeEntity, - after: afterEntity, - }); - - return afterEntity; - } - - /** - * 通知先を削除する. - */ - @bindThis - public async deleteRecipient( - id: MiAbuseReportNotificationRecipient['id'], - updater: MiUser, - ) { - const entity = await this.abuseReportNotificationRecipientRepository.findBy({ id }); - - await this.abuseReportNotificationRecipientRepository.delete(id); - - this.moderationLogService - .log(updater, 'deleteAbuseReportNotificationRecipient', { - recipientId: id, - recipient: entity, - }); - } - - /** - * モデレータ権限を持たない(*1)通知先ユーザを削除する. - * - * *1: 以下の両方を満たすものの事を言う - * - 通知先にユーザIDが設定されている - * - 付与ロールにモデレータ権限がない or アサインの有効期限が切れている - * - * @param recipients 通知先一覧の配列 - * @returns {@lisk recipients}からモデレータ権限を持たない通知先を削除した配列 - */ - @bindThis - private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise { - const userRecipients = recipients.filter(it => it.userId !== null); - const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(x => x != null)); - if (recipientUserIds.size <= 0) { - // ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い - return recipients; - } - - // モデレータ権限の有無で通知先設定を振り分ける - const authorizedUserIds = await this.roleService.getModeratorIds({ - includeAdmins: true, - excludeExpire: true, - }); - const authorizedUserRecipients = Array.of(); - const unauthorizedUserRecipients = Array.of(); - for (const recipient of userRecipients) { - // eslint-disable-next-line - if (authorizedUserIds.includes(recipient.userId!)) { - authorizedUserRecipients.push(recipient); - } else { - unauthorizedUserRecipients.push(recipient); - } - } - - // モデレータ権限を持たない通知先をDBから削除する - if (unauthorizedUserRecipients.length > 0) { - await this.abuseReportNotificationRecipientRepository.delete(unauthorizedUserRecipients.map(it => it.id)); - } - const nonUserRecipients = recipients.filter(it => it.userId === null); - return [...nonUserRecipients, ...authorizedUserRecipients].sort((a, b) => a.id.localeCompare(b.id)); - } - - @bindThis - private async onMessage(_: string, data: string): Promise { - const obj = JSON.parse(data); - if (obj.channel !== 'internal') { - return; - } - - const { type } = obj.message as GlobalEvents['internal']['payload']; - switch (type) { - case 'roleUpdated': - case 'roleDeleted': - case 'userRoleUnassigned': { - // 場合によってはキャッシュ更新よりも先にここが呼ばれてしまう可能性があるのでnextTickで遅延実行 - process.nextTick(async () => { - const recipients = await this.abuseReportNotificationRecipientRepository.findBy({ - userId: Not(IsNull()), - }); - await this.removeUnauthorizedRecipientUsers(recipients); - }); - break; - } - default: { - break; - } - } - } - - @bindThis - public dispose(): void { - this.redisForSub.off('message', this.onMessage); - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); - } -} diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts deleted file mode 100644 index 846d2c8ebd..0000000000 --- a/packages/backend/src/core/AbuseReportService.ts +++ /dev/null @@ -1,176 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; -import { QueueService } from '@/core/QueueService.js'; -import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { SystemAccountService } from '@/core/SystemAccountService.js'; -import { IdService } from './IdService.js'; - -@Injectable() -export class AbuseReportService { - constructor( - @Inject(DI.abuseUserReportsRepository) - private abuseUserReportsRepository: AbuseUserReportsRepository, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private idService: IdService, - private abuseReportNotificationService: AbuseReportNotificationService, - private queueService: QueueService, - private systemAccountService: SystemAccountService, - private apRendererService: ApRendererService, - private moderationLogService: ModerationLogService, - ) { - } - - /** - * ユーザからの通報をDBに記録し、その内容を下記の手段で管理者各位に通知する. - * - 管理者用Redisイベント - * - EMail(モデレータ権限所有者ユーザ+metaテーブルに設定されているメールアドレス) - * - SystemWebhook - * - * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える - * @see AbuseReportNotificationService.notify - */ - @bindThis - public async report(params: { - targetUserId: MiAbuseUserReport['targetUserId'], - targetUserHost: MiAbuseUserReport['targetUserHost'], - reporterId: MiAbuseUserReport['reporterId'], - reporterHost: MiAbuseUserReport['reporterHost'], - comment: string, - }[]) { - const entities = params.map(param => { - return { - id: this.idService.gen(), - targetUserId: param.targetUserId, - targetUserHost: param.targetUserHost, - reporterId: param.reporterId, - reporterHost: param.reporterHost, - comment: param.comment, - }; - }); - - const reports = Array.of(); - for (const entity of entities) { - const report = await this.abuseUserReportsRepository.insertOne(entity); - reports.push(report); - } - - return Promise.all([ - this.abuseReportNotificationService.notifyAdminStream(reports), - this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'), - this.abuseReportNotificationService.notifyMail(reports), - ]); - } - - /** - * 通報を解決し、その内容を下記の手段で管理者各位に通知する. - * - SystemWebhook - * - * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える - * @param moderator 通報を処理したユーザ - * @see AbuseReportNotificationService.notify - */ - @bindThis - public async resolve( - params: { - reportId: string; - resolvedAs: MiAbuseUserReport['resolvedAs']; - }[], - moderator: MiUser, - ) { - const paramsMap = new Map(params.map(it => [it.reportId, it])); - const reports = await this.abuseUserReportsRepository.findBy({ - id: In(params.map(it => it.reportId)), - }); - - for (const report of reports) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const ps = paramsMap.get(report.id)!; - - await this.abuseUserReportsRepository.update(report.id, { - resolved: true, - assigneeId: moderator.id, - resolvedAs: ps.resolvedAs, - }); - - this.moderationLogService - .log(moderator, 'resolveAbuseReport', { - reportId: report.id, - report: report, - resolvedAs: ps.resolvedAs, - }); - } - - return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) }) - .then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved')); - } - - @bindThis - public async forward( - reportId: MiAbuseUserReport['id'], - moderator: MiUser, - ) { - const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId }); - - if (report.targetUserHost == null) { - throw new Error('The target user host is null.'); - } - - if (report.forwarded) { - throw new Error('The report has already been forwarded.'); - } - - await this.abuseUserReportsRepository.update(report.id, { - forwarded: true, - }); - - const actor = await this.systemAccountService.fetch('actor'); - const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId }); - - const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment); - const contextAssignedFlag = this.apRendererService.addContext(flag); - this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false); - - this.moderationLogService - .log(moderator, 'forwardAbuseReport', { - reportId: report.id, - report: report, - }); - } - - @bindThis - public async update( - reportId: MiAbuseUserReport['id'], - params: { - moderationNote?: MiAbuseUserReport['moderationNote']; - }, - moderator: MiUser, - ) { - const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId }); - - await this.abuseUserReportsRepository.update(report.id, { - moderationNote: params.moderationNote, - }); - - if (params.moderationNote != null && report.moderationNote !== params.moderationNote) { - this.moderationLogService.log(moderator, 'updateAbuseReportNote', { - reportId: report.id, - report: report, - before: report.moderationNote, - after: params.moderationNote, - }); - } - } -} diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index f8e3eaf01f..ab11785e28 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -1,16 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { IsNull, In, MoreThan, Not } from 'typeorm'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; -import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MiMeta, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js'; +import type { Config } from '@/config.js'; +import type { LocalUser, RemoteUser } from '@/models/entities/User.js'; +import type { BlockingsRepository, FollowingsRepository, InstancesRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; +import type { User } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -20,18 +17,18 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { CacheService } from '@/core/CacheService.js'; +import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { MetaService } from '@/core/MetaService.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; -import { SystemAccountService } from '@/core/SystemAccountService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { AntennaService } from '@/core/AntennaService.js'; @Injectable() export class AccountMoveService { constructor( - @Inject(DI.meta) - private meta: MiMeta, + @Inject(DI.config) + private config: Config, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -45,8 +42,8 @@ export class AccountMoveService { @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, @@ -57,14 +54,14 @@ export class AccountMoveService { private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, private globalEventService: GlobalEventService, + private proxyAccountService: ProxyAccountService, private perUserFollowingChart: PerUserFollowingChart, private federatedInstanceService: FederatedInstanceService, private instanceChart: InstanceChart, + private metaService: MetaService, private relayService: RelayService, + private cacheService: CacheService, private queueService: QueueService, - private systemAccountService: SystemAccountService, - private roleService: RoleService, - private antennaService: AntennaService, ) { } @@ -74,12 +71,12 @@ export class AccountMoveService { * After delivering Move activity, its local followers unfollow the old account and then follow the new one. */ @bindThis - public async moveFromLocal(src: MiLocalUser, dst: MiLocalUser | MiRemoteUser): Promise { + public async moveFromLocal(src: LocalUser, dst: LocalUser | RemoteUser): Promise { const srcUri = this.userEntityService.getUserUri(src); const dstUri = this.userEntityService.getUserUri(dst); // add movedToUri to indicate that the user has moved - const update = {} as Partial; + const update = {} as Partial; update.alsoKnownAs = src.alsoKnownAs?.includes(dstUri) ? src.alsoKnownAs : src.alsoKnownAs?.concat([dstUri]) ?? [dstUri]; update.movedToUri = dstUri; update.movedAt = new Date(); @@ -87,7 +84,7 @@ export class AccountMoveService { Object.assign(src, update); // Update cache - this.globalEventService.publishInternalEvent('localUserUpdated', src); + this.cacheService.uriPersonCache.set(srcUri, src); const srcPerson = await this.apRendererService.renderPerson(src); const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src)); @@ -99,7 +96,7 @@ export class AccountMoveService { await this.apDeliverManagerService.deliverToFollowers(src, moveAct); // Publish meUpdated event - const iObj = await this.userEntityService.pack(src.id, src, { schema: 'MeDetailed', includeSecrets: true }); + const iObj = await this.userEntityService.pack(src.id, src, { detail: true, includeSecrets: true }); this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj); // Unfollow after 24 hours @@ -117,26 +114,24 @@ export class AccountMoveService { } @bindThis - public async postMoveProcess(src: MiUser, dst: MiUser): Promise { + public async postMoveProcess(src: User, dst: User): Promise { // Copy blockings and mutings, and update lists try { await Promise.all([ this.copyBlocking(src, dst), this.copyMutings(src, dst), - this.copyRoles(src, dst), this.updateLists(src, dst), - this.antennaService.onMoveAccount(src, dst), ]); } catch { /* skip if any error happens */ } // follow the new account - const proxy = await this.systemAccountService.fetch('proxy'); + const proxy = await this.proxyAccountService.fetch(); const followings = await this.followingsRepository.findBy({ followeeId: src.id, followerHost: IsNull(), // follower is local - followerId: Not(proxy.id), + followerId: proxy ? Not(proxy.id) : undefined, }); const followJobs = followings.map(following => ({ from: { id: following.followerId }, @@ -185,13 +180,13 @@ export class AccountMoveService { { muteeId: dst.id, expiresAt: IsNull() }, ).then(mutings => mutings.map(muting => muting.muterId)); - const newMutings: Map = new Map(); + const newMutings: Map = new Map(); // 重複しないようにIDを生成 const genId = (): string => { let id: string; do { - id = this.idService.gen(); + id = this.idService.genId(); } while (newMutings.has(id)); return id; }; @@ -199,6 +194,7 @@ export class AccountMoveService { if (existingMutingsMuterUserIds.includes(muting.muterId)) continue; // skip if already muted indefinitely newMutings.set(genId(), { ...muting, + createdAt: new Date(), muteeId: dst.id, }); } @@ -207,32 +203,6 @@ export class AccountMoveService { await this.mutingsRepository.insert(arrayToInsert); } - @bindThis - public async copyRoles(src: ThinUser, dst: ThinUser): Promise { - // Insert new roles with the same values except userId - // role service may have cache for roles so retrieve roles from service - const [oldRoleAssignments, roles] = await Promise.all([ - this.roleService.getUserAssigns(src.id), - this.roleService.getRoles(), - ]); - - if (oldRoleAssignments.length === 0) return; - - // No promise all since the only async operation is writing to the database - for (const oldRoleAssignment of oldRoleAssignments) { - const role = roles.find(x => x.id === oldRoleAssignment.roleId); - if (role == null) continue; // Very unlikely however removing role may cause this case - if (!role.preserveAssignmentOnMoveAccount) continue; - - try { - await this.roleService.assign(dst.id, role.id, oldRoleAssignment.expiresAt); - } catch (e) { - if (e instanceof RoleService.AlreadyAssignedError) continue; - throw e; - } - } - } - /** * Update lists while moving accounts. * - No removal of the old account from the lists @@ -243,52 +213,54 @@ export class AccountMoveService { * @returns Promise */ @bindThis - public async updateLists(src: ThinUser, dst: MiUser): Promise { + public async updateLists(src: ThinUser, dst: User): Promise { // Return if there is no list to be updated. - const oldMemberships = await this.userListMembershipsRepository.find({ + const oldJoinings = await this.userListJoiningsRepository.find({ where: { userId: src.id, }, }); - if (oldMemberships.length === 0) return; + if (oldJoinings.length === 0) return; - const existingUserListIds = await this.userListMembershipsRepository.find({ + const existingUserListIds = await this.userListJoiningsRepository.find({ where: { userId: dst.id, }, - }).then(memberships => memberships.map(membership => membership.userListId)); + }).then(joinings => joinings.map(joining => joining.userListId)); - const newMemberships: Map = new Map(); + const newJoinings: Map = new Map(); // 重複しないようにIDを生成 const genId = (): string => { let id: string; do { - id = this.idService.gen(); - } while (newMemberships.has(id)); + id = this.idService.genId(); + } while (newJoinings.has(id)); return id; }; - for (const membership of oldMemberships) { - if (existingUserListIds.includes(membership.userListId)) continue; // skip if dst exists in this user's list - newMemberships.set(genId(), { + for (const joining of oldJoinings) { + if (existingUserListIds.includes(joining.userListId)) continue; // skip if dst exists in this user's list + newJoinings.set(genId(), { + createdAt: new Date(), userId: dst.id, - userListId: membership.userListId, - userListUserId: membership.userListUserId, + userListId: joining.userListId, }); } - const arrayToInsert = Array.from(newMemberships.entries()).map(entry => ({ ...entry[1], id: entry[0] })); - await this.userListMembershipsRepository.insert(arrayToInsert); + const arrayToInsert = Array.from(newJoinings.entries()).map(entry => ({ ...entry[1], id: entry[0] })); + await this.userListJoiningsRepository.insert(arrayToInsert); // Have the proxy account follow the new account in the same way as UserListService.push if (this.userEntityService.isRemoteUser(dst)) { - const proxy = await this.systemAccountService.fetch('proxy'); - this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: dst.id } }]); + const proxy = await this.proxyAccountService.fetch(); + if (proxy) { + this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: dst.id } }]); + } } } @bindThis - private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: MiUser): Promise { + private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: User): Promise { if (localFollowerIds.length === 0) return; // Set the old account's following and followers counts to 0. @@ -304,15 +276,13 @@ export class AccountMoveService { } // Update instance stats by decreasing remote followers count by the number of local followers who were following the old account. - if (this.meta.enableStatsForFederatedInstances) { - if (this.userEntityService.isRemoteUser(oldAccount)) { - this.federatedInstanceService.fetchOrRegister(oldAccount.host).then(async i => { - this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length); - if (this.meta.enableChartsForFederatedInstances) { - this.instanceChart.updateFollowers(i.host, false); - } - }); - } + if (this.userEntityService.isRemoteUser(oldAccount)) { + this.federatedInstanceService.fetch(oldAccount.host).then(async i => { + this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowers(i.host, false); + } + }); } // FIXME: expensive? @@ -325,20 +295,20 @@ export class AccountMoveService { * dstユーザーのalsoKnownAsをfetchPersonしていき、本当にmovedToUrlをdstに指定するユーザーが存在するのかを調べる * * @param dst movedToUrlを指定するユーザー - * @param check + * @param check * @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか * @returns Promise */ @bindThis public async validateAlsoKnownAs( - dst: MiLocalUser | MiRemoteUser, - check: (oldUser: MiLocalUser | MiRemoteUser | null, newUser: MiLocalUser | MiRemoteUser) => boolean | Promise = () => true, + dst: LocalUser | RemoteUser, + check: (oldUser: LocalUser | RemoteUser | null, newUser: LocalUser | RemoteUser) => boolean | Promise = () => true, instant = false, - ): Promise { - let resultUser: MiLocalUser | MiRemoteUser | null = null; + ): Promise { + let resultUser: LocalUser | RemoteUser | null = null; if (this.userEntityService.isRemoteUser(dst)) { - if (Date.now() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { + if ((new Date()).getTime() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { await this.apPersonService.updatePerson(dst.uri); } dst = await this.apPersonService.fetchPerson(dst.uri) ?? dst; @@ -354,7 +324,7 @@ export class AccountMoveService { if (!src) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー if (this.userEntityService.isRemoteUser(dst)) { - if (Date.now() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { + if ((new Date()).getTime() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { await this.apPersonService.updatePerson(srcUri); } diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index 69a57b4854..b146fc66be 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -1,12 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; +import type { UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; +import type { User } from '@/models/entities/User.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { RelayService } from '@/core/RelayService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; @@ -16,6 +12,9 @@ import { bindThis } from '@/decorators.js'; @Injectable() export class AccountUpdateService { constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -27,7 +26,7 @@ export class AccountUpdateService { } @bindThis - public async publishToFollowers(userId: MiUser['id']) { + public async publishToFollowers(userId: User['id']) { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) throw new Error('user not found'); diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts index 8d2de89efd..9e223f1492 100644 --- a/packages/backend/src/core/AchievementService.ts +++ b/packages/backend/src/core/AchievementService.ts @@ -1,19 +1,93 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UserProfilesRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; +import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { NotificationService } from '@/core/NotificationService.js'; -import { ACHIEVEMENT_TYPES } from '@/models/UserProfile.js'; + +export const ACHIEVEMENT_TYPES = [ + 'notes1', + 'notes10', + 'notes100', + 'notes500', + 'notes1000', + 'notes5000', + 'notes10000', + 'notes20000', + 'notes30000', + 'notes40000', + 'notes50000', + 'notes60000', + 'notes70000', + 'notes80000', + 'notes90000', + 'notes100000', + 'login3', + 'login7', + 'login15', + 'login30', + 'login60', + 'login100', + 'login200', + 'login300', + 'login400', + 'login500', + 'login600', + 'login700', + 'login800', + 'login900', + 'login1000', + 'passedSinceAccountCreated1', + 'passedSinceAccountCreated2', + 'passedSinceAccountCreated3', + 'loggedInOnBirthday', + 'loggedInOnNewYearsDay', + 'noteClipped1', + 'noteFavorited1', + 'myNoteFavorited1', + 'profileFilled', + 'markedAsCat', + 'following1', + 'following10', + 'following50', + 'following100', + 'following300', + 'followers1', + 'followers10', + 'followers50', + 'followers100', + 'followers300', + 'followers500', + 'followers1000', + 'collectAchievements30', + 'viewAchievements3min', + 'iLoveMisskey', + 'foundTreasure', + 'client30min', + 'client60min', + 'noteDeletedWithin1min', + 'postedAtLateNight', + 'postedAt0min0sec', + 'selfQuote', + 'htl20npm', + 'viewInstanceChart', + 'outputHelloWorldOnScratchpad', + 'open3windows', + 'driveFolderCircularReference', + 'reactWithoutRead', + 'clickedClickHere', + 'justPlainLucky', + 'setNameToSyuilo', + 'cookieClicked', + 'brainDiver', +] as const; @Injectable() export class AchievementService { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @@ -23,7 +97,7 @@ export class AchievementService { @bindThis public async create( - userId: MiUser['id'], + userId: User['id'], type: typeof ACHIEVEMENT_TYPES[number], ): Promise { if (!ACHIEVEMENT_TYPES.includes(type)) return; diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts index 248a9b8979..059e335eff 100644 --- a/packages/backend/src/core/AiService.ts +++ b/packages/backend/src/core/AiService.ts @@ -1,56 +1,46 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import * as nsfw from 'nsfwjs'; import si from 'systeminformation'; -import { Mutex } from 'async-mutex'; -import fetch from 'node-fetch'; +import type { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); -const REQUIRED_CPU_FLAGS_X64 = ['avx2', 'fma']; +const REQUIRED_CPU_FLAGS = ['avx2', 'fma']; let isSupportedCpu: undefined | boolean = undefined; @Injectable() export class AiService { private model: nsfw.NSFWJS; - private modelLoadMutex: Mutex = new Mutex(); constructor( + @Inject(DI.config) + private config: Config, ) { } @bindThis - public async detectSensitive(path: string): Promise { + public async detectSensitive(path: string): Promise { try { if (isSupportedCpu === undefined) { - isSupportedCpu = await this.computeIsSupportedCpu(); + const cpuFlags = await this.getCpuFlags(); + isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required)); } - + if (!isSupportedCpu) { console.error('This CPU cannot use TensorFlow.'); return null; } - + const tf = await import('@tensorflow/tfjs-node'); - tf.env().global.fetch = fetch; - - if (this.model == null) { - await this.modelLoadMutex.runExclusive(async () => { - if (this.model == null) { - this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 }); - } - }); - } - + + if (this.model == null) this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 }); + const buffer = await fs.promises.readFile(path); const image = await tf.node.decodeImage(buffer, 3) as any; try { @@ -65,22 +55,6 @@ export class AiService { } } - private async computeIsSupportedCpu(): Promise { - switch (process.arch) { - case 'x64': { - const cpuFlags = await this.getCpuFlags(); - return REQUIRED_CPU_FLAGS_X64.every(required => cpuFlags.includes(required)); - } - case 'arm64': { - // As far as I know, no required CPU flags for ARM64. - return true; - } - default: { - return false; - } - } - } - @bindThis private async getCpuFlags(): Promise { const str = await si.cpuFlags(); diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts deleted file mode 100644 index a9f6731977..0000000000 --- a/packages/backend/src/core/AnnouncementService.ts +++ /dev/null @@ -1,223 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Brackets, EntityNotFoundError } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import type { MiUser } from '@/models/User.js'; -import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead, UsersRepository } from '@/models/_.js'; -import { bindThis } from '@/decorators.js'; -import { Packed } from '@/misc/json-schema.js'; -import { IdService } from '@/core/IdService.js'; -import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; - -@Injectable() -export class AnnouncementService { - constructor( - @Inject(DI.announcementsRepository) - private announcementsRepository: AnnouncementsRepository, - - @Inject(DI.announcementReadsRepository) - private announcementReadsRepository: AnnouncementReadsRepository, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private idService: IdService, - private globalEventService: GlobalEventService, - private moderationLogService: ModerationLogService, - private announcementEntityService: AnnouncementEntityService, - ) { - } - - @bindThis - public async getReads(userId: MiUser['id']): Promise { - return this.announcementReadsRepository.findBy({ - userId: userId, - }); - } - - @bindThis - public async getUnreadAnnouncements(user: MiUser): Promise { - const readsQuery = this.announcementReadsRepository.createQueryBuilder('read') - .select('read.announcementId') - .where('read.userId = :userId', { userId: user.id }); - - const q = this.announcementsRepository.createQueryBuilder('announcement') - .where('announcement.isActive = true') - .andWhere('announcement.silence = false') - .andWhere(new Brackets(qb => { - qb.orWhere('announcement.userId = :userId', { userId: user.id }); - qb.orWhere('announcement.userId IS NULL'); - })) - .andWhere(new Brackets(qb => { - qb.orWhere('announcement.forExistingUsers = false'); - qb.orWhere('announcement.id > :userId', { userId: user.id }); - })) - .andWhere(`announcement.id NOT IN (${ readsQuery.getQuery() })`); - - q.setParameters(readsQuery.getParameters()); - - return q.getMany(); - } - - @bindThis - public async create(values: Partial, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> { - const announcement = await this.announcementsRepository.insertOne({ - id: this.idService.gen(), - updatedAt: null, - title: values.title, - text: values.text, - imageUrl: values.imageUrl || null, - icon: values.icon, - display: values.display, - forExistingUsers: values.forExistingUsers, - silence: values.silence, - needConfirmationToRead: values.needConfirmationToRead, - userId: values.userId, - }); - - const packed = await this.announcementEntityService.pack(announcement); - - if (values.userId) { - this.globalEventService.publishMainStream(values.userId, 'announcementCreated', { - announcement: packed, - }); - - if (moderator) { - const user = await this.usersRepository.findOneByOrFail({ id: values.userId }); - this.moderationLogService.log(moderator, 'createUserAnnouncement', { - announcementId: announcement.id, - announcement: announcement, - userId: values.userId, - userUsername: user.username, - userHost: user.host, - }); - } - } else { - this.globalEventService.publishBroadcastStream('announcementCreated', { - announcement: packed, - }); - - if (moderator) { - this.moderationLogService.log(moderator, 'createGlobalAnnouncement', { - announcementId: announcement.id, - announcement: announcement, - }); - } - } - - return { - raw: announcement, - packed: packed, - }; - } - - @bindThis - public async update(announcement: MiAnnouncement, values: Partial, moderator?: MiUser): Promise { - await this.announcementsRepository.update(announcement.id, { - updatedAt: new Date(), - title: values.title, - text: values.text, - /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */ - imageUrl: values.imageUrl || null, - display: values.display, - icon: values.icon, - forExistingUsers: values.forExistingUsers, - silence: values.silence, - needConfirmationToRead: values.needConfirmationToRead, - isActive: values.isActive, - }); - - const after = await this.announcementsRepository.findOneByOrFail({ id: announcement.id }); - - if (moderator) { - if (announcement.userId) { - const user = await this.usersRepository.findOneByOrFail({ id: announcement.userId }); - this.moderationLogService.log(moderator, 'updateUserAnnouncement', { - announcementId: announcement.id, - before: announcement, - after: after, - userId: announcement.userId, - userUsername: user.username, - userHost: user.host, - }); - } else { - this.moderationLogService.log(moderator, 'updateGlobalAnnouncement', { - announcementId: announcement.id, - before: announcement, - after: after, - }); - } - } - } - - @bindThis - public async delete(announcement: MiAnnouncement, moderator?: MiUser): Promise { - await this.announcementsRepository.delete(announcement.id); - - if (moderator) { - if (announcement.userId) { - const user = await this.usersRepository.findOneByOrFail({ id: announcement.userId }); - this.moderationLogService.log(moderator, 'deleteUserAnnouncement', { - announcementId: announcement.id, - announcement: announcement, - userId: announcement.userId, - userUsername: user.username, - userHost: user.host, - }); - } else { - this.moderationLogService.log(moderator, 'deleteGlobalAnnouncement', { - announcementId: announcement.id, - announcement: announcement, - }); - } - } - } - - @bindThis - public async getAnnouncement(announcementId: MiAnnouncement['id'], me: MiUser | null): Promise> { - const announcement = await this.announcementsRepository.findOneByOrFail({ id: announcementId }); - if (me) { - if (announcement.userId && announcement.userId !== me.id) { - throw new EntityNotFoundError(this.announcementsRepository.metadata.target, { id: announcementId }); - } - - const read = await this.announcementReadsRepository.findOneBy({ - announcementId: announcement.id, - userId: me.id, - }); - return this.announcementEntityService.pack({ ...announcement, isRead: read !== null }, me); - } else { - return this.announcementEntityService.pack(announcement, null); - } - } - - @bindThis - public async read(user: MiUser, announcementId: MiAnnouncement['id']): Promise { - try { - await this.announcementReadsRepository.insert({ - id: this.idService.gen(), - announcementId: announcementId, - userId: user.id, - }); - } catch (e) { - return; - } - - const announcement = await this.announcementsRepository.findOneBy({ id: announcementId }); - if (announcement != null && announcement.userId === user.id) { - await this.announcementsRepository.update(announcementId, { - isActive: false, - }); - } - - if ((await this.getUnreadAnnouncements(user)).length === 0) { - this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements'); - } - } -} diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index ec79675b06..d8df371916 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -1,48 +1,53 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import { In } from 'typeorm'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import type { Antenna } from '@/models/entities/Antenna.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { User } from '@/models/entities/User.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; +import { IdService } from '@/core/IdService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import { bindThis } from '@/decorators.js'; -import { DI } from '@/di-symbols.js'; +import { PushNotificationService } from '@/core/PushNotificationService.js'; import * as Acct from '@/misc/acct.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { AntennasRepository, UserListMembershipsRepository } from '@/models/_.js'; -import type { MiAntenna } from '@/models/Antenna.js'; -import type { MiNote } from '@/models/Note.js'; -import type { MiUser } from '@/models/User.js'; -import { CacheService } from './CacheService.js'; +import { DI } from '@/di-symbols.js'; +import type { MutingsRepository, NotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { bindThis } from '@/decorators.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class AntennaService implements OnApplicationShutdown { private antennasFetched: boolean; - private antennas: MiAntenna[]; + private antennas: Antenna[]; constructor( - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, + @Inject(DI.redis) + private redisClient: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis, + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, - private cacheService: CacheService, private utilityService: UtilityService, + private idService: IdService, private globalEventService: GlobalEventService, - private fanoutTimelineService: FanoutTimelineService, + private pushNotificationService: PushNotificationService, + private noteEntityService: NoteEntityService, + private antennaEntityService: AntennaEntityService, ) { this.antennasFetched = false; this.antennas = []; @@ -55,35 +60,21 @@ export class AntennaService implements OnApplicationShutdown { const obj = JSON.parse(data); if (obj.channel === 'internal') { - const { type, body } = obj.message as GlobalEvents['internal']['payload']; + const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'antennaCreated': - this.antennas.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい + this.antennas.push({ ...body, + createdAt: new Date(body.createdAt), lastUsedAt: new Date(body.lastUsedAt), - user: null, // joinなカラムは通常取ってこないので - userList: null, // joinなカラムは通常取ってこないので }); break; - case 'antennaUpdated': { - const idx = this.antennas.findIndex(a => a.id === body.id); - if (idx >= 0) { - this.antennas[idx] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい - ...body, - lastUsedAt: new Date(body.lastUsedAt), - user: null, // joinなカラムは通常取ってこないので - userList: null, // joinなカラムは通常取ってこないので - }; - } else { - // サーバ起動時にactiveじゃなかった場合、リストに持っていないので追加する必要あり - this.antennas.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい - ...body, - lastUsedAt: new Date(body.lastUsedAt), - user: null, // joinなカラムは通常取ってこないので - userList: null, // joinなカラムは通常取ってこないので - }); - } - } + case 'antennaUpdated': + this.antennas[this.antennas.findIndex(a => a.id === body.id)] = { + ...body, + createdAt: new Date(body.createdAt), + lastUsedAt: new Date(body.lastUsedAt), + }; break; case 'antennaDeleted': this.antennas = this.antennas.filter(a => a.id !== body.id); @@ -95,15 +86,20 @@ export class AntennaService implements OnApplicationShutdown { } @bindThis - public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise { + public async addNoteToAntennas(note: Note, noteUser: { id: User['id']; username: string; host: string | null; }): Promise { const antennas = await this.getAntennas(); const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const))); const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna); - const redisPipeline = this.redisForTimelines.pipeline(); + const redisPipeline = this.redisClient.pipeline(); for (const antenna of matchedAntennas) { - this.fanoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline); + redisPipeline.xadd( + `antennaTimeline:${antenna.id}`, + 'MAXLEN', '~', '200', + '*', + 'note', note.id); + this.globalEventService.publishAntennaStream(antenna.id, 'note', note); } @@ -113,77 +109,53 @@ export class AntennaService implements OnApplicationShutdown { // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている @bindThis - public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise { - if (antenna.excludeNotesInSensitiveChannel && note.channel?.isSensitive) return false; - - if (antenna.excludeBots && noteUser.isBot) return false; - - if (antenna.localOnly && noteUser.host != null) return false; - + public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise { + if (note.visibility === 'specified') return false; + if (note.visibility === 'followers') return false; + if (!antenna.withReplies && note.replyId != null) return false; - - if (note.visibility === 'specified') { - if (note.userId !== antenna.userId) { - if (note.visibleUserIds == null) return false; - if (!note.visibleUserIds.includes(antenna.userId)) return false; - } - } - - if (note.visibility === 'followers') { - const isFollowing = Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(antenna.userId), note.userId); - if (!isFollowing && antenna.userId !== note.userId) return false; - } - + if (antenna.src === 'home') { // TODO } else if (antenna.src === 'list') { - if (antenna.userListId == null) return false; - const exists = await this.userListMembershipsRepository.exists({ - where: { - userListId: antenna.userListId, - userId: note.userId, - }, - }); - if (!exists) return false; + const listUsers = (await this.userListJoiningsRepository.findBy({ + userListId: antenna.userListId!, + })).map(x => x.userId); + + if (!listUsers.includes(note.userId)) return false; } else if (antenna.src === 'users') { const accts = antenna.users.map(x => { const { username, host } = Acct.parse(x); return this.utilityService.getFullApAccount(username, host).toLowerCase(); }); if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false; - } else if (antenna.src === 'users_blacklist') { - const accts = antenna.users.map(x => { - const { username, host } = Acct.parse(x); - return this.utilityService.getFullApAccount(username, host).toLowerCase(); - }); - if (accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false; } - + const keywords = antenna.keywords // Clean up .map(xs => xs.filter(x => x !== '')) .filter(xs => xs.length > 0); - + if (keywords.length > 0) { if (note.text == null && note.cw == null) return false; const _text = (note.text ?? '') + '\n' + (note.cw ?? ''); - + const matched = keywords.some(and => and.every(keyword => antenna.caseSensitive ? _text.includes(keyword) : _text.toLowerCase().includes(keyword.toLowerCase()), )); - + if (!matched) return false; } - + const excludeKeywords = antenna.excludeKeywords // Clean up .map(xs => xs.filter(x => x !== '')) .filter(xs => xs.length > 0); - + if (excludeKeywords.length > 0) { if (note.text == null && note.cw == null) return false; @@ -195,16 +167,16 @@ export class AntennaService implements OnApplicationShutdown { ? _text.includes(keyword) : _text.toLowerCase().includes(keyword.toLowerCase()), )); - + if (matched) return false; } - + if (antenna.withFile) { if (note.fileIds && note.fileIds.length === 0) return false; } - + // TODO: eval expression - + return true; } @@ -216,45 +188,10 @@ export class AntennaService implements OnApplicationShutdown { }); this.antennasFetched = true; } - + return this.antennas; } - @bindThis - public async onMoveAccount(src: MiUser, dst: MiUser): Promise { - // There is a possibility for users to add the srcUser to their antennas, but it's low, so we don't check it. - - // Get MiAntenna[] from cache and filter to select antennas with the src user is in the users list - const srcUserAcct = this.utilityService.getFullApAccount(src.username, src.host).toLowerCase(); - const antennasToMigrate = (await this.getAntennas()).filter(antenna => { - return antenna.users.some(user => { - const { username, host } = Acct.parse(user); - return this.utilityService.getFullApAccount(username, host).toLowerCase() === srcUserAcct; - }); - }); - - if (antennasToMigrate.length === 0) return; - - const antennaIds = antennasToMigrate.map(x => x.id); - - // Update the antennas by appending dst users acct to the users list - const dstUserAcct = '@' + Acct.toString({ username: dst.username, host: dst.host }); - - await this.antennasRepository.createQueryBuilder('antenna') - .update() - .set({ - users: () => 'array_append(antenna.users, :dstUserAcct)', - }) - .where('antenna.id IN (:...antennaIds)', { antennaIds }) - .setParameters({ dstUserAcct }) - .execute(); - - // announce update to event - for (const newAntenna of await this.antennasRepository.findBy({ id: In(antennaIds) })) { - this.globalEventService.publishInternalEvent('antennaUpdated', newAntenna); - } - } - @bindThis public dispose(): void { this.redisForSub.off('message', this.onRedisMessage); diff --git a/packages/backend/src/core/AppLockService.ts b/packages/backend/src/core/AppLockService.ts index bd2749cb87..8dd805552b 100644 --- a/packages/backend/src/core/AppLockService.ts +++ b/packages/backend/src/core/AppLockService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { promisify } from 'node:util'; import { Inject, Injectable } from '@nestjs/common'; import redisLock from 'redis-lock'; @@ -37,6 +32,11 @@ export class AppLockService { return this.lock(`ap-object:${uri}`, timeout); } + @bindThis + public getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000): Promise<() => void> { + return this.lock(`instance:${host}`, timeout); + } + @bindThis public getChartInsertLock(lockKey: string, timeout = 30 * 1000): Promise<() => void> { return this.lock(`chart-insert:${lockKey}`, timeout); diff --git a/packages/backend/src/core/AvatarDecorationService.ts b/packages/backend/src/core/AvatarDecorationService.ts deleted file mode 100644 index 4efd6122b1..0000000000 --- a/packages/backend/src/core/AvatarDecorationService.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import type { AvatarDecorationsRepository, MiAvatarDecoration, MiUser } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { MemorySingleCache } from '@/misc/cache.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; - -@Injectable() -export class AvatarDecorationService implements OnApplicationShutdown { - public cache: MemorySingleCache; - - constructor( - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - - @Inject(DI.avatarDecorationsRepository) - private avatarDecorationsRepository: AvatarDecorationsRepository, - - private idService: IdService, - private moderationLogService: ModerationLogService, - private globalEventService: GlobalEventService, - ) { - this.cache = new MemorySingleCache(1000 * 60 * 30); // 30s - - this.redisForSub.on('message', this.onMessage); - } - - @bindThis - private async onMessage(_: string, data: string): Promise { - const obj = JSON.parse(data); - - if (obj.channel === 'internal') { - const { type, body } = obj.message as GlobalEvents['internal']['payload']; - switch (type) { - case 'avatarDecorationCreated': - case 'avatarDecorationUpdated': - case 'avatarDecorationDeleted': { - this.cache.delete(); - break; - } - default: - break; - } - } - } - - @bindThis - public async create(options: Partial, moderator?: MiUser): Promise { - const created = await this.avatarDecorationsRepository.insertOne({ - id: this.idService.gen(), - ...options, - }); - - this.globalEventService.publishInternalEvent('avatarDecorationCreated', created); - - if (moderator) { - this.moderationLogService.log(moderator, 'createAvatarDecoration', { - avatarDecorationId: created.id, - avatarDecoration: created, - }); - } - - return created; - } - - @bindThis - public async update(id: MiAvatarDecoration['id'], params: Partial, moderator?: MiUser): Promise { - const avatarDecoration = await this.avatarDecorationsRepository.findOneByOrFail({ id }); - - const date = new Date(); - await this.avatarDecorationsRepository.update(avatarDecoration.id, { - updatedAt: date, - ...params, - }); - - const updated = await this.avatarDecorationsRepository.findOneByOrFail({ id: avatarDecoration.id }); - this.globalEventService.publishInternalEvent('avatarDecorationUpdated', updated); - - if (moderator) { - this.moderationLogService.log(moderator, 'updateAvatarDecoration', { - avatarDecorationId: avatarDecoration.id, - before: avatarDecoration, - after: updated, - }); - } - } - - @bindThis - public async delete(id: MiAvatarDecoration['id'], moderator?: MiUser): Promise { - const avatarDecoration = await this.avatarDecorationsRepository.findOneByOrFail({ id }); - - await this.avatarDecorationsRepository.delete({ id: avatarDecoration.id }); - this.globalEventService.publishInternalEvent('avatarDecorationDeleted', avatarDecoration); - - if (moderator) { - this.moderationLogService.log(moderator, 'deleteAvatarDecoration', { - avatarDecorationId: avatarDecoration.id, - avatarDecoration: avatarDecoration, - }); - } - } - - @bindThis - public async getAll(noCache = false): Promise { - if (noCache) { - this.cache.delete(); - } - return this.cache.fetch(() => this.avatarDecorationsRepository.find()); - } - - @bindThis - public dispose(): void { - this.redisForSub.off('message', this.onMessage); - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); - } -} diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 6725ebe75b..2b7f9a48da 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -1,31 +1,27 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js'; +import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; -import type { MiLocalUser, MiUser } from '@/models/User.js'; +import type { LocalUser, User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class CacheService implements OnApplicationShutdown { - public userByIdCache: MemoryKVCache; - public localUserByNativeTokenCache: MemoryKVCache; - public localUserByIdCache: MemoryKVCache; - public uriPersonCache: MemoryKVCache; - public userProfileCache: RedisKVCache; + public userByIdCache: MemoryKVCache; + public localUserByNativeTokenCache: MemoryKVCache; + public localUserByIdCache: MemoryKVCache; + public uriPersonCache: MemoryKVCache; + public userProfileCache: RedisKVCache; public userMutingsCache: RedisKVCache>; public userBlockingCache: RedisKVCache>; public userBlockedCache: RedisKVCache>; // NOTE: 「被」Blockキャッシュ public renoteMutingsCache: RedisKVCache>; - public userFollowingsCache: RedisKVCache | undefined>>; + public userFollowingsCache: RedisKVCache>; + public userFollowingChannelsCache: RedisKVCache>; constructor( @Inject(DI.redis) @@ -52,16 +48,19 @@ export class CacheService implements OnApplicationShutdown { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + private userEntityService: UserEntityService, ) { //this.onMessage = this.onMessage.bind(this); - this.userByIdCache = new MemoryKVCache(1000 * 60 * 5); // 5m - this.localUserByNativeTokenCache = new MemoryKVCache(1000 * 60 * 5); // 5m - this.localUserByIdCache = new MemoryKVCache(1000 * 60 * 5); // 5m - this.uriPersonCache = new MemoryKVCache(1000 * 60 * 5); // 5m + this.userByIdCache = new MemoryKVCache(Infinity); + this.localUserByNativeTokenCache = new MemoryKVCache(Infinity); + this.localUserByIdCache = new MemoryKVCache(Infinity); + this.uriPersonCache = new MemoryKVCache(Infinity); - this.userProfileCache = new RedisKVCache(this.redisClient, 'userProfile', { + this.userProfileCache = new RedisKVCache(this.redisClient, 'userProfile', { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60, // 1m fetcher: (key) => this.userProfilesRepository.findOneByOrFail({ userId: key }), @@ -101,21 +100,21 @@ export class CacheService implements OnApplicationShutdown { fromRedisConverter: (value) => new Set(JSON.parse(value)), }); - this.userFollowingsCache = new RedisKVCache | undefined>>(this.redisClient, 'userFollowings', { + this.userFollowingsCache = new RedisKVCache>(this.redisClient, 'userFollowings', { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60, // 1m - fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId', 'withReplies'] }).then(xs => { - const obj: Record | undefined> = {}; - for (const x of xs) { - obj[x.followeeId] = { withReplies: x.withReplies }; - } - return obj; - }), - toRedisConverter: (value) => JSON.stringify(value), - fromRedisConverter: (value) => JSON.parse(value), + fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))), + toRedisConverter: (value) => JSON.stringify(Array.from(value)), + fromRedisConverter: (value) => new Set(JSON.parse(value)), }); - // NOTE: チャンネルのフォロー状況キャッシュはChannelFollowingServiceで行っている + this.userFollowingChannelsCache = new RedisKVCache>(this.redisClient, 'userFollowingChannels', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60, // 1m + fetcher: (key) => this.channelFollowingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))), + toRedisConverter: (value) => JSON.stringify(Array.from(value)), + fromRedisConverter: (value) => new Set(JSON.parse(value)), + }); this.redisForSub.on('message', this.onMessage); } @@ -125,37 +124,25 @@ export class CacheService implements OnApplicationShutdown { const obj = JSON.parse(data); if (obj.channel === 'internal') { - const { type, body } = obj.message as GlobalEvents['internal']['payload']; + const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'userChangeSuspendedState': - case 'userChangeDeletedState': - case 'remoteUserUpdated': - case 'localUserUpdated': { - const user = await this.usersRepository.findOneBy({ id: body.id }); - if (user == null) { - this.userByIdCache.delete(body.id); - this.localUserByIdCache.delete(body.id); - for (const [k, v] of this.uriPersonCache.entries) { - if (v.value?.id === body.id) { - this.uriPersonCache.delete(k); - } - } - } else { - this.userByIdCache.set(user.id, user); - for (const [k, v] of this.uriPersonCache.entries) { - if (v.value?.id === user.id) { - this.uriPersonCache.set(k, user); - } - } - if (this.userEntityService.isLocalUser(user)) { - this.localUserByNativeTokenCache.set(user.token!, user); - this.localUserByIdCache.set(user.id, user); + case 'remoteUserUpdated': { + const user = await this.usersRepository.findOneByOrFail({ id: body.id }); + this.userByIdCache.set(user.id, user); + for (const [k, v] of this.uriPersonCache.cache.entries()) { + if (v.value?.id === user.id) { + this.uriPersonCache.set(k, user); } } + if (this.userEntityService.isLocalUser(user)) { + this.localUserByNativeTokenCache.set(user.token!, user); + this.localUserByIdCache.set(user.id, user); + } break; } case 'userTokenRegenerated': { - const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as MiLocalUser; + const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser; this.localUserByNativeTokenCache.delete(body.oldToken); this.localUserByNativeTokenCache.set(body.newToken, user); break; @@ -165,7 +152,6 @@ export class CacheService implements OnApplicationShutdown { if (follower) follower.followingCount++; const followee = this.userByIdCache.get(body.followeeId); if (followee) followee.followersCount++; - this.userFollowingsCache.delete(body.followerId); break; } default: @@ -175,7 +161,7 @@ export class CacheService implements OnApplicationShutdown { } @bindThis - public findUserById(userId: MiUser['id']) { + public findUserById(userId: User['id']) { return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId })); } @@ -192,6 +178,7 @@ export class CacheService implements OnApplicationShutdown { this.userBlockedCache.dispose(); this.renoteMutingsCache.dispose(); this.userFollowingsCache.dispose(); + this.userFollowingChannelsCache.dispose(); } @bindThis diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index ee081f29b0..1a52a229c5 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -1,70 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; -import { MiMeta } from '@/models/Meta.js'; -import Logger from '@/logger.js'; -import { LoggerService } from './LoggerService.js'; - -export const supportedCaptchaProviders = ['none', 'hcaptcha', 'mcaptcha', 'recaptcha', 'turnstile', 'testcaptcha'] as const; -export type CaptchaProvider = typeof supportedCaptchaProviders[number]; - -export const captchaErrorCodes = { - invalidProvider: Symbol('invalidProvider'), - invalidParameters: Symbol('invalidParameters'), - noResponseProvided: Symbol('noResponseProvided'), - requestFailed: Symbol('requestFailed'), - verificationFailed: Symbol('verificationFailed'), - unknown: Symbol('unknown'), -} as const; -export type CaptchaErrorCode = typeof captchaErrorCodes[keyof typeof captchaErrorCodes]; - -export type CaptchaSetting = { - provider: CaptchaProvider; - hcaptcha: { - siteKey: string | null; - secretKey: string | null; - } - mcaptcha: { - siteKey: string | null; - secretKey: string | null; - instanceUrl: string | null; - } - recaptcha: { - siteKey: string | null; - secretKey: string | null; - } - turnstile: { - siteKey: string | null; - secretKey: string | null; - } -}; - -export class CaptchaError extends Error { - public readonly code: CaptchaErrorCode; - public readonly cause?: unknown; - - constructor(code: CaptchaErrorCode, message: string, cause?: unknown) { - super(message); - this.code = code; - this.cause = cause; - this.name = 'CaptchaError'; - } -} - -export type CaptchaSaveSuccess = { - success: true; -}; -export type CaptchaSaveFailure = { - success: false; - error: CaptchaError; -}; -export type CaptchaSaveResult = CaptchaSaveSuccess | CaptchaSaveFailure; type CaptchaResponse = { success: boolean; @@ -73,14 +9,9 @@ type CaptchaResponse = { @Injectable() export class CaptchaService { - private readonly logger: Logger; - constructor( private httpRequestService: HttpRequestService, - private metaService: MetaService, - loggerService: LoggerService, ) { - this.logger = loggerService.getLogger('captcha'); } @bindThis @@ -89,7 +20,7 @@ export class CaptchaService { secret, response, }); - + const res = await this.httpRequestService.send(url, { method: 'POST', body: params.toString(), @@ -97,309 +28,60 @@ export class CaptchaService { 'Content-Type': 'application/x-www-form-urlencoded', }, }, { throwErrorWhenResponseNotOk: false }); - + if (!res.ok) { throw new Error(`${res.status}`); } - + return await res.json() as CaptchaResponse; - } - + } + @bindThis public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise { if (response == null) { - throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'recaptcha-failed: no response provided'); + throw new Error('recaptcha-failed: no response provided'); } const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(err => { - throw new CaptchaError(captchaErrorCodes.requestFailed, `recaptcha-request-failed: ${err}`); + throw new Error(`recaptcha-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw new CaptchaError(captchaErrorCodes.verificationFailed, `recaptcha-failed: ${errorCodes}`); + throw new Error(`recaptcha-failed: ${errorCodes}`); } } @bindThis public async verifyHcaptcha(secret: string, response: string | null | undefined): Promise { if (response == null) { - throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'hcaptcha-failed: no response provided'); + throw new Error('hcaptcha-failed: no response provided'); } const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => { - throw new CaptchaError(captchaErrorCodes.requestFailed, `hcaptcha-request-failed: ${err}`); + throw new Error(`hcaptcha-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw new CaptchaError(captchaErrorCodes.verificationFailed, `hcaptcha-failed: ${errorCodes}`); - } - } - - // https://codeberg.org/Gusted/mCaptcha/src/branch/main/mcaptcha.go - @bindThis - public async verifyMcaptcha(secret: string, siteKey: string, instanceHost: string, response: string | null | undefined): Promise { - if (response == null) { - throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'mcaptcha-failed: no response provided'); - } - - const endpointUrl = new URL('/api/v1/pow/siteverify', instanceHost); - const result = await this.httpRequestService.send(endpointUrl.toString(), { - method: 'POST', - body: JSON.stringify({ - key: siteKey, - secret: secret, - token: response, - }), - headers: { - 'Content-Type': 'application/json', - }, - }, { throwErrorWhenResponseNotOk: false }); - - if (result.status !== 200) { - throw new CaptchaError(captchaErrorCodes.requestFailed, 'mcaptcha-failed: mcaptcha didn\'t return 200 OK'); - } - - const resp = (await result.json()) as { valid: boolean }; - - if (!resp.valid) { - throw new CaptchaError(captchaErrorCodes.verificationFailed, 'mcaptcha-request-failed'); + throw new Error(`hcaptcha-failed: ${errorCodes}`); } } @bindThis public async verifyTurnstile(secret: string, response: string | null | undefined): Promise { if (response == null) { - throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'turnstile-failed: no response provided'); + throw new Error('turnstile-failed: no response provided'); } - + const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => { - throw new CaptchaError(captchaErrorCodes.requestFailed, `turnstile-request-failed: ${err}`); + throw new Error(`turnstile-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw new CaptchaError(captchaErrorCodes.verificationFailed, `turnstile-failed: ${errorCodes}`); + throw new Error(`turnstile-failed: ${errorCodes}`); } } - - @bindThis - public async verifyTestcaptcha(response: string | null | undefined): Promise { - if (response == null) { - throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'testcaptcha-failed: no response provided'); - } - - const success = response === 'testcaptcha-passed'; - - if (!success) { - throw new CaptchaError(captchaErrorCodes.verificationFailed, 'testcaptcha-failed'); - } - } - - @bindThis - public async get(): Promise { - const meta = await this.metaService.fetch(true); - - let provider: CaptchaProvider; - switch (true) { - case meta.enableHcaptcha: { - provider = 'hcaptcha'; - break; - } - case meta.enableMcaptcha: { - provider = 'mcaptcha'; - break; - } - case meta.enableRecaptcha: { - provider = 'recaptcha'; - break; - } - case meta.enableTurnstile: { - provider = 'turnstile'; - break; - } - case meta.enableTestcaptcha: { - provider = 'testcaptcha'; - break; - } - default: { - provider = 'none'; - break; - } - } - - return { - provider: provider, - hcaptcha: { - siteKey: meta.hcaptchaSiteKey, - secretKey: meta.hcaptchaSecretKey, - }, - mcaptcha: { - siteKey: meta.mcaptchaSitekey, - secretKey: meta.mcaptchaSecretKey, - instanceUrl: meta.mcaptchaInstanceUrl, - }, - recaptcha: { - siteKey: meta.recaptchaSiteKey, - secretKey: meta.recaptchaSecretKey, - }, - turnstile: { - siteKey: meta.turnstileSiteKey, - secretKey: meta.turnstileSecretKey, - }, - }; - } - - /** - * captchaの設定を更新します. その際、フロントエンド側で受け取ったcaptchaからの戻り値を検証し、passした場合のみ設定を更新します. - * 実際の検証処理はサービス内で定義されている各captchaプロバイダの検証関数に委譲します. - * - * @param provider 検証するcaptchaのプロバイダ - * @param params - * @param params.sitekey hcaptcha, recaptcha, turnstile, mcaptchaの場合に指定するsitekey. それ以外のプロバイダでは無視されます - * @param params.secret hcaptcha, recaptcha, turnstile, mcaptchaの場合に指定するsecret. それ以外のプロバイダでは無視されます - * @param params.instanceUrl mcaptchaの場合に指定するインスタンスのURL. それ以外のプロバイダでは無視されます - * @param params.captchaResult フロントエンド側で受け取ったcaptchaプロバイダからの戻り値. この値を使ってサーバサイドでの検証を行います - * @see verifyHcaptcha - * @see verifyMcaptcha - * @see verifyRecaptcha - * @see verifyTurnstile - * @see verifyTestcaptcha - */ - @bindThis - public async save( - provider: CaptchaProvider, - params?: { - sitekey?: string | null; - secret?: string | null; - instanceUrl?: string | null; - captchaResult?: string | null; - }, - ): Promise { - if (!supportedCaptchaProviders.includes(provider)) { - return { - success: false, - error: new CaptchaError(captchaErrorCodes.invalidProvider, `Invalid captcha provider: ${provider}`), - }; - } - - const operation = { - none: async () => { - await this.updateMeta(provider, params); - }, - hcaptcha: async () => { - if (!params?.secret || !params.captchaResult) { - throw new CaptchaError(captchaErrorCodes.invalidParameters, 'hcaptcha-failed: secret and captureResult are required'); - } - - await this.verifyHcaptcha(params.secret, params.captchaResult); - await this.updateMeta(provider, params); - }, - mcaptcha: async () => { - if (!params?.secret || !params.sitekey || !params.instanceUrl || !params.captchaResult) { - throw new CaptchaError(captchaErrorCodes.invalidParameters, 'mcaptcha-failed: secret, sitekey, instanceUrl and captureResult are required'); - } - - await this.verifyMcaptcha(params.secret, params.sitekey, params.instanceUrl, params.captchaResult); - await this.updateMeta(provider, params); - }, - recaptcha: async () => { - if (!params?.secret || !params.captchaResult) { - throw new CaptchaError(captchaErrorCodes.invalidParameters, 'recaptcha-failed: secret and captureResult are required'); - } - - await this.verifyRecaptcha(params.secret, params.captchaResult); - await this.updateMeta(provider, params); - }, - turnstile: async () => { - if (!params?.secret || !params.captchaResult) { - throw new CaptchaError(captchaErrorCodes.invalidParameters, 'turnstile-failed: secret and captureResult are required'); - } - - await this.verifyTurnstile(params.secret, params.captchaResult); - await this.updateMeta(provider, params); - }, - testcaptcha: async () => { - if (!params?.captchaResult) { - throw new CaptchaError(captchaErrorCodes.invalidParameters, 'turnstile-failed: captureResult are required'); - } - - await this.verifyTestcaptcha(params.captchaResult); - await this.updateMeta(provider, params); - }, - }[provider]; - - return operation() - .then(() => ({ success: true }) as CaptchaSaveSuccess) - .catch(err => { - this.logger.info(err); - const error = err instanceof CaptchaError - ? err - : new CaptchaError(captchaErrorCodes.unknown, `unknown error: ${err}`); - return { - success: false, - error, - }; - }); - } - - @bindThis - private async updateMeta( - provider: CaptchaProvider, - params?: { - sitekey?: string | null; - secret?: string | null; - instanceUrl?: string | null; - }, - ) { - const metaPartial: Partial< - Pick< - MiMeta, - ('enableHcaptcha' | 'hcaptchaSiteKey' | 'hcaptchaSecretKey') | - ('enableMcaptcha' | 'mcaptchaSitekey' | 'mcaptchaSecretKey' | 'mcaptchaInstanceUrl') | - ('enableRecaptcha' | 'recaptchaSiteKey' | 'recaptchaSecretKey') | - ('enableTurnstile' | 'turnstileSiteKey' | 'turnstileSecretKey') | - ('enableTestcaptcha') - > - > = { - enableHcaptcha: provider === 'hcaptcha', - enableMcaptcha: provider === 'mcaptcha', - enableRecaptcha: provider === 'recaptcha', - enableTurnstile: provider === 'turnstile', - enableTestcaptcha: provider === 'testcaptcha', - }; - - const updateIfNotUndefined = (key: K, value: typeof metaPartial[K]) => { - if (value !== undefined) { - metaPartial[key] = value; - } - }; - switch (provider) { - case 'hcaptcha': { - updateIfNotUndefined('hcaptchaSiteKey', params?.sitekey); - updateIfNotUndefined('hcaptchaSecretKey', params?.secret); - break; - } - case 'mcaptcha': { - updateIfNotUndefined('mcaptchaSitekey', params?.sitekey); - updateIfNotUndefined('mcaptchaSecretKey', params?.secret); - updateIfNotUndefined('mcaptchaInstanceUrl', params?.instanceUrl); - break; - } - case 'recaptcha': { - updateIfNotUndefined('recaptchaSiteKey', params?.sitekey); - updateIfNotUndefined('recaptchaSecretKey', params?.secret); - break; - } - case 'turnstile': { - updateIfNotUndefined('turnstileSiteKey', params?.sitekey); - updateIfNotUndefined('turnstileSecretKey', params?.secret); - break; - } - } - - await this.metaService.update(metaPartial); - } } diff --git a/packages/backend/src/core/ChannelFollowingService.ts b/packages/backend/src/core/ChannelFollowingService.ts deleted file mode 100644 index 12251595e2..0000000000 --- a/packages/backend/src/core/ChannelFollowingService.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; -import Redis from 'ioredis'; -import { DI } from '@/di-symbols.js'; -import type { ChannelFollowingsRepository } from '@/models/_.js'; -import { MiChannel } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js'; -import { bindThis } from '@/decorators.js'; -import type { MiLocalUser } from '@/models/User.js'; -import { RedisKVCache } from '@/misc/cache.js'; - -@Injectable() -export class ChannelFollowingService implements OnModuleInit { - public userFollowingChannelsCache: RedisKVCache>; - - constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - @Inject(DI.channelFollowingsRepository) - private channelFollowingsRepository: ChannelFollowingsRepository, - private idService: IdService, - private globalEventService: GlobalEventService, - ) { - this.userFollowingChannelsCache = new RedisKVCache>(this.redisClient, 'userFollowingChannels', { - lifetime: 1000 * 60 * 30, // 30m - memoryCacheLifetime: 1000 * 60, // 1m - fetcher: (key) => this.channelFollowingsRepository.find({ - where: { followerId: key }, - select: ['followeeId'], - }).then(xs => new Set(xs.map(x => x.followeeId))), - toRedisConverter: (value) => JSON.stringify(Array.from(value)), - fromRedisConverter: (value) => new Set(JSON.parse(value)), - }); - - this.redisForSub.on('message', this.onMessage); - } - - onModuleInit() { - } - - @bindThis - public async follow( - requestUser: MiLocalUser, - targetChannel: MiChannel, - ): Promise { - await this.channelFollowingsRepository.insert({ - id: this.idService.gen(), - followerId: requestUser.id, - followeeId: targetChannel.id, - }); - - this.globalEventService.publishInternalEvent('followChannel', { - userId: requestUser.id, - channelId: targetChannel.id, - }); - } - - @bindThis - public async unfollow( - requestUser: MiLocalUser, - targetChannel: MiChannel, - ): Promise { - await this.channelFollowingsRepository.delete({ - followerId: requestUser.id, - followeeId: targetChannel.id, - }); - - this.globalEventService.publishInternalEvent('unfollowChannel', { - userId: requestUser.id, - channelId: targetChannel.id, - }); - } - - @bindThis - private async onMessage(_: string, data: string): Promise { - const obj = JSON.parse(data); - - if (obj.channel === 'internal') { - const { type, body } = obj.message as GlobalEvents['internal']['payload']; - switch (type) { - case 'followChannel': { - this.userFollowingChannelsCache.refresh(body.userId); - break; - } - case 'unfollowChannel': { - this.userFollowingChannelsCache.delete(body.userId); - break; - } - } - } - } - - @bindThis - public dispose(): void { - this.userFollowingChannelsCache.dispose(); - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); - } -} diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts deleted file mode 100644 index 4e81847a52..0000000000 --- a/packages/backend/src/core/ChatService.ts +++ /dev/null @@ -1,945 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import { Brackets } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; -import { QueueService } from '@/core/QueueService.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; -import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; -import { PushNotificationService } from '@/core/PushNotificationService.js'; -import { bindThis } from '@/decorators.js'; -import type { ChatApprovalsRepository, ChatMessagesRepository, ChatRoomInvitationsRepository, ChatRoomMembershipsRepository, ChatRoomsRepository, MiChatMessage, MiChatRoom, MiChatRoomMembership, MiDriveFile, MiUser, MutingsRepository, UsersRepository } from '@/models/_.js'; -import { UserBlockingService } from '@/core/UserBlockingService.js'; -import { QueryService } from '@/core/QueryService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { UserFollowingService } from '@/core/UserFollowingService.js'; -import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js'; -import { Packed } from '@/misc/json-schema.js'; -import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; -import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import { emojiRegex } from '@/misc/emoji-regex.js'; -import { NotificationService } from '@/core/NotificationService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; - -const MAX_ROOM_MEMBERS = 50; -const MAX_REACTIONS_PER_MESSAGE = 100; -const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/; - -// TODO: ReactionServiceのやつと共通化 -function normalizeEmojiString(x: string) { - const match = emojiRegex.exec(x); - if (match) { - // 合字を含む1つの絵文字 - const unicode = match[0]; - - // 異体字セレクタ除去 - return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, ''); - } else { - throw new Error('invalid emoji'); - } -} - -@Injectable() -export class ChatService { - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.redis) - private redisClient: Redis.Redis, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.chatMessagesRepository) - private chatMessagesRepository: ChatMessagesRepository, - - @Inject(DI.chatApprovalsRepository) - private chatApprovalsRepository: ChatApprovalsRepository, - - @Inject(DI.chatRoomsRepository) - private chatRoomsRepository: ChatRoomsRepository, - - @Inject(DI.chatRoomInvitationsRepository) - private chatRoomInvitationsRepository: ChatRoomInvitationsRepository, - - @Inject(DI.chatRoomMembershipsRepository) - private chatRoomMembershipsRepository: ChatRoomMembershipsRepository, - - @Inject(DI.mutingsRepository) - private mutingsRepository: MutingsRepository, - - private userEntityService: UserEntityService, - private chatEntityService: ChatEntityService, - private idService: IdService, - private globalEventService: GlobalEventService, - private apRendererService: ApRendererService, - private queueService: QueueService, - private pushNotificationService: PushNotificationService, - private notificationService: NotificationService, - private userBlockingService: UserBlockingService, - private queryService: QueryService, - private roleService: RoleService, - private userFollowingService: UserFollowingService, - private customEmojiService: CustomEmojiService, - private moderationLogService: ModerationLogService, - ) { - } - - @bindThis - public async getChatAvailability(userId: MiUser['id']): Promise<{ read: boolean; write: boolean; }> { - const policies = await this.roleService.getUserPolicies(userId); - - switch (policies.chatAvailability) { - case 'available': - return { - read: true, - write: true, - }; - case 'readonly': - return { - read: true, - write: false, - }; - case 'unavailable': - return { - read: false, - write: false, - }; - default: - throw new Error('invalid chat availability (unreachable)'); - } - } - - /** getChatAvailabilityの糖衣。主にAPI呼び出し時に走らせて、権限的に問題ない場合はそのまま続行する */ - @bindThis - public async checkChatAvailability(userId: MiUser['id'], permission: 'read' | 'write') { - const policy = await this.getChatAvailability(userId); - if (policy[permission] === false) { - throw new Error('ROLE_PERMISSION_DENIED'); - } - } - - @bindThis - public async createMessageToUser(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toUser: MiUser, params: { - text?: string | null; - file?: MiDriveFile | null; - uri?: string | null; - }): Promise> { - if (fromUser.id === toUser.id) { - throw new Error('yourself'); - } - - const approvals = await this.chatApprovalsRepository.createQueryBuilder('approval') - .where(new Brackets(qb => { // 自分が相手を許可しているか - qb.where('approval.userId = :fromUserId', { fromUserId: fromUser.id }) - .andWhere('approval.otherId = :toUserId', { toUserId: toUser.id }); - })) - .orWhere(new Brackets(qb => { // 相手が自分を許可しているか - qb.where('approval.userId = :toUserId', { toUserId: toUser.id }) - .andWhere('approval.otherId = :fromUserId', { fromUserId: fromUser.id }); - })) - .take(2) - .getMany(); - - const otherApprovedMe = approvals.some(approval => approval.userId === toUser.id); - const iApprovedOther = approvals.some(approval => approval.userId === fromUser.id); - - if (!otherApprovedMe) { - if (toUser.chatScope === 'none') { - throw new Error('recipient is cannot chat (none)'); - } else if (toUser.chatScope === 'followers') { - const isFollower = await this.userFollowingService.isFollowing(fromUser.id, toUser.id); - if (!isFollower) { - throw new Error('recipient is cannot chat (followers)'); - } - } else if (toUser.chatScope === 'following') { - const isFollowing = await this.userFollowingService.isFollowing(toUser.id, fromUser.id); - if (!isFollowing) { - throw new Error('recipient is cannot chat (following)'); - } - } else if (toUser.chatScope === 'mutual') { - const isMutual = await this.userFollowingService.isMutual(fromUser.id, toUser.id); - if (!isMutual) { - throw new Error('recipient is cannot chat (mutual)'); - } - } - } - - if (!(await this.getChatAvailability(toUser.id)).write) { - throw new Error('recipient is cannot chat (policy)'); - } - - const blocked = await this.userBlockingService.checkBlocked(toUser.id, fromUser.id); - if (blocked) { - throw new Error('blocked'); - } - - const message = { - id: this.idService.gen(), - fromUserId: fromUser.id, - toUserId: toUser.id, - text: params.text ? params.text.trim() : null, - fileId: params.file ? params.file.id : null, - reads: [], - uri: params.uri ?? null, - } satisfies Partial; - - const inserted = await this.chatMessagesRepository.insertOne(message); - - // 相手を許可しておく - if (!iApprovedOther) { - this.chatApprovalsRepository.insertOne({ - id: this.idService.gen(), - userId: fromUser.id, - otherId: toUser.id, - }); - } - - const packedMessage = await this.chatEntityService.packMessageLiteFor1on1(inserted); - - if (this.userEntityService.isLocalUser(toUser)) { - const redisPipeline = this.redisClient.pipeline(); - redisPipeline.set(`newUserChatMessageExists:${toUser.id}:${fromUser.id}`, message.id); - redisPipeline.sadd(`newChatMessagesExists:${toUser.id}`, `user:${fromUser.id}`); - redisPipeline.exec(); - } - - if (this.userEntityService.isLocalUser(fromUser)) { - // 自分のストリーム - this.globalEventService.publishChatUserStream(fromUser.id, toUser.id, 'message', packedMessage); - } - - if (this.userEntityService.isLocalUser(toUser)) { - // 相手のストリーム - this.globalEventService.publishChatUserStream(toUser.id, fromUser.id, 'message', packedMessage); - } - - // 3秒経っても既読にならなかったらイベント発行 - if (this.userEntityService.isLocalUser(toUser)) { - setTimeout(async () => { - const marker = await this.redisClient.get(`newUserChatMessageExists:${toUser.id}:${fromUser.id}`); - - if (marker == null) return; // 既読 - - const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted, toUser); - this.globalEventService.publishMainStream(toUser.id, 'newChatMessage', packedMessageForTo); - this.pushNotificationService.pushNotification(toUser.id, 'newChatMessage', packedMessageForTo); - }, 3000); - } - - return packedMessage; - } - - @bindThis - public async createMessageToRoom(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toRoom: MiChatRoom, params: { - text?: string | null; - file?: MiDriveFile | null; - uri?: string | null; - }): Promise> { - const memberships = (await this.chatRoomMembershipsRepository.findBy({ roomId: toRoom.id })).map(m => ({ - userId: m.userId, - isMuted: m.isMuted, - })).concat({ // ownerはmembershipレコードを作らないため - userId: toRoom.ownerId, - isMuted: false, - }); - - if (!memberships.some(member => member.userId === fromUser.id)) { - throw new Error('you are not a member of the room'); - } - - const membershipsOtherThanMe = memberships.filter(member => member.userId !== fromUser.id); - - const message = { - id: this.idService.gen(), - fromUserId: fromUser.id, - toRoomId: toRoom.id, - text: params.text ? params.text.trim() : null, - fileId: params.file ? params.file.id : null, - reads: [], - uri: params.uri ?? null, - } satisfies Partial; - - const inserted = await this.chatMessagesRepository.insertOne(message); - - const packedMessage = await this.chatEntityService.packMessageLiteForRoom(inserted); - - this.globalEventService.publishChatRoomStream(toRoom.id, 'message', packedMessage); - - const redisPipeline = this.redisClient.pipeline(); - for (const membership of membershipsOtherThanMe) { - if (membership.isMuted) continue; - - redisPipeline.set(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`, message.id); - redisPipeline.sadd(`newChatMessagesExists:${membership.userId}`, `room:${toRoom.id}`); - } - redisPipeline.exec(); - - // 3秒経っても既読にならなかったらイベント発行 - setTimeout(async () => { - const redisPipeline = this.redisClient.pipeline(); - for (const membership of membershipsOtherThanMe) { - redisPipeline.get(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`); - } - const markers = await redisPipeline.exec(); - if (markers == null) throw new Error('redis error'); - - if (markers.every(marker => marker[1] == null)) return; - - const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted); - - for (let i = 0; i < membershipsOtherThanMe.length; i++) { - const marker = markers[i][1]; - if (marker == null) continue; - - this.globalEventService.publishMainStream(membershipsOtherThanMe[i].userId, 'newChatMessage', packedMessageForTo); - this.pushNotificationService.pushNotification(membershipsOtherThanMe[i].userId, 'newChatMessage', packedMessageForTo); - } - }, 3000); - - return packedMessage; - } - - @bindThis - public async readUserChatMessage( - readerId: MiUser['id'], - senderId: MiUser['id'], - ): Promise { - const redisPipeline = this.redisClient.pipeline(); - redisPipeline.del(`newUserChatMessageExists:${readerId}:${senderId}`); - redisPipeline.srem(`newChatMessagesExists:${readerId}`, `user:${senderId}`); - await redisPipeline.exec(); - } - - @bindThis - public async readRoomChatMessage( - readerId: MiUser['id'], - roomId: MiChatRoom['id'], - ): Promise { - const redisPipeline = this.redisClient.pipeline(); - redisPipeline.del(`newRoomChatMessageExists:${readerId}:${roomId}`); - redisPipeline.srem(`newChatMessagesExists:${readerId}`, `room:${roomId}`); - await redisPipeline.exec(); - } - - @bindThis - public findMessageById(messageId: MiChatMessage['id']) { - return this.chatMessagesRepository.findOneBy({ id: messageId }); - } - - @bindThis - public findMyMessageById(userId: MiUser['id'], messageId: MiChatMessage['id']) { - return this.chatMessagesRepository.findOneBy({ id: messageId, fromUserId: userId }); - } - - @bindThis - public async hasPermissionToViewRoomTimeline(meId: MiUser['id'], room: MiChatRoom) { - if (await this.isRoomMember(room, meId)) { - return true; - } else { - const iAmModerator = await this.roleService.isModerator({ id: meId }); - if (iAmModerator) { - return true; - } - - return false; - } - } - - @bindThis - public async deleteMessage(message: MiChatMessage) { - await this.chatMessagesRepository.delete(message.id); - - if (message.toUserId) { - const [fromUser, toUser] = await Promise.all([ - this.usersRepository.findOneByOrFail({ id: message.fromUserId }), - this.usersRepository.findOneByOrFail({ id: message.toUserId }), - ]); - - if (this.userEntityService.isLocalUser(fromUser)) this.globalEventService.publishChatUserStream(message.fromUserId, message.toUserId, 'deleted', message.id); - if (this.userEntityService.isLocalUser(toUser)) this.globalEventService.publishChatUserStream(message.toUserId, message.fromUserId, 'deleted', message.id); - - if (this.userEntityService.isLocalUser(fromUser) && this.userEntityService.isRemoteUser(toUser)) { - //const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), fromUser)); - //this.queueService.deliver(fromUser, activity, toUser.inbox); - } - } else if (message.toRoomId) { - this.globalEventService.publishChatRoomStream(message.toRoomId, 'deleted', message.id); - } - } - - @bindThis - public async userTimeline(meId: MiUser['id'], otherId: MiUser['id'], limit: number, sinceId?: MiChatMessage['id'] | null, untilId?: MiChatMessage['id'] | null) { - const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), sinceId, untilId) - .andWhere(new Brackets(qb => { - qb - .where(new Brackets(qb => { - qb - .where('message.fromUserId = :meId') - .andWhere('message.toUserId = :otherId'); - })) - .orWhere(new Brackets(qb => { - qb - .where('message.fromUserId = :otherId') - .andWhere('message.toUserId = :meId'); - })); - })) - .setParameter('meId', meId) - .setParameter('otherId', otherId); - - const messages = await query.take(limit).getMany(); - - return messages; - } - - @bindThis - public async roomTimeline(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatMessage['id'] | null, untilId?: MiChatMessage['id'] | null) { - const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), sinceId, untilId) - .andWhere('message.toRoomId = :roomId', { roomId }) - .leftJoinAndSelect('message.file', 'file') - .leftJoinAndSelect('message.fromUser', 'fromUser'); - - const messages = await query.take(limit).getMany(); - - return messages; - } - - @bindThis - public async userHistory(meId: MiUser['id'], limit: number): Promise { - const history: MiChatMessage[] = []; - - const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') - .select('muting.muteeId') - .where('muting.muterId = :muterId', { muterId: meId }); - - for (let i = 0; i < limit; i++) { - const found = history.map(m => (m.fromUserId === meId) ? m.toUserId! : m.fromUserId!); - - const query = this.chatMessagesRepository.createQueryBuilder('message') - .orderBy('message.id', 'DESC') - .where(new Brackets(qb => { - qb - .where('message.fromUserId = :meId', { meId: meId }) - .orWhere('message.toUserId = :meId', { meId: meId }); - })) - .andWhere('message.toRoomId IS NULL') - .andWhere(`message.fromUserId NOT IN (${ mutingQuery.getQuery() })`) - .andWhere(`message.toUserId NOT IN (${ mutingQuery.getQuery() })`); - - if (found.length > 0) { - query.andWhere('message.fromUserId NOT IN (:...found)', { found: found }); - query.andWhere('message.toUserId NOT IN (:...found)', { found: found }); - } - - query.setParameters(mutingQuery.getParameters()); - - const message = await query.getOne(); - - if (message) { - history.push(message); - } else { - break; - } - } - - return history; - } - - @bindThis - public async roomHistory(meId: MiUser['id'], limit: number): Promise { - // TODO: 一回のクエリにまとめられるかも - const [memberRoomIds, ownedRoomIds] = await Promise.all([ - this.chatRoomMembershipsRepository.findBy({ - userId: meId, - }).then(xs => xs.map(x => x.roomId)), - this.chatRoomsRepository.findBy({ - ownerId: meId, - }).then(xs => xs.map(x => x.id)), - ]); - - const roomIds = memberRoomIds.concat(ownedRoomIds); - - if (memberRoomIds.length === 0 && ownedRoomIds.length === 0) { - return []; - } - - const history: MiChatMessage[] = []; - - for (let i = 0; i < limit; i++) { - const found = history.map(m => m.toRoomId!); - - const query = this.chatMessagesRepository.createQueryBuilder('message') - .orderBy('message.id', 'DESC') - .where('message.toRoomId IN (:...roomIds)', { roomIds }); - - if (found.length > 0) { - query.andWhere('message.toRoomId NOT IN (:...found)', { found: found }); - } - - const message = await query.getOne(); - - if (message) { - history.push(message); - } else { - break; - } - } - - return history; - } - - @bindThis - public async getUserReadStateMap(userId: MiUser['id'], otherIds: MiUser['id'][]) { - const readStateMap: Record = {}; - - const redisPipeline = this.redisClient.pipeline(); - - for (const otherId of otherIds) { - redisPipeline.get(`newUserChatMessageExists:${userId}:${otherId}`); - } - - const markers = await redisPipeline.exec(); - if (markers == null) throw new Error('redis error'); - - for (let i = 0; i < otherIds.length; i++) { - const marker = markers[i][1]; - readStateMap[otherIds[i]] = marker == null; - } - - return readStateMap; - } - - @bindThis - public async getRoomReadStateMap(userId: MiUser['id'], roomIds: MiChatRoom['id'][]) { - const readStateMap: Record = {}; - - const redisPipeline = this.redisClient.pipeline(); - - for (const roomId of roomIds) { - redisPipeline.get(`newRoomChatMessageExists:${userId}:${roomId}`); - } - - const markers = await redisPipeline.exec(); - if (markers == null) throw new Error('redis error'); - - for (let i = 0; i < roomIds.length; i++) { - const marker = markers[i][1]; - readStateMap[roomIds[i]] = marker == null; - } - - return readStateMap; - } - - @bindThis - public async hasUnreadMessages(userId: MiUser['id']) { - const card = await this.redisClient.scard(`newChatMessagesExists:${userId}`); - return card > 0; - } - - @bindThis - public async createRoom(owner: MiUser, params: Partial<{ - name: string; - description: string; - }>) { - const room = { - id: this.idService.gen(), - name: params.name, - description: params.description, - ownerId: owner.id, - } satisfies Partial; - - const created = await this.chatRoomsRepository.insertOne(room); - - return created; - } - - @bindThis - public async hasPermissionToDeleteRoom(meId: MiUser['id'], room: MiChatRoom) { - if (room.ownerId === meId) { - return true; - } - - const iAmModerator = await this.roleService.isModerator({ id: meId }); - if (iAmModerator) { - return true; - } - - return false; - } - - @bindThis - public async deleteRoom(room: MiChatRoom, deleter?: MiUser) { - const memberships = (await this.chatRoomMembershipsRepository.findBy({ roomId: room.id })).map(m => ({ - userId: m.userId, - })).concat({ // ownerはmembershipレコードを作らないため - userId: room.ownerId, - }); - - // 未読フラグ削除 - const redisPipeline = this.redisClient.pipeline(); - for (const membership of memberships) { - redisPipeline.del(`newRoomChatMessageExists:${membership.userId}:${room.id}`); - redisPipeline.srem(`newChatMessagesExists:${membership.userId}`, `room:${room.id}`); - } - await redisPipeline.exec(); - - await this.chatRoomsRepository.delete(room.id); - - if (deleter) { - const deleterIsModerator = await this.roleService.isModerator(deleter); - - if (deleterIsModerator) { - this.moderationLogService.log(deleter, 'deleteChatRoom', { - roomId: room.id, - room: room, - }); - } - } - } - - @bindThis - public async findMyRoomById(ownerId: MiUser['id'], roomId: MiChatRoom['id']) { - return this.chatRoomsRepository.findOneBy({ id: roomId, ownerId: ownerId }); - } - - @bindThis - public async findRoomById(roomId: MiChatRoom['id']) { - return this.chatRoomsRepository.findOne({ where: { id: roomId }, relations: ['owner'] }); - } - - @bindThis - public async isRoomMember(room: MiChatRoom, userId: MiUser['id']) { - if (room.ownerId === userId) return true; - const membership = await this.chatRoomMembershipsRepository.findOneBy({ roomId: room.id, userId }); - return membership != null; - } - - @bindThis - public async createRoomInvitation(inviterId: MiUser['id'], roomId: MiChatRoom['id'], inviteeId: MiUser['id']) { - if (inviterId === inviteeId) { - throw new Error('yourself'); - } - - const room = await this.chatRoomsRepository.findOneByOrFail({ id: roomId, ownerId: inviterId }); - - if (await this.isRoomMember(room, inviteeId)) { - throw new Error('already member'); - } - - const existingInvitation = await this.chatRoomInvitationsRepository.findOneBy({ roomId, userId: inviteeId }); - if (existingInvitation) { - throw new Error('already invited'); - } - - const membershipsCount = await this.chatRoomMembershipsRepository.countBy({ roomId }); - if (membershipsCount >= MAX_ROOM_MEMBERS) { - throw new Error('room is full'); - } - - // TODO: cehck block - - const invitation = { - id: this.idService.gen(), - roomId: room.id, - userId: inviteeId, - } satisfies Partial; - - const created = await this.chatRoomInvitationsRepository.insertOne(invitation); - - this.notificationService.createNotification(inviteeId, 'chatRoomInvitationReceived', { - invitationId: invitation.id, - }, inviterId); - - return created; - } - - @bindThis - public async getSentRoomInvitationsWithPagination(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatRoomInvitation['id'] | null, untilId?: MiChatRoomInvitation['id'] | null) { - const query = this.queryService.makePaginationQuery(this.chatRoomInvitationsRepository.createQueryBuilder('invitation'), sinceId, untilId) - .andWhere('invitation.roomId = :roomId', { roomId }); - - const invitations = await query.take(limit).getMany(); - - return invitations; - } - - @bindThis - public async getOwnedRoomsWithPagination(ownerId: MiUser['id'], limit: number, sinceId?: MiChatRoom['id'] | null, untilId?: MiChatRoom['id'] | null) { - const query = this.queryService.makePaginationQuery(this.chatRoomsRepository.createQueryBuilder('room'), sinceId, untilId) - .andWhere('room.ownerId = :ownerId', { ownerId }); - - const rooms = await query.take(limit).getMany(); - - return rooms; - } - - @bindThis - public async getReceivedRoomInvitationsWithPagination(userId: MiUser['id'], limit: number, sinceId?: MiChatRoomInvitation['id'] | null, untilId?: MiChatRoomInvitation['id'] | null) { - const query = this.queryService.makePaginationQuery(this.chatRoomInvitationsRepository.createQueryBuilder('invitation'), sinceId, untilId) - .andWhere('invitation.userId = :userId', { userId }) - .andWhere('invitation.ignored = FALSE'); - - const invitations = await query.take(limit).getMany(); - - return invitations; - } - - @bindThis - public async joinToRoom(userId: MiUser['id'], roomId: MiChatRoom['id']) { - const invitation = await this.chatRoomInvitationsRepository.findOneByOrFail({ roomId, userId }); - - const membershipsCount = await this.chatRoomMembershipsRepository.countBy({ roomId }); - if (membershipsCount >= MAX_ROOM_MEMBERS) { - throw new Error('room is full'); - } - - const membership = { - id: this.idService.gen(), - roomId: roomId, - userId: userId, - } satisfies Partial; - - // TODO: transaction - await this.chatRoomMembershipsRepository.insertOne(membership); - await this.chatRoomInvitationsRepository.delete(invitation.id); - } - - @bindThis - public async ignoreRoomInvitation(userId: MiUser['id'], roomId: MiChatRoom['id']) { - const invitation = await this.chatRoomInvitationsRepository.findOneByOrFail({ roomId, userId }); - await this.chatRoomInvitationsRepository.update(invitation.id, { ignored: true }); - } - - @bindThis - public async leaveRoom(userId: MiUser['id'], roomId: MiChatRoom['id']) { - const membership = await this.chatRoomMembershipsRepository.findOneByOrFail({ roomId, userId }); - await this.chatRoomMembershipsRepository.delete(membership.id); - - // 未読フラグを消す (「既読にする」というわけでもないのでreadメソッドは使わないでおく) - const redisPipeline = this.redisClient.pipeline(); - redisPipeline.del(`newRoomChatMessageExists:${userId}:${roomId}`); - redisPipeline.srem(`newChatMessagesExists:${userId}`, `room:${roomId}`); - await redisPipeline.exec(); - } - - @bindThis - public async muteRoom(userId: MiUser['id'], roomId: MiChatRoom['id'], mute: boolean) { - const membership = await this.chatRoomMembershipsRepository.findOneByOrFail({ roomId, userId }); - await this.chatRoomMembershipsRepository.update(membership.id, { isMuted: mute }); - } - - @bindThis - public async updateRoom(room: MiChatRoom, params: { - name?: string; - description?: string; - }): Promise { - return this.chatRoomsRepository.createQueryBuilder().update() - .set(params) - .where('id = :id', { id: room.id }) - .returning('*') - .execute() - .then((response) => { - return response.raw[0]; - }); - } - - @bindThis - public async getRoomMembershipsWithPagination(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatRoomMembership['id'] | null, untilId?: MiChatRoomMembership['id'] | null) { - const query = this.queryService.makePaginationQuery(this.chatRoomMembershipsRepository.createQueryBuilder('membership'), sinceId, untilId) - .andWhere('membership.roomId = :roomId', { roomId }); - - const memberships = await query.take(limit).getMany(); - - return memberships; - } - - @bindThis - public async searchMessages(meId: MiUser['id'], query: string, limit: number, params: { - userId?: MiUser['id'] | null; - roomId?: MiChatRoom['id'] | null; - }) { - const q = this.chatMessagesRepository.createQueryBuilder('message'); - - if (params.userId) { - q.andWhere(new Brackets(qb => { - qb - .where(new Brackets(qb => { - qb - .where('message.fromUserId = :meId') - .andWhere('message.toUserId = :otherId'); - })) - .orWhere(new Brackets(qb => { - qb - .where('message.fromUserId = :otherId') - .andWhere('message.toUserId = :meId'); - })); - })) - .setParameter('meId', meId) - .setParameter('otherId', params.userId); - } else if (params.roomId) { - q.where('message.toRoomId = :roomId', { roomId: params.roomId }); - } else { - const membershipsQuery = this.chatRoomMembershipsRepository.createQueryBuilder('membership') - .select('membership.roomId') - .where('membership.userId = :meId', { meId: meId }); - - const ownedRoomsQuery = this.chatRoomsRepository.createQueryBuilder('room') - .select('room.id') - .where('room.ownerId = :meId', { meId }); - - q.andWhere(new Brackets(qb => { - qb - .where('message.fromUserId = :meId') - .orWhere('message.toUserId = :meId') - .orWhere(`message.toRoomId IN (${membershipsQuery.getQuery()})`) - .orWhere(`message.toRoomId IN (${ownedRoomsQuery.getQuery()})`); - })); - - q.setParameters(membershipsQuery.getParameters()); - q.setParameters(ownedRoomsQuery.getParameters()); - } - - q.andWhere('LOWER(message.text) LIKE :q', { q: `%${ sqlLikeEscape(query.toLowerCase()) }%` }); - - q.leftJoinAndSelect('message.file', 'file'); - q.leftJoinAndSelect('message.fromUser', 'fromUser'); - q.leftJoinAndSelect('message.toUser', 'toUser'); - q.leftJoinAndSelect('message.toRoom', 'toRoom'); - q.leftJoinAndSelect('toRoom.owner', 'toRoomOwner'); - - const messages = await q.orderBy('message.id', 'DESC').take(limit).getMany(); - - return messages; - } - - @bindThis - public async react(messageId: MiChatMessage['id'], userId: MiUser['id'], reaction_: string) { - let reaction; - - const custom = reaction_.match(isCustomEmojiRegexp); - - if (custom == null) { - reaction = normalizeEmojiString(reaction_); - } else { - const name = custom[1]; - const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name); - - if (emoji == null) { - throw new Error('no such emoji'); - } else { - reaction = `:${name}:`; - } - } - - const message = await this.chatMessagesRepository.findOneByOrFail({ id: messageId }); - - if (message.fromUserId === userId) { - throw new Error('cannot react to own message'); - } - - if (message.toRoomId === null && message.toUserId !== userId) { - throw new Error('cannot react to others message'); - } - - if (message.reactions.length >= MAX_REACTIONS_PER_MESSAGE) { - throw new Error('too many reactions'); - } - - const room = message.toRoomId ? await this.chatRoomsRepository.findOneByOrFail({ id: message.toRoomId }) : null; - - if (room) { - if (!await this.isRoomMember(room, userId)) { - throw new Error('cannot react to others message'); - } - } - - await this.chatMessagesRepository.createQueryBuilder().update() - .set({ - reactions: () => `array_append("reactions", '${userId}/${reaction}')`, - }) - .where('id = :id', { id: message.id }) - .execute(); - - if (room) { - this.globalEventService.publishChatRoomStream(room.id, 'react', { - messageId: message.id, - user: await this.userEntityService.pack(userId), - reaction, - }); - } else { - this.globalEventService.publishChatUserStream(message.fromUserId, message.toUserId!, 'react', { - messageId: message.id, - reaction, - }); - this.globalEventService.publishChatUserStream(message.toUserId!, message.fromUserId, 'react', { - messageId: message.id, - reaction, - }); - } - } - - @bindThis - public async unreact(messageId: MiChatMessage['id'], userId: MiUser['id'], reaction_: string) { - let reaction; - - const custom = reaction_.match(isCustomEmojiRegexp); - - if (custom == null) { - reaction = normalizeEmojiString(reaction_); - } else { // 削除されたカスタム絵文字のリアクションを削除したいかもしれないので絵文字の存在チェックはする必要なし - const name = custom[1]; - reaction = `:${name}:`; - } - - // NOTE: 自分のリアクションを(あれば)削除するだけなので諸々の権限チェックは必要なし - - const message = await this.chatMessagesRepository.findOneByOrFail({ id: messageId }); - - const room = message.toRoomId ? await this.chatRoomsRepository.findOneByOrFail({ id: message.toRoomId }) : null; - - await this.chatMessagesRepository.createQueryBuilder().update() - .set({ - reactions: () => `array_remove("reactions", '${userId}/${reaction}')`, - }) - .where('id = :id', { id: message.id }) - .execute(); - - // TODO: 実際に削除が行われたときのみイベントを発行する - - if (room) { - this.globalEventService.publishChatRoomStream(room.id, 'unreact', { - messageId: message.id, - user: await this.userEntityService.pack(userId), - reaction, - }); - } else { - this.globalEventService.publishChatUserStream(message.fromUserId, message.toUserId!, 'unreact', { - messageId: message.id, - reaction, - }); - this.globalEventService.publishChatUserStream(message.toUserId!, message.fromUserId, 'unreact', { - messageId: message.id, - reaction, - }); - } - } - - @bindThis - public async getMyMemberships(userId: MiUser['id'], limit: number, sinceId?: MiChatRoomMembership['id'] | null, untilId?: MiChatRoomMembership['id'] | null) { - const query = this.queryService.makePaginationQuery(this.chatRoomMembershipsRepository.createQueryBuilder('membership'), sinceId, untilId) - .andWhere('membership.userId = :userId', { userId }); - - const memberships = await query.take(limit).getMany(); - - return memberships; - } -} diff --git a/packages/backend/src/core/ClipService.ts b/packages/backend/src/core/ClipService.ts deleted file mode 100644 index 929a9db064..0000000000 --- a/packages/backend/src/core/ClipService.ts +++ /dev/null @@ -1,158 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { QueryFailedError } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import type { ClipsRepository, MiNote, MiClip, ClipNotesRepository, NotesRepository } from '@/models/_.js'; -import { bindThis } from '@/decorators.js'; -import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import { RoleService } from '@/core/RoleService.js'; -import { IdService } from '@/core/IdService.js'; -import type { MiLocalUser } from '@/models/User.js'; - -@Injectable() -export class ClipService { - public static NoSuchNoteError = class extends Error {}; - public static NoSuchClipError = class extends Error {}; - public static AlreadyAddedError = class extends Error {}; - public static TooManyClipNotesError = class extends Error {}; - public static TooManyClipsError = class extends Error {}; - - constructor( - @Inject(DI.clipsRepository) - private clipsRepository: ClipsRepository, - - @Inject(DI.clipNotesRepository) - private clipNotesRepository: ClipNotesRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - private roleService: RoleService, - private idService: IdService, - ) { - } - - @bindThis - public async create(me: MiLocalUser, name: string, isPublic: boolean, description: string | null): Promise { - const currentCount = await this.clipsRepository.countBy({ - userId: me.id, - }); - if (currentCount >= (await this.roleService.getUserPolicies(me.id)).clipLimit) { - throw new ClipService.TooManyClipsError(); - } - - const clip = await this.clipsRepository.insertOne({ - id: this.idService.gen(), - userId: me.id, - name: name, - isPublic: isPublic, - description: description, - }); - - return clip; - } - - @bindThis - public async update(me: MiLocalUser, clipId: MiClip['id'], name: string | undefined, isPublic: boolean | undefined, description: string | null | undefined): Promise { - const clip = await this.clipsRepository.findOneBy({ - id: clipId, - userId: me.id, - }); - - if (clip == null) { - throw new ClipService.NoSuchClipError(); - } - - await this.clipsRepository.update(clip.id, { - name: name, - description: description, - isPublic: isPublic, - }); - } - - @bindThis - public async delete(me: MiLocalUser, clipId: MiClip['id']): Promise { - const clip = await this.clipsRepository.findOneBy({ - id: clipId, - userId: me.id, - }); - - if (clip == null) { - throw new ClipService.NoSuchClipError(); - } - - await this.clipsRepository.delete(clip.id); - } - - @bindThis - public async addNote(me: MiLocalUser, clipId: MiClip['id'], noteId: MiNote['id']): Promise { - const clip = await this.clipsRepository.findOneBy({ - id: clipId, - userId: me.id, - }); - - if (clip == null) { - throw new ClipService.NoSuchClipError(); - } - - const currentCount = await this.clipNotesRepository.countBy({ - clipId: clip.id, - }); - if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) { - throw new ClipService.TooManyClipNotesError(); - } - - try { - await this.clipNotesRepository.insert({ - id: this.idService.gen(), - noteId: noteId, - clipId: clip.id, - }); - } catch (e: unknown) { - if (e instanceof QueryFailedError) { - if (isDuplicateKeyValueError(e)) { - throw new ClipService.AlreadyAddedError(); - } else if (e.driverError.detail.includes('is not present in table "note".')) { - throw new ClipService.NoSuchNoteError(); - } - } - - throw e; - } - - this.clipsRepository.update(clip.id, { - lastClippedAt: new Date(), - }); - - this.notesRepository.increment({ id: noteId }, 'clippedCount', 1); - } - - @bindThis - public async removeNote(me: MiLocalUser, clipId: MiClip['id'], noteId: MiNote['id']): Promise { - const clip = await this.clipsRepository.findOneBy({ - id: clipId, - userId: me.id, - }); - - if (clip == null) { - throw new ClipService.NoSuchClipError(); - } - - const note = await this.notesRepository.findOneBy({ id: noteId }); - - if (note == null) { - throw new ClipService.NoSuchNoteError(); - } - - await this.clipNotesRepository.delete({ - noteId: noteId, - clipId: clip.id, - }); - - this.notesRepository.decrement({ id: noteId }, 'clippedCount', 1); - } -} diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index d8617e343c..d3a1b1b024 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -1,29 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Module } from '@nestjs/common'; -import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; -import { AbuseReportService } from '@/core/AbuseReportService.js'; -import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; -import { - AbuseReportNotificationRecipientEntityService, -} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { UserSearchService } from '@/core/UserSearchService.js'; -import { WebhookTestService } from '@/core/WebhookTestService.js'; -import { FlashService } from '@/core/FlashService.js'; import { AccountMoveService } from './AccountMoveService.js'; import { AccountUpdateService } from './AccountUpdateService.js'; import { AiService } from './AiService.js'; -import { AnnouncementService } from './AnnouncementService.js'; import { AntennaService } from './AntennaService.js'; import { AppLockService } from './AppLockService.js'; import { AchievementService } from './AchievementService.js'; -import { AvatarDecorationService } from './AvatarDecorationService.js'; import { CaptchaService } from './CaptchaService.js'; +import { CreateSystemUserService } from './CreateSystemUserService.js'; import { CustomEmojiService } from './CustomEmojiService.js'; import { DeleteAccountService } from './DeleteAccountService.js'; import { DownloadService } from './DownloadService.js'; @@ -36,7 +19,7 @@ import { HashtagService } from './HashtagService.js'; import { HttpRequestService } from './HttpRequestService.js'; import { IdService } from './IdService.js'; import { ImageProcessingService } from './ImageProcessingService.js'; -import { SystemAccountService } from './SystemAccountService.js'; +import { InstanceActorService } from './InstanceActorService.js'; import { InternalStorageService } from './InternalStorageService.js'; import { MetaService } from './MetaService.js'; import { MfmService } from './MfmService.js'; @@ -44,40 +27,30 @@ import { ModerationLogService } from './ModerationLogService.js'; import { NoteCreateService } from './NoteCreateService.js'; import { NoteDeleteService } from './NoteDeleteService.js'; import { NotePiningService } from './NotePiningService.js'; +import { NoteReadService } from './NoteReadService.js'; import { NotificationService } from './NotificationService.js'; import { PollService } from './PollService.js'; import { PushNotificationService } from './PushNotificationService.js'; import { QueryService } from './QueryService.js'; import { ReactionService } from './ReactionService.js'; -import { ReactionsBufferingService } from './ReactionsBufferingService.js'; import { RelayService } from './RelayService.js'; import { RoleService } from './RoleService.js'; import { S3Service } from './S3Service.js'; import { SignupService } from './SignupService.js'; -import { WebAuthnService } from './WebAuthnService.js'; +import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js'; import { UserBlockingService } from './UserBlockingService.js'; import { CacheService } from './CacheService.js'; -import { UserService } from './UserService.js'; import { UserFollowingService } from './UserFollowingService.js'; import { UserKeypairService } from './UserKeypairService.js'; import { UserListService } from './UserListService.js'; import { UserMutingService } from './UserMutingService.js'; -import { UserRenoteMutingService } from './UserRenoteMutingService.js'; import { UserSuspendService } from './UserSuspendService.js'; -import { UserAuthService } from './UserAuthService.js'; import { VideoProcessingService } from './VideoProcessingService.js'; -import { UserWebhookService } from './UserWebhookService.js'; +import { WebhookService } from './WebhookService.js'; +import { ProxyAccountService } from './ProxyAccountService.js'; import { UtilityService } from './UtilityService.js'; import { FileInfoService } from './FileInfoService.js'; import { SearchService } from './SearchService.js'; -import { ClipService } from './ClipService.js'; -import { FeaturedService } from './FeaturedService.js'; -import { FanoutTimelineService } from './FanoutTimelineService.js'; -import { ChannelFollowingService } from './ChannelFollowingService.js'; -import { ChatService } from './ChatService.js'; -import { RegistryApiService } from './RegistryApiService.js'; -import { ReversiService } from './ReversiService.js'; - import { ChartLoggerService } from './chart/ChartLoggerService.js'; import FederationChart from './chart/charts/federation.js'; import NotesChart from './chart/charts/notes.js'; @@ -92,15 +65,12 @@ 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'; import { ChartManagementService } from './chart/ChartManagementService.js'; - import { AbuseUserReportEntityService } from './entities/AbuseUserReportEntityService.js'; -import { AnnouncementEntityService } from './entities/AnnouncementEntityService.js'; import { AntennaEntityService } from './entities/AntennaEntityService.js'; import { AppEntityService } from './entities/AppEntityService.js'; import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js'; import { BlockingEntityService } from './entities/BlockingEntityService.js'; import { ChannelEntityService } from './entities/ChannelEntityService.js'; -import { ChatEntityService } from './entities/ChatEntityService.js'; import { ClipEntityService } from './entities/ClipEntityService.js'; import { DriveFileEntityService } from './entities/DriveFileEntityService.js'; import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js'; @@ -111,7 +81,6 @@ import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js import { GalleryPostEntityService } from './entities/GalleryPostEntityService.js'; import { HashtagEntityService } from './entities/HashtagEntityService.js'; import { InstanceEntityService } from './entities/InstanceEntityService.js'; -import { InviteCodeEntityService } from './entities/InviteCodeEntityService.js'; import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js'; import { MutingEntityService } from './entities/MutingEntityService.js'; import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js'; @@ -127,9 +96,6 @@ import { UserListEntityService } from './entities/UserListEntityService.js'; import { FlashEntityService } from './entities/FlashEntityService.js'; import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js'; import { RoleEntityService } from './entities/RoleEntityService.js'; -import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; -import { MetaEntityService } from './entities/MetaEntityService.js'; - import { ApAudienceService } from './activitypub/ApAudienceService.js'; import { ApDbResolverService } from './activitypub/ApDbResolverService.js'; import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js'; @@ -139,7 +105,7 @@ import { ApMfmService } from './activitypub/ApMfmService.js'; import { ApRendererService } from './activitypub/ApRendererService.js'; import { ApRequestService } from './activitypub/ApRequestService.js'; import { ApResolverService } from './activitypub/ApResolverService.js'; -import { JsonLdService } from './activitypub/JsonLdService.js'; +import { LdSignatureService } from './activitypub/LdSignatureService.js'; import { RemoteLoggerService } from './RemoteLoggerService.js'; import { RemoteUserResolveService } from './RemoteUserResolveService.js'; import { WebfingerService } from './WebfingerService.js'; @@ -155,17 +121,14 @@ import type { Provider } from '@nestjs/common'; //#region 文字列ベースでのinjection用(循環参照対応のため) const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService }; -const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService }; -const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService }; const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService }; const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService }; const $AiService: Provider = { provide: 'AiService', useExisting: AiService }; -const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExisting: AnnouncementService }; const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService }; const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService }; const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService }; -const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService }; const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService }; +const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService }; const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService }; const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService }; const $DownloadService: Provider = { provide: 'DownloadService', useExisting: DownloadService }; @@ -178,6 +141,7 @@ const $HashtagService: Provider = { provide: 'HashtagService', useExisting: Hash const $HttpRequestService: Provider = { provide: 'HttpRequestService', useExisting: HttpRequestService }; const $IdService: Provider = { provide: 'IdService', useExisting: IdService }; const $ImageProcessingService: Provider = { provide: 'ImageProcessingService', useExisting: ImageProcessingService }; +const $InstanceActorService: Provider = { provide: 'InstanceActorService', useExisting: InstanceActorService }; const $InternalStorageService: Provider = { provide: 'InternalStorageService', useExisting: InternalStorageService }; const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaService }; const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService }; @@ -185,45 +149,30 @@ const $ModerationLogService: Provider = { provide: 'ModerationLogService', useEx const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService }; const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService }; const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService }; +const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService }; const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService }; const $PollService: Provider = { provide: 'PollService', useExisting: PollService }; -const $SystemAccountService: Provider = { provide: 'SystemAccountService', useExisting: SystemAccountService }; +const $ProxyAccountService: Provider = { provide: 'ProxyAccountService', useExisting: ProxyAccountService }; const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService }; const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService }; const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService }; -const $ReactionsBufferingService: Provider = { provide: 'ReactionsBufferingService', useExisting: ReactionsBufferingService }; const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService }; const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService }; const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service }; const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService }; -const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService }; +const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService }; const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService }; const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService }; -const $UserService: Provider = { provide: 'UserService', useExisting: UserService }; const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService }; const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService }; const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService }; const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService }; -const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService', useExisting: UserRenoteMutingService }; -const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService }; const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService }; -const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService }; const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService }; -const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService }; -const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService }; -const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService }; +const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService }; const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService }; const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService }; -const $FlashService: Provider = { provide: 'FlashService', useExisting: FlashService }; const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; -const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService }; -const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService }; -const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService }; -const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService }; -const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService }; -const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatService }; -const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService }; -const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; @@ -241,14 +190,11 @@ const $ApRequestChart: Provider = { provide: 'ApRequestChart', useExisting: ApRe const $ChartManagementService: Provider = { provide: 'ChartManagementService', useExisting: ChartManagementService }; const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService }; -const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService }; -const $AbuseReportNotificationRecipientEntityService: Provider = { provide: 'AbuseReportNotificationRecipientEntityService', useExisting: AbuseReportNotificationRecipientEntityService }; const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService }; const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService }; const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService }; const $BlockingEntityService: Provider = { provide: 'BlockingEntityService', useExisting: BlockingEntityService }; const $ChannelEntityService: Provider = { provide: 'ChannelEntityService', useExisting: ChannelEntityService }; -const $ChatEntityService: Provider = { provide: 'ChatEntityService', useExisting: ChatEntityService }; const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting: ClipEntityService }; const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService }; const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useExisting: DriveFolderEntityService }; @@ -259,7 +205,6 @@ const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService const $GalleryPostEntityService: Provider = { provide: 'GalleryPostEntityService', useExisting: GalleryPostEntityService }; const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useExisting: HashtagEntityService }; const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService }; -const $InviteCodeEntityService: Provider = { provide: 'InviteCodeEntityService', useExisting: InviteCodeEntityService }; const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService }; const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService }; const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService }; @@ -275,9 +220,6 @@ const $UserListEntityService: Provider = { provide: 'UserListEntityService', use const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService }; const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService }; const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService }; -const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService }; -const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService }; -const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService }; const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService }; const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService }; @@ -288,7 +230,7 @@ const $ApMfmService: Provider = { provide: 'ApMfmService', useExisting: ApMfmSer const $ApRendererService: Provider = { provide: 'ApRendererService', useExisting: ApRendererService }; const $ApRequestService: Provider = { provide: 'ApRequestService', useExisting: ApRequestService }; const $ApResolverService: Provider = { provide: 'ApResolverService', useExisting: ApResolverService }; -const $JsonLdService: Provider = { provide: 'JsonLdService', useExisting: JsonLdService }; +const $LdSignatureService: Provider = { provide: 'LdSignatureService', useExisting: LdSignatureService }; const $RemoteLoggerService: Provider = { provide: 'RemoteLoggerService', useExisting: RemoteLoggerService }; const $RemoteUserResolveService: Provider = { provide: 'RemoteUserResolveService', useExisting: RemoteUserResolveService }; const $WebfingerService: Provider = { provide: 'WebfingerService', useExisting: WebfingerService }; @@ -305,17 +247,14 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ], providers: [ LoggerService, - AbuseReportService, - AbuseReportNotificationService, AccountMoveService, AccountUpdateService, AiService, - AnnouncementService, AntennaService, AppLockService, AchievementService, - AvatarDecorationService, CaptchaService, + CreateSystemUserService, CustomEmojiService, DeleteAccountService, DownloadService, @@ -328,6 +267,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting HttpRequestService, IdService, ImageProcessingService, + InstanceActorService, InternalStorageService, MetaService, MfmService, @@ -335,46 +275,30 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting NoteCreateService, NoteDeleteService, NotePiningService, + NoteReadService, NotificationService, PollService, - SystemAccountService, + ProxyAccountService, PushNotificationService, QueryService, ReactionService, - ReactionsBufferingService, RelayService, RoleService, S3Service, SignupService, - WebAuthnService, + TwoFactorAuthenticationService, UserBlockingService, CacheService, - UserService, UserFollowingService, UserKeypairService, UserListService, UserMutingService, - UserRenoteMutingService, - UserSearchService, UserSuspendService, - UserAuthService, VideoProcessingService, - UserWebhookService, - SystemWebhookService, - WebhookTestService, + WebhookService, UtilityService, FileInfoService, - FlashService, SearchService, - ClipService, - FeaturedService, - FanoutTimelineService, - FanoutTimelineEndpointService, - ChannelFollowingService, - ChatService, - RegistryApiService, - ReversiService, - ChartLoggerService, FederationChart, NotesChart, @@ -389,16 +313,12 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PerUserDriveChart, ApRequestChart, ChartManagementService, - AbuseUserReportEntityService, - AnnouncementEntityService, - AbuseReportNotificationRecipientEntityService, AntennaEntityService, AppEntityService, AuthSessionEntityService, BlockingEntityService, ChannelEntityService, - ChatEntityService, ClipEntityService, DriveFileEntityService, DriveFolderEntityService, @@ -409,7 +329,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting GalleryPostEntityService, HashtagEntityService, InstanceEntityService, - InviteCodeEntityService, ModerationLogEntityService, MutingEntityService, RenoteMutingEntityService, @@ -425,10 +344,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FlashEntityService, FlashLikeEntityService, RoleEntityService, - ReversiGameEntityService, - MetaEntityService, - SystemWebhookEntityService, - ApAudienceService, ApDbResolverService, ApDeliverManagerService, @@ -438,7 +353,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ApRendererService, ApRequestService, ApResolverService, - JsonLdService, + LdSignatureService, RemoteLoggerService, RemoteUserResolveService, WebfingerService, @@ -451,17 +366,14 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting //#region 文字列ベースでのinjection用(循環参照対応のため) $LoggerService, - $AbuseReportService, - $AbuseReportNotificationService, $AccountMoveService, $AccountUpdateService, $AiService, - $AnnouncementService, $AntennaService, $AppLockService, $AchievementService, - $AvatarDecorationService, $CaptchaService, + $CreateSystemUserService, $CustomEmojiService, $DeleteAccountService, $DownloadService, @@ -474,6 +386,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $HttpRequestService, $IdService, $ImageProcessingService, + $InstanceActorService, $InternalStorageService, $MetaService, $MfmService, @@ -481,46 +394,30 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $NoteCreateService, $NoteDeleteService, $NotePiningService, + $NoteReadService, $NotificationService, $PollService, - $SystemAccountService, + $ProxyAccountService, $PushNotificationService, $QueryService, $ReactionService, - $ReactionsBufferingService, $RelayService, $RoleService, $S3Service, $SignupService, - $WebAuthnService, + $TwoFactorAuthenticationService, $UserBlockingService, $CacheService, - $UserService, $UserFollowingService, $UserKeypairService, $UserListService, $UserMutingService, - $UserRenoteMutingService, - $UserSearchService, $UserSuspendService, - $UserAuthService, $VideoProcessingService, - $UserWebhookService, - $SystemWebhookService, - $WebhookTestService, + $WebhookService, $UtilityService, $FileInfoService, - $FlashService, $SearchService, - $ClipService, - $FeaturedService, - $FanoutTimelineService, - $FanoutTimelineEndpointService, - $ChannelFollowingService, - $ChatService, - $RegistryApiService, - $ReversiService, - $ChartLoggerService, $FederationChart, $NotesChart, @@ -535,16 +432,12 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PerUserDriveChart, $ApRequestChart, $ChartManagementService, - $AbuseUserReportEntityService, - $AnnouncementEntityService, - $AbuseReportNotificationRecipientEntityService, $AntennaEntityService, $AppEntityService, $AuthSessionEntityService, $BlockingEntityService, $ChannelEntityService, - $ChatEntityService, $ClipEntityService, $DriveFileEntityService, $DriveFolderEntityService, @@ -555,7 +448,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $GalleryPostEntityService, $HashtagEntityService, $InstanceEntityService, - $InviteCodeEntityService, $ModerationLogEntityService, $MutingEntityService, $RenoteMutingEntityService, @@ -571,10 +463,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FlashEntityService, $FlashLikeEntityService, $RoleEntityService, - $ReversiGameEntityService, - $MetaEntityService, - $SystemWebhookEntityService, - $ApAudienceService, $ApDbResolverService, $ApDeliverManagerService, @@ -584,7 +472,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $ApRendererService, $ApRequestService, $ApResolverService, - $JsonLdService, + $LdSignatureService, $RemoteLoggerService, $RemoteUserResolveService, $WebfingerService, @@ -598,17 +486,14 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting exports: [ QueueModule, LoggerService, - AbuseReportService, - AbuseReportNotificationService, AccountMoveService, AccountUpdateService, AiService, - AnnouncementService, AntennaService, AppLockService, AchievementService, - AvatarDecorationService, CaptchaService, + CreateSystemUserService, CustomEmojiService, DeleteAccountService, DownloadService, @@ -621,6 +506,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting HttpRequestService, IdService, ImageProcessingService, + InstanceActorService, InternalStorageService, MetaService, MfmService, @@ -628,46 +514,30 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting NoteCreateService, NoteDeleteService, NotePiningService, + NoteReadService, NotificationService, PollService, - SystemAccountService, + ProxyAccountService, PushNotificationService, QueryService, ReactionService, - ReactionsBufferingService, RelayService, RoleService, S3Service, SignupService, - WebAuthnService, + TwoFactorAuthenticationService, UserBlockingService, CacheService, - UserService, UserFollowingService, UserKeypairService, UserListService, UserMutingService, - UserRenoteMutingService, - UserSearchService, UserSuspendService, - UserAuthService, VideoProcessingService, - UserWebhookService, - SystemWebhookService, - WebhookTestService, + WebhookService, UtilityService, FileInfoService, - FlashService, SearchService, - ClipService, - FeaturedService, - FanoutTimelineService, - FanoutTimelineEndpointService, - ChannelFollowingService, - ChatService, - RegistryApiService, - ReversiService, - FederationChart, NotesChart, UsersChart, @@ -681,16 +551,12 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PerUserDriveChart, ApRequestChart, ChartManagementService, - AbuseUserReportEntityService, - AnnouncementEntityService, - AbuseReportNotificationRecipientEntityService, AntennaEntityService, AppEntityService, AuthSessionEntityService, BlockingEntityService, ChannelEntityService, - ChatEntityService, ClipEntityService, DriveFileEntityService, DriveFolderEntityService, @@ -701,7 +567,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting GalleryPostEntityService, HashtagEntityService, InstanceEntityService, - InviteCodeEntityService, ModerationLogEntityService, MutingEntityService, RenoteMutingEntityService, @@ -717,10 +582,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FlashEntityService, FlashLikeEntityService, RoleEntityService, - ReversiGameEntityService, - MetaEntityService, - SystemWebhookEntityService, - ApAudienceService, ApDbResolverService, ApDeliverManagerService, @@ -730,7 +591,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ApRendererService, ApRequestService, ApResolverService, - JsonLdService, + LdSignatureService, RemoteLoggerService, RemoteUserResolveService, WebfingerService, @@ -743,17 +604,14 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting //#region 文字列ベースでのinjection用(循環参照対応のため) $LoggerService, - $AbuseReportService, - $AbuseReportNotificationService, $AccountMoveService, $AccountUpdateService, $AiService, - $AnnouncementService, $AntennaService, $AppLockService, $AchievementService, - $AvatarDecorationService, $CaptchaService, + $CreateSystemUserService, $CustomEmojiService, $DeleteAccountService, $DownloadService, @@ -766,6 +624,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $HttpRequestService, $IdService, $ImageProcessingService, + $InstanceActorService, $InternalStorageService, $MetaService, $MfmService, @@ -773,45 +632,30 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $NoteCreateService, $NoteDeleteService, $NotePiningService, + $NoteReadService, $NotificationService, $PollService, - $SystemAccountService, + $ProxyAccountService, $PushNotificationService, $QueryService, $ReactionService, - $ReactionsBufferingService, $RelayService, $RoleService, $S3Service, $SignupService, - $WebAuthnService, + $TwoFactorAuthenticationService, $UserBlockingService, $CacheService, - $UserService, $UserFollowingService, $UserKeypairService, $UserListService, $UserMutingService, - $UserRenoteMutingService, - $UserSearchService, $UserSuspendService, - $UserAuthService, $VideoProcessingService, - $UserWebhookService, - $SystemWebhookService, - $WebhookTestService, + $WebhookService, $UtilityService, $FileInfoService, $SearchService, - $ClipService, - $FeaturedService, - $FanoutTimelineService, - $FanoutTimelineEndpointService, - $ChannelFollowingService, - $ChatService, - $RegistryApiService, - $ReversiService, - $FederationChart, $NotesChart, $UsersChart, @@ -825,16 +669,12 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PerUserDriveChart, $ApRequestChart, $ChartManagementService, - $AbuseUserReportEntityService, - $AnnouncementEntityService, - $AbuseReportNotificationRecipientEntityService, $AntennaEntityService, $AppEntityService, $AuthSessionEntityService, $BlockingEntityService, $ChannelEntityService, - $ChatEntityService, $ClipEntityService, $DriveFileEntityService, $DriveFolderEntityService, @@ -845,7 +685,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $GalleryPostEntityService, $HashtagEntityService, $InstanceEntityService, - $InviteCodeEntityService, $ModerationLogEntityService, $MutingEntityService, $RenoteMutingEntityService, @@ -861,10 +700,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FlashEntityService, $FlashLikeEntityService, $RoleEntityService, - $ReversiGameEntityService, - $MetaEntityService, - $SystemWebhookEntityService, - $ApAudienceService, $ApDbResolverService, $ApDeliverManagerService, @@ -874,7 +709,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $ApRendererService, $ApRequestService, $ApResolverService, - $JsonLdService, + $LdSignatureService, $RemoteLoggerService, $RemoteUserResolveService, $WebfingerService, diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts new file mode 100644 index 0000000000..8f887d90f9 --- /dev/null +++ b/packages/backend/src/core/CreateSystemUserService.ts @@ -0,0 +1,82 @@ +import { Inject, Injectable } from '@nestjs/common'; +import bcrypt from 'bcryptjs'; +import { v4 as uuid } from 'uuid'; +import { IsNull, DataSource } from 'typeorm'; +import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; +import { User } from '@/models/entities/User.js'; +import { UserProfile } from '@/models/entities/UserProfile.js'; +import { IdService } from '@/core/IdService.js'; +import { UserKeypair } from '@/models/entities/UserKeypair.js'; +import { UsedUsername } from '@/models/entities/UsedUsername.js'; +import { DI } from '@/di-symbols.js'; +import generateNativeUserToken from '@/misc/generate-native-user-token.js'; +import { bindThis } from '@/decorators.js'; + +@Injectable() +export class CreateSystemUserService { + constructor( + @Inject(DI.db) + private db: DataSource, + + private idService: IdService, + ) { + } + + @bindThis + public async createSystemUser(username: string): Promise { + const password = uuid(); + + // Generate hash of password + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(password, salt); + + // Generate secret + const secret = generateNativeUserToken(); + + const keyPair = await genRsaKeyPair(4096); + + let account!: User; + + // Start transaction + await this.db.transaction(async transactionalEntityManager => { + const exist = await transactionalEntityManager.findOneBy(User, { + usernameLower: username.toLowerCase(), + host: IsNull(), + }); + + if (exist) throw new Error('the user is already exists'); + + account = await transactionalEntityManager.insert(User, { + id: this.idService.genId(), + createdAt: new Date(), + username: username, + usernameLower: username.toLowerCase(), + host: null, + token: secret, + isRoot: false, + isLocked: true, + isExplorable: false, + isBot: true, + }).then(x => transactionalEntityManager.findOneByOrFail(User, x.identifiers[0])); + + await transactionalEntityManager.insert(UserKeypair, { + publicKey: keyPair.publicKey, + privateKey: keyPair.privateKey, + userId: account.id, + }); + + await transactionalEntityManager.insert(UserProfile, { + userId: account.id, + autoAcceptFollowed: false, + password: hash, + }); + + await transactionalEntityManager.insert(UsedUsername, { + createdAt: new Date(), + username: username.toLowerCase(), + }); + }); + + return account; + } +} diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index da71a5de6f..5f2ced77eb 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -1,87 +1,55 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import { DataSource, In, IsNull } from 'typeorm'; import * as Redis from 'ioredis'; -import { In, IsNull } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { IdService } from '@/core/IdService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { IdService } from '@/core/IdService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { UtilityService } from '@/core/UtilityService.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { Emoji } from '@/models/entities/Emoji.js'; +import type { EmojisRepository, Role } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; -import { DI } from '@/di-symbols.js'; import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; -import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; -import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js'; -import type { MiEmoji } from '@/models/Emoji.js'; -import type { Serialized } from '@/types.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import type { Config } from '@/config.js'; +import { query } from '@/misc/prelude/url.js'; +import type { Serialized } from '@/server/api/stream/types.js'; -const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/; - -export const fetchEmojisHostTypes = [ - 'local', - 'remote', - 'all', -] as const; -export type FetchEmojisHostTypes = typeof fetchEmojisHostTypes[number]; -export const fetchEmojisSortKeys = [ - '+id', - '-id', - '+updatedAt', - '-updatedAt', - '+name', - '-name', - '+host', - '-host', - '+uri', - '-uri', - '+publicUrl', - '-publicUrl', - '+type', - '-type', - '+aliases', - '-aliases', - '+category', - '-category', - '+license', - '-license', - '+isSensitive', - '-isSensitive', - '+localOnly', - '-localOnly', - '+roleIdsThatCanBeUsedThisEmojiAsReaction', - '-roleIdsThatCanBeUsedThisEmojiAsReaction', -] as const; -export type FetchEmojisSortKeys = typeof fetchEmojisSortKeys[number]; +const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; @Injectable() export class CustomEmojiService implements OnApplicationShutdown { - private emojisCache: MemoryKVCache; - public localEmojisCache: RedisSingleCache>; + private cache: MemoryKVCache; + public localEmojisCache: RedisSingleCache>; constructor( @Inject(DI.redis) private redisClient: Redis.Redis, + + @Inject(DI.config) + private config: Config, + + @Inject(DI.db) + private db: DataSource, + @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, + private utilityService: UtilityService, private idService: IdService, private emojiEntityService: EmojiEntityService, - private moderationLogService: ModerationLogService, private globalEventService: GlobalEventService, ) { - this.emojisCache = new MemoryKVCache(1000 * 60 * 60 * 12); // 12h + this.cache = new MemoryKVCache(1000 * 60 * 60 * 12); - this.localEmojisCache = new RedisSingleCache>(this.redisClient, 'localEmojis', { + this.localEmojisCache = new RedisSingleCache>(this.redisClient, 'localEmojis', { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60 * 3, // 3m fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))), toRedisConverter: (value) => JSON.stringify(Array.from(value.values())), fromRedisConverter: (value) => { - return new Map(JSON.parse(value).map((x: Serialized) => [x.name, { + if (!Array.isArray(JSON.parse(value))) return undefined; // 古いバージョンの壊れたキャッシュが残っていることがある(そのうち消す) + return new Map(JSON.parse(value).map((x: Serialized) => [x.name, { ...x, updatedAt: x.updatedAt ? new Date(x.updatedAt) : null, }])); @@ -91,9 +59,7 @@ export class CustomEmojiService implements OnApplicationShutdown { @bindThis public async add(data: { - originalUrl: string; - publicUrl: string; - fileType: string; + driveFile: DriveFile; name: string; category: string | null; aliases: string[]; @@ -101,23 +67,23 @@ export class CustomEmojiService implements OnApplicationShutdown { license: string | null; isSensitive: boolean; localOnly: boolean; - roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][]; - }, moderator?: MiUser): Promise { - const emoji = await this.emojisRepository.insertOne({ - id: this.idService.gen(), + roleIdsThatCanBeUsedThisEmojiAsReaction: Role['id'][]; + }): Promise { + const emoji = await this.emojisRepository.insert({ + id: this.idService.genId(), updatedAt: new Date(), name: data.name, category: data.category, host: data.host, aliases: data.aliases, - originalUrl: data.originalUrl, - publicUrl: data.publicUrl, - type: data.fileType, + originalUrl: data.driveFile.url, + publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url, + type: data.driveFile.webpublicType ?? data.driveFile.type, license: data.license, isSensitive: data.isSensitive, localOnly: data.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction, - }); + }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); if (data.host == null) { this.localEmojisCache.refresh(); @@ -125,48 +91,25 @@ export class CustomEmojiService implements OnApplicationShutdown { this.globalEventService.publishBroadcastStream('emojiAdded', { emoji: await this.emojiEntityService.packDetailed(emoji.id), }); - - if (moderator) { - this.moderationLogService.log(moderator, 'addCustomEmoji', { - emojiId: emoji.id, - emoji: emoji, - }); - } } return emoji; } @bindThis - public async update(data: ( - { id: MiEmoji['id'], name?: string; } | { name: string; id?: MiEmoji['id'], } - ) & { - originalUrl?: string; - publicUrl?: string; - fileType?: string; + public async update(id: Emoji['id'], data: { + driveFile?: DriveFile; + name?: string; category?: string | null; aliases?: string[]; license?: string | null; isSensitive?: boolean; localOnly?: boolean; - roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][]; - }, moderator?: MiUser): Promise< - null - | 'NO_SUCH_EMOJI' - | 'SAME_NAME_EMOJI_EXISTS' - > { - const emoji = data.id - ? await this.getEmojiById(data.id) - : await this.getEmojiByName(data.name!); - if (emoji === null) return 'NO_SUCH_EMOJI'; - const id = emoji.id; - - // IDと絵文字名が両方指定されている場合は絵文字名の変更を行うため重複チェックが必要 - const doNameUpdate = data.id && data.name && (data.name !== emoji.name); - if (doNameUpdate) { - const isDuplicate = await this.checkDuplicate(data.name!); - if (isDuplicate) return 'SAME_NAME_EMOJI_EXISTS'; - } + roleIdsThatCanBeUsedThisEmojiAsReaction?: Role['id'][]; + }): Promise { + const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); + const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() }); + if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists'); await this.emojisRepository.update(emoji.id, { updatedAt: new Date(), @@ -176,19 +119,19 @@ export class CustomEmojiService implements OnApplicationShutdown { license: data.license, isSensitive: data.isSensitive, localOnly: data.localOnly, - originalUrl: data.originalUrl, - publicUrl: data.publicUrl, - type: data.fileType, + originalUrl: data.driveFile != null ? data.driveFile.url : undefined, + publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, + type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined, }); this.localEmojisCache.refresh(); - const packed = await this.emojiEntityService.packDetailed(emoji.id); + const updated = await this.emojiEntityService.packDetailed(emoji.id); - if (!doNameUpdate) { + if (emoji.name === data.name) { this.globalEventService.publishBroadcastStream('emojiUpdated', { - emojis: [packed], + emojis: [updated], }); } else { this.globalEventService.publishBroadcastStream('emojiDeleted', { @@ -196,23 +139,13 @@ export class CustomEmojiService implements OnApplicationShutdown { }); this.globalEventService.publishBroadcastStream('emojiAdded', { - emoji: packed, - }); + emoji: updated, + }); } - - if (moderator) { - const updated = await this.emojisRepository.findOneByOrFail({ id: id }); - this.moderationLogService.log(moderator, 'updateCustomEmoji', { - emojiId: emoji.id, - before: emoji, - after: updated, - }); - } - return null; } @bindThis - public async addAliasesBulk(ids: MiEmoji['id'][], aliases: string[]) { + public async addAliasesBulk(ids: Emoji['id'][], aliases: string[]) { const emojis = await this.emojisRepository.findBy({ id: In(ids), }); @@ -232,7 +165,7 @@ export class CustomEmojiService implements OnApplicationShutdown { } @bindThis - public async setAliasesBulk(ids: MiEmoji['id'][], aliases: string[]) { + public async setAliasesBulk(ids: Emoji['id'][], aliases: string[]) { await this.emojisRepository.update({ id: In(ids), }, { @@ -248,7 +181,7 @@ export class CustomEmojiService implements OnApplicationShutdown { } @bindThis - public async removeAliasesBulk(ids: MiEmoji['id'][], aliases: string[]) { + public async removeAliasesBulk(ids: Emoji['id'][], aliases: string[]) { const emojis = await this.emojisRepository.findBy({ id: In(ids), }); @@ -261,14 +194,14 @@ export class CustomEmojiService implements OnApplicationShutdown { } this.localEmojisCache.refresh(); - + this.globalEventService.publishBroadcastStream('emojiUpdated', { emojis: await this.emojiEntityService.packDetailedMany(ids), }); } @bindThis - public async setCategoryBulk(ids: MiEmoji['id'][], category: string | null) { + public async setCategoryBulk(ids: Emoji['id'][], category: string | null) { await this.emojisRepository.update({ id: In(ids), }, { @@ -282,9 +215,9 @@ export class CustomEmojiService implements OnApplicationShutdown { emojis: await this.emojiEntityService.packDetailedMany(ids), }); } - + @bindThis - public async setLicenseBulk(ids: MiEmoji['id'][], license: string | null) { + public async setLicenseBulk(ids: Emoji['id'][], license: string | null) { await this.emojisRepository.update({ id: In(ids), }, { @@ -300,7 +233,7 @@ export class CustomEmojiService implements OnApplicationShutdown { } @bindThis - public async delete(id: MiEmoji['id'], moderator?: MiUser) { + public async delete(id: Emoji['id']) { const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); await this.emojisRepository.delete(emoji.id); @@ -310,30 +243,16 @@ export class CustomEmojiService implements OnApplicationShutdown { this.globalEventService.publishBroadcastStream('emojiDeleted', { emojis: [await this.emojiEntityService.packDetailed(emoji)], }); - - if (moderator) { - this.moderationLogService.log(moderator, 'deleteCustomEmoji', { - emojiId: emoji.id, - emoji: emoji, - }); - } } @bindThis - public async deleteBulk(ids: MiEmoji['id'][], moderator?: MiUser) { + public async deleteBulk(ids: Emoji['id'][]) { const emojis = await this.emojisRepository.findBy({ id: In(ids), }); for (const emoji of emojis) { await this.emojisRepository.delete(emoji.id); - - if (moderator) { - this.moderationLogService.log(moderator, 'deleteCustomEmoji', { - emojiId: emoji.id, - emoji: emoji, - }); - } } this.localEmojisCache.refresh(); @@ -345,7 +264,7 @@ export class CustomEmojiService implements OnApplicationShutdown { @bindThis private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null { - // クエリに使うホスト + // クエリに使うホスト let host = src === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ) : src === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない) : this.utilityService.isSelfHost(src) ? null // 自ホスト指定 @@ -383,10 +302,10 @@ export class CustomEmojiService implements OnApplicationShutdown { const queryOrNull = async () => (await this.emojisRepository.findOneBy({ name, - host, + host: host ?? IsNull(), })) ?? null; - const emoji = await this.emojisCache.fetch(`${name} ${host}`, queryOrNull); + const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull); if (emoji == null) return null; return emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) @@ -398,11 +317,10 @@ export class CustomEmojiService implements OnApplicationShutdown { @bindThis public async populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise> { const emojis = await Promise.all(emojiNames.map(x => this.populateEmoji(x, noteUserHost))); - const res = {} as Record; + const res = {} as any; for (let i = 0; i < emojiNames.length; i++) { - const resolvedEmoji = emojis[i]; - if (resolvedEmoji != null) { - res[emojiNames[i]] = resolvedEmoji; + if (emojis[i] != null) { + res[emojiNames[i]] = emojis[i]; } } return res; @@ -413,7 +331,7 @@ export class CustomEmojiService implements OnApplicationShutdown { */ @bindThis public async prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise { - const notCachedEmojis = emojis.filter(emoji => this.emojisCache.get(`${emoji.name} ${emoji.host}`) == null); + const notCachedEmojis = emojis.filter(emoji => this.cache.get(`${emoji.name} ${emoji.host}`) == null); const emojisQuery: any[] = []; const hosts = new Set(notCachedEmojis.map(e => e.host)); for (const host of hosts) { @@ -428,177 +346,13 @@ export class CustomEmojiService implements OnApplicationShutdown { select: ['name', 'host', 'originalUrl', 'publicUrl'], }) : []; for (const emoji of _emojis) { - this.emojisCache.set(`${emoji.name} ${emoji.host}`, emoji); + this.cache.set(`${emoji.name} ${emoji.host}`, emoji); } } - /** - * ローカル内の絵文字に重複がないかチェックします - * @param name 絵文字名 - */ - @bindThis - public checkDuplicate(name: string): Promise { - return this.emojisRepository.exists({ where: { name, host: IsNull() } }); - } - - @bindThis - public getEmojiById(id: string): Promise { - return this.emojisRepository.findOneBy({ id }); - } - - @bindThis - public getEmojiByName(name: string): Promise { - return this.emojisRepository.findOneBy({ name, host: IsNull() }); - } - - @bindThis - public async fetchEmojis( - params?: { - query?: { - updatedAtFrom?: string; - updatedAtTo?: string; - name?: string; - host?: string; - uri?: string; - publicUrl?: string; - type?: string; - aliases?: string; - category?: string; - license?: string; - isSensitive?: boolean; - localOnly?: boolean; - hostType?: FetchEmojisHostTypes; - roleIds?: string[]; - }, - sinceId?: string; - untilId?: string; - }, - opts?: { - limit?: number; - page?: number; - sortKeys?: FetchEmojisSortKeys[] - }, - ) { - function multipleWordsToQuery(words: string) { - return words.split(/\s/).filter(x => x.length > 0).map(x => `%${sqlLikeEscape(x)}%`); - } - - const builder = this.emojisRepository.createQueryBuilder('emoji'); - if (params?.query) { - const q = params.query; - if (q.updatedAtFrom) { - // noIndexScan - builder.andWhere('CAST(emoji.updatedAt AS DATE) >= :updateAtFrom', { updateAtFrom: q.updatedAtFrom }); - } - if (q.updatedAtTo) { - // noIndexScan - builder.andWhere('CAST(emoji.updatedAt AS DATE) <= :updateAtTo', { updateAtTo: q.updatedAtTo }); - } - if (q.name) { - builder.andWhere('emoji.name ~~ ANY(ARRAY[:...name])', { name: multipleWordsToQuery(q.name) }); - } - - switch (true) { - case q.hostType === 'local': { - builder.andWhere('emoji.host IS NULL'); - break; - } - case q.hostType === 'remote': { - if (q.host) { - // noIndexScan - builder.andWhere('emoji.host ~~ ANY(ARRAY[:...host])', { host: multipleWordsToQuery(q.host) }); - } else { - builder.andWhere('emoji.host IS NOT NULL'); - } - break; - } - } - - if (q.uri) { - // noIndexScan - builder.andWhere('emoji.uri ~~ ANY(ARRAY[:...uri])', { uri: multipleWordsToQuery(q.uri) }); - } - if (q.publicUrl) { - // noIndexScan - builder.andWhere('emoji.publicUrl ~~ ANY(ARRAY[:...publicUrl])', { publicUrl: multipleWordsToQuery(q.publicUrl) }); - } - if (q.type) { - // noIndexScan - builder.andWhere('emoji.type ~~ ANY(ARRAY[:...type])', { type: multipleWordsToQuery(q.type) }); - } - if (q.aliases) { - // noIndexScan - const subQueryBuilder = builder.subQuery() - .select('COUNT(0)', 'count') - .from( - sq2 => sq2 - .select('unnest(subEmoji.aliases)', 'alias') - .addSelect('subEmoji.id', 'id') - .from('emoji', 'subEmoji'), - 'aliasTable', - ) - .where('"emoji"."id" = "aliasTable"."id"') - .andWhere('"aliasTable"."alias" ~~ ANY(ARRAY[:...aliases])', { aliases: multipleWordsToQuery(q.aliases) }); - - builder.andWhere(`(${subQueryBuilder.getQuery()}) > 0`); - } - if (q.category) { - builder.andWhere('emoji.category ~~ ANY(ARRAY[:...category])', { category: multipleWordsToQuery(q.category) }); - } - if (q.license) { - // noIndexScan - builder.andWhere('emoji.license ~~ ANY(ARRAY[:...license])', { license: multipleWordsToQuery(q.license) }); - } - if (q.isSensitive != null) { - // noIndexScan - builder.andWhere('emoji.isSensitive = :isSensitive', { isSensitive: q.isSensitive }); - } - if (q.localOnly != null) { - // noIndexScan - builder.andWhere('emoji.localOnly = :localOnly', { localOnly: q.localOnly }); - } - if (q.roleIds && q.roleIds.length > 0) { - builder.andWhere('emoji.roleIdsThatCanBeUsedThisEmojiAsReaction && ARRAY[:...roleIds]::VARCHAR[]', { roleIds: q.roleIds }); - } - } - - if (params?.sinceId) { - builder.andWhere('emoji.id > :sinceId', { sinceId: params.sinceId }); - } - if (params?.untilId) { - builder.andWhere('emoji.id < :untilId', { untilId: params.untilId }); - } - - if (opts?.sortKeys && opts.sortKeys.length > 0) { - for (const sortKey of opts.sortKeys) { - const direction = sortKey.startsWith('-') ? 'DESC' : 'ASC'; - const key = sortKey.replace(/^[+-]/, ''); - builder.addOrderBy(`emoji.${key}`, direction); - } - } else { - builder.addOrderBy('emoji.id', 'DESC'); - } - - const limit = opts?.limit ?? 10; - if (opts?.page) { - builder.skip((opts.page - 1) * limit); - } - - builder.take(limit); - - const [emojis, count] = await builder.getManyAndCount(); - - return { - emojis, - count: (count > limit ? emojis.length : count), - allCount: count, - allPages: Math.ceil(count / limit), - }; - } - @bindThis public dispose(): void { - this.emojisCache.dispose(); + this.cache.dispose(); } @bindThis diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index 483f14ce7f..327283106f 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -1,38 +1,20 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import { Not, IsNull } from 'typeorm'; -import type { FollowingsRepository, MiMeta, MiUser, UsersRepository } from '@/models/_.js'; +import type { UsersRepository } from '@/models/index.js'; import { QueueService } from '@/core/QueueService.js'; +import { UserSuspendService } from '@/core/UserSuspendService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { SystemAccountService } from '@/core/SystemAccountService.js'; @Injectable() export class DeleteAccountService { constructor( - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - - private userEntityService: UserEntityService, - private apRendererService: ApRendererService, + private userSuspendService: UserSuspendService, private queueService: QueueService, private globalEventService: GlobalEventService, - private moderationLogService: ModerationLogService, - private systemAccountService: SystemAccountService, ) { } @@ -40,62 +22,19 @@ export class DeleteAccountService { public async deleteAccount(user: { id: string; host: string | null; - }, moderator?: MiUser): Promise { - if (this.meta.rootUserId === user.id) throw new Error('cannot delete a root account'); - + }): Promise { const _user = await this.usersRepository.findOneByOrFail({ id: user.id }); - - if (user.host === null && _user.username.includes('.')) { - throw new Error('cannot delete a system account'); - } - - if (moderator != null) { - this.moderationLogService.log(moderator, 'deleteAccount', { - userId: user.id, - userUsername: _user.username, - userHost: user.host, - }); - } + if (_user.isRoot) throw new Error('cannot delete a root account'); // 物理削除する前にDelete activityを送信する - if (this.userEntityService.isLocalUser(user)) { - // 知り得る全SharedInboxにDelete配信 - const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user)); - - const queue: string[] = []; - - const followings = await this.followingsRepository.find({ - where: [ - { followerSharedInbox: Not(IsNull()) }, - { followeeSharedInbox: Not(IsNull()) }, - ], - select: ['followerSharedInbox', 'followeeSharedInbox'], - }); - - const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); - - for (const inbox of inboxes) { - if (inbox != null && !queue.includes(inbox)) queue.push(inbox); - } - - for (const inbox of queue) { - this.queueService.deliver(user, content, inbox, true); - } - - this.queueService.createDeleteAccountJob(user, { - soft: false, - }); - } else { - // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する - this.queueService.createDeleteAccountJob(user, { - soft: true, - }); - } - + await this.userSuspendService.doPostSuspend(user).catch(e => {}); + + this.queueService.createDeleteAccountJob(user, { + soft: false, + }); + await this.usersRepository.update(user.id, { isDeleted: true, }); - - this.globalEventService.publishInternalEvent('userChangeDeletedState', { id: user.id, isDeleted: true }); } } diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index a2b74d1ab2..09039a8b57 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -1,11 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as fs from 'node:fs'; -import * as stream from 'node:stream/promises'; +import * as stream from 'node:stream'; +import * as util from 'node:util'; import { Inject, Injectable } from '@nestjs/common'; +import ipaddr from 'ipaddr.js'; import chalk from 'chalk'; import got, * as Got from 'got'; import { parse } from 'content-disposition'; @@ -17,6 +14,7 @@ import { StatusError } from '@/misc/status-error.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; +const pipeline = util.promisify(stream.pipeline); import { bindThis } from '@/decorators.js'; @Injectable() @@ -41,7 +39,7 @@ export class DownloadService { const timeout = 30 * 1000; const operationTimeout = 60 * 1000; - const maxSize = this.config.maxFileSize; + const maxSize = this.config.maxFileSize ?? 262144000; const urlObj = new URL(url); let filename = urlObj.pathname.split('/').pop() ?? 'untitled'; @@ -60,8 +58,8 @@ export class DownloadService { request: operationTimeout, // whole operation timeout }, agent: { - http: this.httpRequestService.getAgentForHttp(urlObj, true), - https: this.httpRequestService.getAgentForHttps(urlObj, true), + http: this.httpRequestService.httpAgent, + https: this.httpRequestService.httpsAgent, }, http2: false, // default retry: { @@ -69,6 +67,13 @@ export class DownloadService { }, enableUnixSockets: false, }).on('response', (res: Got.Response) => { + if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) { + if (this.isPrivateIp(res.ip)) { + this.logger.warn(`Blocked address: ${res.ip}`); + req.destroy(); + } + } + const contentLength = res.headers['content-length']; if (contentLength != null) { const size = Number(contentLength); @@ -97,7 +102,7 @@ export class DownloadService { }); try { - await stream.pipeline(req, fs.createWriteStream(path)); + await pipeline(req, fs.createWriteStream(path)); } catch (e) { if (e instanceof Got.HTTPError) { throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage); @@ -124,11 +129,24 @@ export class DownloadService { // write content at URL to temp file await this.downloadUrl(url, path); - const text = await fs.promises.readFile(path, 'utf8'); + const text = await util.promisify(fs.readFile)(path, 'utf8'); return text; } finally { cleanup(); } } + + @bindThis + private isPrivateIp(ip: string): boolean { + const parsedIp = ipaddr.parse(ip); + + for (const net of this.config.allowedPrivateNetworks ?? []) { + if (parsedIp.match(ipaddr.parseCIDR(net))) { + return false; + } + } + + return parsedIp.range() !== 'unicast'; + } } diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 1945c58e5b..1483b55469 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -1,21 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { randomUUID } from 'node:crypto'; import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; +import { v4 as uuid } from 'uuid'; import sharp from 'sharp'; -import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; -import { In, IsNull } from 'typeorm'; +import { sharpBmp } from 'sharp-read-bmp'; +import { IsNull } from 'typeorm'; import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js'; +import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import Logger from '@/logger.js'; -import type { MiRemoteUser, MiUser } from '@/models/User.js'; -import { MiDriveFile } from '@/models/DriveFile.js'; +import type { RemoteUser, User } from '@/models/entities/User.js'; +import { MetaService } from '@/core/MetaService.js'; +import { DriveFile } from '@/models/entities/DriveFile.js'; import { IdService } from '@/core/IdService.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; @@ -26,7 +22,7 @@ import { VideoProcessingService } from '@/core/VideoProcessingService.js'; import { ImageProcessingService } from '@/core/ImageProcessingService.js'; import type { IImage } from '@/core/ImageProcessingService.js'; import { QueueService } from '@/core/QueueService.js'; -import type { MiDriveFolder } from '@/models/DriveFolder.js'; +import type { DriveFolder } from '@/models/entities/DriveFolder.js'; import { createTemp } from '@/misc/create-temp.js'; import DriveChart from '@/core/chart/charts/drive.js'; import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; @@ -41,12 +37,10 @@ import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { correctFilename } from '@/misc/correct-filename.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { UtilityService } from '@/core/UtilityService.js'; type AddFileArgs = { /** User who wish to add file */ - user: { id: MiUser['id']; host: MiUser['host'] } | null; + user: { id: User['id']; host: User['host'] } | null; /** File path */ path: string; /** Name */ @@ -74,8 +68,8 @@ type AddFileArgs = { type UploadFromUrlArgs = { url: string; - user: { id: MiUser['id']; host: MiUser['host'] } | null; - folderId?: MiDriveFolder['id'] | null; + user: { id: User['id']; host: User['host'] } | null; + folderId?: DriveFolder['id'] | null; uri?: string | null; sensitive?: boolean; force?: boolean; @@ -87,9 +81,6 @@ type UploadFromUrlArgs = { @Injectable() export class DriveService { - public static NoSuchFolderError = class extends Error {}; - public static InvalidFileNameError = class extends Error {}; - public static CannotUnmarkSensitiveError = class extends Error {}; private registerLogger: Logger; private downloaderLogger: Logger; private deleteLogger: Logger; @@ -98,9 +89,6 @@ export class DriveService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -117,6 +105,7 @@ export class DriveService { private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, private idService: IdService, + private metaService: MetaService, private downloadService: DownloadService, private internalStorageService: InternalStorageService, private s3Service: S3Service, @@ -125,11 +114,9 @@ export class DriveService { private globalEventService: GlobalEventService, private queueService: QueueService, private roleService: RoleService, - private moderationLogService: ModerationLogService, private driveChart: DriveChart, private perUserDriveChart: PerUserDriveChart, private instanceChart: InstanceChart, - private utilityService: UtilityService, ) { const logger = new Logger('drive', 'blue'); this.registerLogger = logger.createSubLogger('register', 'yellow'); @@ -146,11 +133,13 @@ export class DriveService { * @param size Size for original */ @bindThis - private async save(file: MiDriveFile, path: string, name: string, type: string, hash: string, size: number): Promise { + private async save(file: DriveFile, path: string, name: string, type: string, hash: string, size: number): Promise { // thunbnail, webpublic を必要なら生成 const alts = await this.generateAlts(path, type, !file.uri); - if (this.meta.useObjectStorage) { + const meta = await this.metaService.fetch(); + + if (meta.useObjectStorage) { //#region ObjectStorage params let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']); @@ -169,12 +158,11 @@ export class DriveService { ext = ''; } - const baseUrl = this.meta.objectStorageBaseUrl - ?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`; + const baseUrl = meta.objectStorageBaseUrl + ?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`; // for original - const prefix = this.meta.objectStoragePrefix ? `${this.meta.objectStoragePrefix}/` : ''; - const key = `${prefix}${randomUUID()}${ext}`; + const key = `${meta.objectStoragePrefix}/${uuid()}${ext}`; const url = `${ baseUrl }/${ key }`; // for alts @@ -191,7 +179,7 @@ export class DriveService { ]; if (alts.webpublic) { - webpublicKey = `${prefix}webpublic-${randomUUID()}.${alts.webpublic.ext}`; + webpublicKey = `${meta.objectStoragePrefix}/webpublic-${uuid()}.${alts.webpublic.ext}`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`; this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); @@ -199,7 +187,7 @@ export class DriveService { } if (alts.thumbnail) { - thumbnailKey = `${prefix}thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; + thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${uuid()}.${alts.thumbnail.ext}`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); @@ -222,11 +210,11 @@ export class DriveService { file.size = size; file.storedInternal = false; - return await this.driveFilesRepository.insertOne(file); + return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); } else { // use internal storage - const accessKey = randomUUID(); - const thumbnailAccessKey = 'thumbnail-' + randomUUID(); - const webpublicAccessKey = 'webpublic-' + randomUUID(); + const accessKey = uuid(); + const thumbnailAccessKey = 'thumbnail-' + uuid(); + const webpublicAccessKey = 'webpublic-' + uuid(); const url = this.internalStorageService.saveFromPath(accessKey, path); @@ -256,7 +244,7 @@ export class DriveService { file.md5 = hash; file.size = size; - return await this.driveFilesRepository.insertOne(file); + return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); } } @@ -339,7 +327,7 @@ export class DriveService { this.registerLogger.debug('web image not created (not an required image)'); } } catch (err) { - this.registerLogger.warn('web image not created (an error occurred)', err as Error); + this.registerLogger.warn('web image not created (an error occured)', err as Error); } } else { if (satisfyWebpublic) this.registerLogger.info('web image not created (original satisfies webpublic)'); @@ -358,7 +346,7 @@ export class DriveService { thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 422); } } catch (err) { - this.registerLogger.warn('thumbnail not created (an error occurred)', err as Error); + this.registerLogger.warn('thumbnail not created (an error occured)', err as Error); } // #endregion thumbnail @@ -376,8 +364,10 @@ export class DriveService { if (type === 'image/apng') type = 'image/png'; if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream'; + const meta = await this.metaService.fetch(); + const params = { - Bucket: this.meta.objectStorageBucket, + Bucket: meta.objectStorageBucket, Key: key, Body: stream, ContentType: type, @@ -390,9 +380,9 @@ export class DriveService { // 許可されているファイル形式でしか拡張子をつけない ext ? correctFilename(filename, ext) : filename, ); - if (this.meta.objectStorageSetPublicRead) params.ACL = 'public-read'; + if (meta.objectStorageSetPublicRead) params.ACL = 'public-read'; - await this.s3Service.upload(this.meta, params) + await this.s3Service.upload(meta, params) .then( result => { if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput @@ -410,7 +400,7 @@ export class DriveService { // Expire oldest file (without avatar or banner) of remote user @bindThis - private async expireOldFile(user: MiRemoteUser, driveCapacity: number) { + private async expireOldFile(user: RemoteUser, driveCapacity: number) { const q = this.driveFilesRepository.createQueryBuilder('file') .where('file.userId = :userId', { userId: user.id }) .andWhere('file.isLink = FALSE'); @@ -456,34 +446,34 @@ export class DriveService { requestIp = null, requestHeaders = null, ext = null, - }: AddFileArgs): Promise { + }: AddFileArgs): Promise { let skipNsfwCheck = false; + const instance = await this.metaService.fetch(); const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw; if (user == null) { skipNsfwCheck = true; } else if (userRoleNSFW) { skipNsfwCheck = true; } - if (this.meta.sensitiveMediaDetection === 'none') skipNsfwCheck = true; - if (user && this.meta.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true; - if (user && this.meta.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true; + if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true; + if (user && instance.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true; + if (user && instance.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true; const info = await this.fileInfoService.getFileInfo(path, { - fileName: name, skipSensitiveDetection: skipNsfwCheck, sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる - this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 : - this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 : - this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 : - this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 : - 0.5, + instance.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 : + instance.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 : + instance.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 : + instance.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 : + 0.5, sensitiveThresholdForPorn: 0.75, - enableSensitiveMediaDetectionForVideos: this.meta.enableSensitiveMediaDetectionForVideos, + enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, }); this.registerLogger.info(`${JSON.stringify(info)}`); // 現状 false positive が多すぎて実用に耐えない - //if (info.porn && this.meta.disallowUploadWhenPredictedAsPorn) { + //if (info.porn && instance.disallowUploadWhenPredictedAsPorn) { // throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.'); //} @@ -497,51 +487,26 @@ export class DriveService { if (user && !force) { // Check if there is a file with the same hash - const matched = await this.driveFilesRepository.findOneBy({ + const much = await this.driveFilesRepository.findOneBy({ md5: info.md5, userId: user.id, }); - if (matched) { - this.registerLogger.info(`file with same hash is found: ${matched.id}`); - if (sensitive && !matched.isSensitive) { - // The file is federated as sensitive for this time, but was federated as non-sensitive before. - // Therefore, update the file to sensitive. - await this.driveFilesRepository.update({ id: matched.id }, { isSensitive: true }); - matched.isSensitive = true; - } - return matched; + if (much) { + this.registerLogger.info(`file with same hash is found: ${much.id}`); + return much; } } this.registerLogger.debug(`ADD DRIVE FILE: user ${user?.id ?? 'not set'}, name ${detectedName}, tmp ${path}`); - //#region Check drive usage and mime type + //#region Check drive usage if (user && !isLink) { - const isLocalUser = this.userEntityService.isLocalUser(user); - const policies = await this.roleService.getUserPolicies(user.id); - - const allowedMimeTypes = policies.uploadableFileTypes; - const isAllowed = allowedMimeTypes.some((mimeType) => { - if (mimeType === '*' || mimeType === '*/*') return true; - if (mimeType.endsWith('/*')) return info.type.mime.startsWith(mimeType.slice(0, -1)); - return info.type.mime === mimeType; - }); - if (!isAllowed) { - throw new IdentifiableError('bd71c601-f9b0-4808-9137-a330647ced9b', `Unallowed file type: ${info.type.mime}`); - } - - const driveCapacity = 1024 * 1024 * policies.driveCapacityMb; - const maxFileSize = 1024 * 1024 * policies.maxFileSizeMb; - - if (maxFileSize < info.size) { - if (isLocalUser) { - throw new IdentifiableError('f9e4e5f3-4df4-40b5-b400-f236945f7073', 'Max file size exceeded.'); - } - } - const usage = await this.driveFileEntityService.calcDriveUsageOf(user); + const isLocalUser = this.userEntityService.isLocalUser(user); + const policies = await this.roleService.getUserPolicies(user.id); + const driveCapacity = 1024 * 1024 * policies.driveCapacityMb; this.registerLogger.debug('drive capacity override applied'); this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); @@ -550,7 +515,7 @@ export class DriveService { if (isLocalUser) { throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.'); } - await this.expireOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as MiRemoteUser, driveCapacity - info.size); + await this.expireOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as RemoteUser, driveCapacity - info.size); } } //#endregion @@ -588,8 +553,9 @@ export class DriveService { const folder = await fetchFolder(); - let file = new MiDriveFile(); - file.id = this.idService.gen(); + let file = new DriveFile(); + file.id = this.idService.genId(); + file.createdAt = new Date(); file.userId = user ? user.id : null; file.userHost = user ? user.host : null; file.folderId = folder !== null ? folder.id : null; @@ -603,12 +569,13 @@ export class DriveService { file.maybePorn = info.porn; file.isSensitive = user ? this.userEntityService.isLocalUser(user) && profile!.alwaysMarkNsfw ? true : - sensitive ?? false + (sensitive !== null && sensitive !== undefined) + ? sensitive + : false : false; - if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true; if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; - if (info.sensitive && this.meta.setSensitiveFlagAutomatically) file.isSensitive = true; + if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true; if (userRoleNSFW) file.isSensitive = true; if (url !== null) { @@ -617,9 +584,9 @@ export class DriveService { if (isLink) { file.url = url; // ローカルプロキシ用 - file.accessKey = randomUUID(); - file.thumbnailAccessKey = 'thumbnail-' + randomUUID(); - file.webpublicAccessKey = 'webpublic-' + randomUUID(); + file.accessKey = uuid(); + file.thumbnailAccessKey = 'thumbnail-' + uuid(); + file.webpublicAccessKey = 'webpublic-' + uuid(); } } @@ -635,7 +602,7 @@ export class DriveService { file.type = info.type.mime; file.storedInternal = false; - file = await this.driveFilesRepository.insertOne(file); + file = await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); } catch (err) { // duplicate key error (when already registered) if (isDuplicateKeyValueError(err)) { @@ -644,7 +611,7 @@ export class DriveService { file = await this.driveFilesRepository.findOneBy({ uri: file.uri!, userId: user ? user.id : IsNull(), - }) as MiDriveFile; + }) as DriveFile; } else { this.registerLogger.error(err as Error); throw err; @@ -669,7 +636,7 @@ export class DriveService { // ローカルユーザーのみ this.perUserDriveChart.update(file, true); } else { - if (this.meta.enableChartsForFederatedInstances) { + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { this.instanceChart.updateDrive(file, true); } } @@ -678,78 +645,7 @@ export class DriveService { } @bindThis - public async updateFile(file: MiDriveFile, values: Partial, updater: MiUser) { - const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw; - - if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) { - throw new DriveService.InvalidFileNameError(); - } - - if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive && alwaysMarkNsfw && !values.isSensitive) { - throw new DriveService.CannotUnmarkSensitiveError(); - } - - if (values.folderId != null) { - const folder = await this.driveFoldersRepository.findOneBy({ - id: values.folderId, - userId: file.userId!, - }); - - if (folder == null) { - throw new DriveService.NoSuchFolderError(); - } - } - - await this.driveFilesRepository.update(file.id, values); - - const fileObj = await this.driveFileEntityService.pack(file.id, { self: true }); - - // Publish fileUpdated event - if (file.userId) { - this.globalEventService.publishDriveStream(file.userId, 'fileUpdated', fileObj); - } - - if (await this.roleService.isModerator(updater) && (file.userId !== updater.id)) { - if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive) { - const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null; - if (values.isSensitive) { - this.moderationLogService.log(updater, 'markSensitiveDriveFile', { - fileId: file.id, - fileUserId: file.userId, - fileUserUsername: user?.username ?? null, - fileUserHost: user?.host ?? null, - }); - } else { - this.moderationLogService.log(updater, 'unmarkSensitiveDriveFile', { - fileId: file.id, - fileUserId: file.userId, - fileUserUsername: user?.username ?? null, - fileUserHost: user?.host ?? null, - }); - } - } - } - - return fileObj; - } - - @bindThis - public async moveFiles(fileIds: MiDriveFile['id'][], folderId: MiDriveFolder['id'] | null, userId: MiUser['id']) { - const folder = folderId ? await this.driveFoldersRepository.findOneByOrFail({ - id: folderId, - userId: userId, - }) : null; - - await this.driveFilesRepository.update({ - id: In(fileIds), - userId: userId, - }, { - folderId: folder ? folder.id : null, - }); - } - - @bindThis - public async deleteFile(file: MiDriveFile, isExpired = false, deleter?: MiUser) { + public async deleteFile(file: DriveFile, isExpired = false) { if (file.storedInternal) { this.internalStorageService.del(file.accessKey!); @@ -772,11 +668,11 @@ export class DriveService { } } - this.deletePostProcess(file, isExpired, deleter); + this.deletePostProcess(file, isExpired); } @bindThis - public async deleteFileSync(file: MiDriveFile, isExpired = false, deleter?: MiUser) { + public async deleteFileSync(file: DriveFile, isExpired = false) { if (file.storedInternal) { this.internalStorageService.del(file.accessKey!); @@ -803,11 +699,11 @@ export class DriveService { await Promise.all(promises); } - this.deletePostProcess(file, isExpired, deleter); + this.deletePostProcess(file, isExpired); } @bindThis - private async deletePostProcess(file: MiDriveFile, isExpired = false, deleter?: MiUser) { + private async deletePostProcess(file: DriveFile, isExpired = false) { // リモートファイル期限切れ削除後は直リンクにする if (isExpired && file.userHost !== null && file.uri != null) { this.driveFilesRepository.update(file.id, { @@ -817,9 +713,9 @@ export class DriveService { webpublicUrl: null, storedInternal: false, // ローカルプロキシ用 - accessKey: randomUUID(), - thumbnailAccessKey: 'thumbnail-' + randomUUID(), - webpublicAccessKey: 'webpublic-' + randomUUID(), + accessKey: uuid(), + thumbnailAccessKey: 'thumbnail-' + uuid(), + webpublicAccessKey: 'webpublic-' + uuid(), }); } else { this.driveFilesRepository.delete(file.id); @@ -830,35 +726,22 @@ export class DriveService { // ローカルユーザーのみ this.perUserDriveChart.update(file, false); } else { - if (this.meta.enableChartsForFederatedInstances) { + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { this.instanceChart.updateDrive(file, false); } } - - if (file.userId) { - this.globalEventService.publishDriveStream(file.userId, 'fileDeleted', file.id); - } - - if (deleter && await this.roleService.isModerator(deleter) && (file.userId !== deleter.id)) { - const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null; - this.moderationLogService.log(deleter, 'deleteDriveFile', { - fileId: file.id, - fileUserId: file.userId, - fileUserUsername: user?.username ?? null, - fileUserHost: user?.host ?? null, - }); - } } @bindThis public async deleteObjectStorageFile(key: string) { + const meta = await this.metaService.fetch(); try { const param = { - Bucket: this.meta.objectStorageBucket, + Bucket: meta.objectStorageBucket, Key: key, } as DeleteObjectCommandInput; - await this.s3Service.delete(this.meta, param); + await this.s3Service.delete(meta, param); } catch (err: any) { if (err.name === 'NoSuchKey') { this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error); @@ -883,7 +766,7 @@ export class DriveService { comment = null, requestIp = null, requestHeaders = null, - }: UploadFromUrlArgs): Promise { + }: UploadFromUrlArgs): Promise { // Create temp file const [path, cleanup] = await createTemp(); diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 45d7ea11e4..59932a5b88 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -1,21 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { URLSearchParams } from 'node:url'; import * as nodemailer from 'nodemailer'; -import juice from 'juice'; import { Inject, Injectable } from '@nestjs/common'; import { validate as validateEmail } from 'deep-email-validator'; -import { UtilityService } from '@/core/UtilityService.js'; +import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; -import type { MiMeta, UserProfilesRepository } from '@/models/_.js'; +import type { UserProfilesRepository } from '@/models/index.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; -import { HttpRequestService } from '@/core/HttpRequestService.js'; @Injectable() export class EmailService { @@ -25,41 +17,44 @@ export class EmailService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + private metaService: MetaService, private loggerService: LoggerService, - private utilityService: UtilityService, - private httpRequestService: HttpRequestService, ) { this.logger = this.loggerService.getLogger('email'); } @bindThis public async sendEmail(to: string, subject: string, html: string, text: string) { - if (!this.meta.enableEmail) return; - + const meta = await this.metaService.fetch(true); + const iconUrl = `${this.config.url}/static-assets/mi-white.png`; const emailSettingUrl = `${this.config.url}/settings/email`; - - const enableAuth = this.meta.smtpUser != null && this.meta.smtpUser !== ''; - + + const enableAuth = meta.smtpUser != null && meta.smtpUser !== ''; + const transporter = nodemailer.createTransport({ - host: this.meta.smtpHost, - port: this.meta.smtpPort, - secure: this.meta.smtpSecure, + host: meta.smtpHost, + port: meta.smtpPort, + secure: meta.smtpSecure, ignoreTLS: !enableAuth, proxy: this.config.proxySmtp, auth: enableAuth ? { - user: this.meta.smtpUser, - pass: this.meta.smtpPass, + user: meta.smtpUser, + pass: meta.smtpPass, } : undefined, } as any); - - const htmlContent = ` + + try { + // TODO: htmlサニタイズ + const info = await transporter.sendMail({ + from: meta.email!, + to: to, + subject: subject, + text: text, + html: ` @@ -124,7 +119,7 @@ export class EmailService {
- +

${ subject }

@@ -138,20 +133,9 @@ export class EmailService { ${ this.config.host } -`; - - const inlinedHtml = juice(htmlContent); - - try { - // TODO: htmlサニタイズ - const info = await transporter.sendMail({ - from: this.meta.email!, - to: to, - subject: subject, - text: text, - html: inlinedHtml, +`, }); - + this.logger.info(`Message sent: ${info.messageId}`); } catch (err) { this.logger.error(err as Error); @@ -162,212 +146,35 @@ export class EmailService { @bindThis public async validateEmailForAccount(emailAddress: string): Promise<{ available: boolean; - reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist'; + reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp'; }> { - if (!this.utilityService.validateEmailFormat(emailAddress)) { - return { - available: false, - reason: 'format', - }; - } - + const meta = await this.metaService.fetch(); + const exist = await this.userProfilesRepository.countBy({ emailVerified: true, email: emailAddress, }); - - if (exist !== 0) { - return { - available: false, - reason: 'used', - }; - } - - let validated: { - valid: boolean, - reason?: string | null, - } = { valid: true, reason: null }; - - if (this.meta.enableActiveEmailValidation) { - if (this.meta.enableVerifymailApi && this.meta.verifymailAuthKey != null) { - validated = await this.verifyMail(emailAddress, this.meta.verifymailAuthKey); - } else if (this.meta.enableTruemailApi && this.meta.truemailInstance && this.meta.truemailAuthKey != null) { - validated = await this.trueMail(this.meta.truemailInstance, emailAddress, this.meta.truemailAuthKey); - } else { - validated = await validateEmail({ - email: emailAddress, - validateRegex: true, - validateMx: true, - validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので - validateDisposable: true, // 捨てアドかどうかチェック - validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので - }); - } - } - - if (!validated.valid) { - const formatReason: Record = { - regex: 'format', - disposable: 'disposable', - mx: 'mx', - smtp: 'smtp', - network: 'network', - blacklist: 'blacklist', - }; - - return { - available: false, - reason: validated.reason ? formatReason[validated.reason] ?? null : null, - }; - } - - const emailDomain: string = emailAddress.split('@')[1]; - const isBanned = this.utilityService.isBlockedHost(this.meta.bannedEmailDomains, emailDomain); - - if (isBanned) { - return { - available: false, - reason: 'banned', - }; - } - + + const validated = meta.enableActiveEmailValidation ? await validateEmail({ + email: emailAddress, + validateRegex: true, + validateMx: true, + validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので + validateDisposable: true, // 捨てアドかどうかチェック + validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので + }) : { valid: true, reason: null }; + + const available = exist === 0 && validated.valid; + return { - available: true, - reason: null, + available, + reason: available ? null : + exist !== 0 ? 'used' : + validated.reason === 'regex' ? 'format' : + validated.reason === 'disposable' ? 'disposable' : + validated.reason === 'mx' ? 'mx' : + validated.reason === 'smtp' ? 'smtp' : + null, }; } - - private async verifyMail(emailAddress: string, verifymailAuthKey: string): Promise<{ - valid: boolean; - reason: 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | null; - }> { - const endpoint = 'https://verifymail.io/api/' + emailAddress + '?key=' + verifymailAuthKey; - const res = await this.httpRequestService.send(endpoint, { - method: 'GET', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Accept: 'application/json, */*', - }, - }); - - const json = (await res.json()) as Partial<{ - message: string; - block: boolean; - catch_all: boolean; - deliverable_email: boolean; - disposable: boolean; - domain: string; - email_address: string; - email_provider: string; - mx: boolean; - mx_fallback: boolean; - mx_host: string[]; - mx_ip: string[]; - mx_priority: { [key: string]: number }; - privacy: boolean; - related_domains: string[]; - }>; - - /* api error: when there is only one `message` attribute in the returned result */ - if (Object.keys(json).length === 1 && Reflect.has(json, 'message')) { - return { - valid: false, - reason: null, - }; - } - if (json.email_address === undefined) { - return { - valid: false, - reason: 'format', - }; - } - if (json.deliverable_email !== undefined && !json.deliverable_email) { - return { - valid: false, - reason: 'smtp', - }; - } - if (json.disposable) { - return { - valid: false, - reason: 'disposable', - }; - } - if (json.mx !== undefined && !json.mx) { - return { - valid: false, - reason: 'mx', - }; - } - - return { - valid: true, - reason: null, - }; - } - - private async trueMail(truemailInstance: string, emailAddress: string, truemailAuthKey: string): Promise<{ - valid: boolean; - reason: 'used' | 'format' | 'blacklist' | 'mx' | 'smtp' | 'network' | T | null; - }> { - const endpoint = truemailInstance + '?email=' + emailAddress; - try { - const res = await this.httpRequestService.send(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - Authorization: truemailAuthKey, - }, - isLocalAddressAllowed: true, - }); - - const json = (await res.json()) as { - email: string; - success: boolean; - error?: string; - errors?: { - list_match?: string; - regex?: string; - mx?: string; - smtp?: string; - } | null; - }; - - if (json.email === undefined || json.errors?.regex) { - return { - valid: false, - reason: 'format', - }; - } - if (json.errors?.smtp) { - return { - valid: false, - reason: 'smtp', - }; - } - if (json.errors?.mx) { - return { - valid: false, - reason: 'mx', - }; - } - if (!json.success) { - return { - valid: false, - reason: json.errors?.list_match as T || 'blacklist', - }; - } - - return { - valid: true, - reason: null, - }; - } catch (error) { - return { - valid: false, - reason: 'network', - }; - } - } } diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts deleted file mode 100644 index 97b617096a..0000000000 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ /dev/null @@ -1,219 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiNote } from '@/models/Note.js'; -import type { MiMeta } from '@/models/Meta.js'; -import { Packed } from '@/misc/json-schema.js'; -import type { NotesRepository } from '@/models/_.js'; -import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; -import { CacheService } from '@/core/CacheService.js'; -import { isReply } from '@/misc/is-reply.js'; -import { isInstanceMuted } from '@/misc/is-instance-muted.js'; - -type TimelineOptions = { - untilId: string | null, - sinceId: string | null, - limit: number, - allowPartial: boolean, - me?: { id: MiUser['id'] } | undefined | null, - useDbFallback: boolean, - redisTimelines: FanoutTimelineName[], - noteFilter?: (note: MiNote) => boolean, - alwaysIncludeMyNotes?: boolean; - ignoreAuthorFromBlock?: boolean; - ignoreAuthorFromMute?: boolean; - ignoreAuthorFromInstanceBlock?: boolean; - excludeNoFiles?: boolean; - excludeReplies?: boolean; - excludePureRenotes: boolean; - ignoreAuthorFromUserSuspension?: boolean; - dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise, -}; - -@Injectable() -export class FanoutTimelineEndpointService { - constructor( - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - @Inject(DI.meta) - private meta: MiMeta, - - private noteEntityService: NoteEntityService, - private cacheService: CacheService, - private fanoutTimelineService: FanoutTimelineService, - private utilityService: UtilityService, - ) { - } - - @bindThis - async timeline(ps: TimelineOptions): Promise[]> { - return await this.noteEntityService.packMany(await this.getMiNotes(ps), ps.me); - } - - @bindThis - async getMiNotes(ps: TimelineOptions): Promise { - // 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える - if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]); - - const ascending = ps.sinceId && !ps.untilId; - const idCompare: (a: string, b: string) => number = ascending ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1; - - const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId); - - // TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい - const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare); - - let noteIds = redisResultIds.slice(0, ps.limit); - const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1]; - const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId; - - if (!shouldFallbackToDb) { - let filter = ps.noteFilter ?? (_note => true); - - if (ps.alwaysIncludeMyNotes && ps.me) { - const me = ps.me; - const parentFilter = filter; - filter = (note) => note.userId === me.id || parentFilter(note); - } - - if (ps.excludeNoFiles) { - const parentFilter = filter; - filter = (note) => note.fileIds.length !== 0 && parentFilter(note); - } - - if (ps.excludeReplies) { - const parentFilter = filter; - filter = (note) => !isReply(note, ps.me?.id) && parentFilter(note); - } - - if (ps.excludePureRenotes) { - const parentFilter = filter; - filter = (note) => (!isRenote(note) || isQuote(note)) && parentFilter(note); - } - - if (ps.me) { - const me = ps.me; - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - userMutedInstances, - ] = await Promise.all([ - this.cacheService.userMutingsCache.fetch(ps.me.id), - this.cacheService.renoteMutingsCache.fetch(ps.me.id), - this.cacheService.userBlockedCache.fetch(ps.me.id), - this.cacheService.userProfileCache.fetch(me.id).then(p => new Set(p.mutedInstances)), - ]); - - const parentFilter = filter; - filter = (note) => { - if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false; - if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false; - if (isUserRelated(note.renote, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false; - if (isUserRelated(note.renote, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false; - if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false; - if (isInstanceMuted(note, userMutedInstances)) return false; - - return parentFilter(note); - }; - } - - { - const parentFilter = filter; - filter = (note) => { - if (!ps.ignoreAuthorFromInstanceBlock) { - if (this.utilityService.isBlockedHost(this.meta.blockedHosts, note.userHost)) return false; - } - if (note.userId !== note.renoteUserId && this.utilityService.isBlockedHost(this.meta.blockedHosts, note.renoteUserHost)) return false; - if (note.userId !== note.replyUserId && this.utilityService.isBlockedHost(this.meta.blockedHosts, note.replyUserHost)) return false; - - return parentFilter(note); - }; - } - - { - const parentFilter = filter; - filter = (note) => { - const noteJoined = note as MiNote & { - renoteUser: MiUser | null; - replyUser: MiUser | null; - }; - if (!ps.ignoreAuthorFromUserSuspension) { - if (note.user!.isSuspended) return false; - } - if (note.userId !== note.renoteUserId && noteJoined.renoteUser?.isSuspended) return false; - if (note.userId !== note.replyUserId && noteJoined.replyUser?.isSuspended) return false; - - return parentFilter(note); - }; - } - - const redisTimeline: MiNote[] = []; - let readFromRedis = 0; - let lastSuccessfulRate = 1; // rateをキャッシュする? - - while ((redisResultIds.length - readFromRedis) !== 0) { - const remainingToRead = ps.limit - redisTimeline.length; - - // DBからの取り直しを減らす初回と同じ割合以上で成功すると仮定するが、クエリの長さを考えて三倍まで - const countToGet = Math.ceil(remainingToRead * Math.min(1.1 / lastSuccessfulRate, 3)); - noteIds = redisResultIds.slice(readFromRedis, readFromRedis + countToGet); - - readFromRedis += noteIds.length; - - const gotFromDb = await this.getAndFilterFromDb(noteIds, filter, idCompare); - redisTimeline.push(...gotFromDb); - lastSuccessfulRate = gotFromDb.length / noteIds.length; - - if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) { - // 十分Redisからとれた - return redisTimeline.slice(0, ps.limit); - } - } - - // まだ足りない分はDBにフォールバック - const remainingToRead = ps.limit - redisTimeline.length; - let dbUntil: string | null; - let dbSince: string | null; - if (ascending) { - dbUntil = ps.untilId; - dbSince = noteIds[noteIds.length - 1]; - } else { - dbUntil = noteIds[noteIds.length - 1]; - dbSince = ps.sinceId; - } - const gotFromDb = await ps.dbFallback(dbUntil, dbSince, remainingToRead); - return [...redisTimeline, ...gotFromDb]; - } - - return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit); - } - - private async getAndFilterFromDb(noteIds: string[], noteFilter: (note: MiNote) => boolean, idCompare: (a: string, b: string) => number): Promise { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - const notes = (await query.getMany()).filter(noteFilter); - - notes.sort((a, b) => idCompare(a.id, b.id)); - - return notes; - } -} diff --git a/packages/backend/src/core/FanoutTimelineService.ts b/packages/backend/src/core/FanoutTimelineService.ts deleted file mode 100644 index 24999bf4da..0000000000 --- a/packages/backend/src/core/FanoutTimelineService.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; - -export type FanoutTimelineName = ( - // home timeline - | `homeTimeline:${string}` - | `homeTimelineWithFiles:${string}` // only notes with files are included - // local timeline - | `localTimeline` // replies are not included - | `localTimelineWithFiles` // only non-reply notes with files are included - | `localTimelineWithReplies` // only replies are included - | `localTimelineWithReplyTo:${string}` // Only replies to specific local user are included. Parameter is reply user id. - - // antenna - | `antennaTimeline:${string}` - - // user timeline - | `userTimeline:${string}` // replies are not included - | `userTimelineWithFiles:${string}` // only non-reply notes with files are included - | `userTimelineWithReplies:${string}` // only replies are included - | `userTimelineWithChannel:${string}` // only channel notes are included, replies are included - - // user list timelines - | `userListTimeline:${string}` - | `userListTimelineWithFiles:${string}` // only notes with files are included - - // channel timelines - | `channelTimeline:${string}` // replies are included - - // role timelines - | `roleTimeline:${string}` // any notes are included -); - -@Injectable() -export class FanoutTimelineService { - constructor( - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, - - private idService: IdService, - ) { - } - - @bindThis - public push(tl: FanoutTimelineName, id: string, maxlen: number, pipeline: Redis.ChainableCommander) { - // リモートから遅れて届いた(もしくは後から追加された)投稿日時が古い投稿が追加されるとページネーション時に問題を引き起こすため、 - // 3分以内に投稿されたものでない場合、Redisにある最古のIDより新しい場合のみ追加する - if (this.idService.parse(id).date.getTime() > Date.now() - 1000 * 60 * 3) { - pipeline.lpush('list:' + tl, id); - if (Math.random() < 0.1) { // 10%の確率でトリム - pipeline.ltrim('list:' + tl, 0, maxlen - 1); - } - } else { - // 末尾のIDを取得 - this.redisForTimelines.lindex('list:' + tl, -1).then(lastId => { - if (lastId == null || (this.idService.parse(id).date.getTime() > this.idService.parse(lastId).date.getTime())) { - this.redisForTimelines.lpush('list:' + tl, id); - } else { - Promise.resolve(); - } - }); - } - } - - @bindThis - public get(name: FanoutTimelineName, untilId?: string | null, sinceId?: string | null) { - if (untilId && sinceId) { - return this.redisForTimelines.lrange('list:' + name, 0, -1) - .then(ids => ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1)); - } else if (untilId) { - return this.redisForTimelines.lrange('list:' + name, 0, -1) - .then(ids => ids.filter(id => id < untilId).sort((a, b) => a > b ? -1 : 1)); - } else if (sinceId) { - return this.redisForTimelines.lrange('list:' + name, 0, -1) - .then(ids => ids.filter(id => id > sinceId).sort((a, b) => a < b ? -1 : 1)); - } else { - return this.redisForTimelines.lrange('list:' + name, 0, -1) - .then(ids => ids.sort((a, b) => a > b ? -1 : 1)); - } - } - - @bindThis - public getMulti(name: FanoutTimelineName[], untilId?: string | null, sinceId?: string | null): Promise { - const pipeline = this.redisForTimelines.pipeline(); - for (const n of name) { - pipeline.lrange('list:' + n, 0, -1); - } - return pipeline.exec().then(res => { - if (res == null) return []; - const tls = res.map(r => r[1] as string[]); - return tls.map(ids => - (untilId && sinceId) - ? ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1) - : untilId - ? ids.filter(id => id < untilId).sort((a, b) => a > b ? -1 : 1) - : sinceId - ? ids.filter(id => id > sinceId).sort((a, b) => a < b ? -1 : 1) - : ids.sort((a, b) => a > b ? -1 : 1), - ); - }); - } - - @bindThis - public purge(name: FanoutTimelineName) { - return this.redisForTimelines.del('list:' + name); - } -} diff --git a/packages/backend/src/core/FeaturedService.ts b/packages/backend/src/core/FeaturedService.ts deleted file mode 100644 index b3335e38da..0000000000 --- a/packages/backend/src/core/FeaturedService.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import type { MiGalleryPost, MiNote, MiUser } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; - -const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと -export const GALLERY_POSTS_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと -const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと -const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 1時間ごと - -const featuredEpoc = new Date('2023-01-01T00:00:00Z').getTime(); - -@Injectable() -export class FeaturedService { - constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする - ) { - } - - @bindThis - private getCurrentWindow(windowRange: number): number { - const passed = new Date().getTime() - featuredEpoc; - return Math.floor(passed / windowRange); - } - - @bindThis - private async updateRankingOf(name: string, windowRange: number, element: string, score = 1): Promise { - const currentWindow = this.getCurrentWindow(windowRange); - const redisTransaction = this.redisClient.multi(); - redisTransaction.zincrby( - `${name}:${currentWindow}`, - score, - element); - redisTransaction.expire( - `${name}:${currentWindow}`, - (windowRange * 3) / 1000, - 'NX'); // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定 - await redisTransaction.exec(); - } - - @bindThis - private async getRankingOf(name: string, windowRange: number, threshold: number): Promise { - const currentWindow = this.getCurrentWindow(windowRange); - const previousWindow = currentWindow - 1; - - const redisPipeline = this.redisClient.pipeline(); - redisPipeline.zrange( - `${name}:${currentWindow}`, 0, threshold, 'REV', 'WITHSCORES'); - redisPipeline.zrange( - `${name}:${previousWindow}`, 0, threshold, 'REV', 'WITHSCORES'); - const [currentRankingResult, previousRankingResult] = await redisPipeline.exec().then(result => result ? result.map(r => (r[1] ?? []) as string[]) : [[], []]); - - const ranking = new Map(); - for (let i = 0; i < currentRankingResult.length; i += 2) { - const noteId = currentRankingResult[i]; - const score = parseInt(currentRankingResult[i + 1], 10); - ranking.set(noteId, score); - } - for (let i = 0; i < previousRankingResult.length; i += 2) { - const noteId = previousRankingResult[i]; - const score = parseInt(previousRankingResult[i + 1], 10); - const exist = ranking.get(noteId); - if (exist != null) { - ranking.set(noteId, (exist + score) / 2); - } else { - ranking.set(noteId, score); - } - } - - return Array.from(ranking.keys()); - } - - @bindThis - private async removeFromRanking(name: string, windowRange: number, element: string): Promise { - const currentWindow = this.getCurrentWindow(windowRange); - const previousWindow = currentWindow - 1; - - const redisPipeline = this.redisClient.pipeline(); - redisPipeline.zrem(`${name}:${currentWindow}`, element); - redisPipeline.zrem(`${name}:${previousWindow}`, element); - await redisPipeline.exec(); - } - - @bindThis - public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise { - return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score); - } - - @bindThis - public updateGalleryPostsRanking(galleryPostId: MiGalleryPost['id'], score = 1): Promise { - return this.updateRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, galleryPostId, score); - } - - @bindThis - public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise { - return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score); - } - - @bindThis - public updatePerUserNotesRanking(userId: MiUser['id'], noteId: MiNote['id'], score = 1): Promise { - return this.updateRankingOf(`featuredPerUserNotesRanking:${userId}`, PER_USER_NOTES_RANKING_WINDOW, noteId, score); - } - - @bindThis - public updateHashtagsRanking(hashtag: string, score = 1): Promise { - return this.updateRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, hashtag, score); - } - - @bindThis - public getGlobalNotesRanking(threshold: number): Promise { - return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, threshold); - } - - @bindThis - public getGalleryPostsRanking(threshold: number): Promise { - return this.getRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, threshold); - } - - @bindThis - public getInChannelNotesRanking(channelId: MiNote['channelId'], threshold: number): Promise { - return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, threshold); - } - - @bindThis - public getPerUserNotesRanking(userId: MiUser['id'], threshold: number): Promise { - return this.getRankingOf(`featuredPerUserNotesRanking:${userId}`, PER_USER_NOTES_RANKING_WINDOW, threshold); - } - - @bindThis - public getHashtagsRanking(threshold: number): Promise { - return this.getRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, threshold); - } - - @bindThis - public removeHashtagsFromRanking(hashtag: string): Promise { - return this.removeFromRanking('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, hashtag); - } -} diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts index 73bbf03b26..3603d59dcc 100644 --- a/packages/backend/src/core/FederatedInstanceService.ts +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { InstancesRepository } from '@/models/_.js'; -import type { MiInstance } from '@/models/Instance.js'; +import type { InstancesRepository } from '@/models/index.js'; +import type { Instance } from '@/models/entities/Instance.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; @@ -15,7 +10,7 @@ import { bindThis } from '@/decorators.js'; @Injectable() export class FederatedInstanceService implements OnApplicationShutdown { - public federatedInstanceCache: RedisKVCache; + public federatedInstanceCache: RedisKVCache; constructor( @Inject(DI.redis) @@ -27,7 +22,7 @@ export class FederatedInstanceService implements OnApplicationShutdown { private utilityService: UtilityService, private idService: IdService, ) { - this.federatedInstanceCache = new RedisKVCache(this.redisClient, 'federatedInstance', { + this.federatedInstanceCache = new RedisKVCache(this.redisClient, 'federatedInstance', { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60 * 3, // 3m fetcher: (key) => this.instancesRepository.findOneBy({ host: key }), @@ -40,28 +35,27 @@ export class FederatedInstanceService implements OnApplicationShutdown { firstRetrievedAt: new Date(parsed.firstRetrievedAt), latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null, infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null, - notRespondingSince: parsed.notRespondingSince ? new Date(parsed.notRespondingSince) : null, }; }, }); } @bindThis - public async fetchOrRegister(host: string): Promise { + public async fetch(host: string): Promise { host = this.utilityService.toPuny(host); - + const cached = await this.federatedInstanceCache.get(host); if (cached) return cached; - + const index = await this.instancesRepository.findOneBy({ host }); - + if (index == null) { - const i = await this.instancesRepository.insertOne({ - id: this.idService.gen(), + const i = await this.instancesRepository.insert({ + id: this.idService.genId(), host, firstRetrievedAt: new Date(), - }); - + }).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0])); + this.federatedInstanceCache.set(host, i); return i; } else { @@ -71,25 +65,7 @@ export class FederatedInstanceService implements OnApplicationShutdown { } @bindThis - public async fetch(host: string): Promise { - host = this.utilityService.toPuny(host); - - const cached = await this.federatedInstanceCache.get(host); - if (cached !== undefined) return cached; - - const index = await this.instancesRepository.findOneBy({ host }); - - if (index == null) { - this.federatedInstanceCache.set(host, null); - return null; - } else { - this.federatedInstanceCache.set(host, index); - return index; - } - } - - @bindThis - public async update(id: MiInstance['id'], data: Partial): Promise { + public async update(id: Instance['id'], data: Partial): Promise { const result = await this.instancesRepository.createQueryBuilder().update() .set(data) .where('id = :id', { id }) @@ -98,7 +74,7 @@ export class FederatedInstanceService implements OnApplicationShutdown { .then((response) => { return response.raw[0]; }); - + this.federatedInstanceCache.set(result.host, result); } diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index ce3af7c774..9de633350b 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -1,14 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import { JSDOM } from 'jsdom'; import tinycolor from 'tinycolor2'; -import * as Redis from 'ioredis'; -import type { MiInstance } from '@/models/Instance.js'; +import type { Instance } from '@/models/entities/Instance.js'; +import type { InstancesRepository } from '@/models/index.js'; +import { AppLockService } from '@/core/AppLockService.js'; import type Logger from '@/logger.js'; import { DI } from '@/di-symbols.js'; import { LoggerService } from '@/core/LoggerService.js'; @@ -41,63 +37,39 @@ export class FetchInstanceMetadataService { private logger: Logger; constructor( + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + private appLockService: AppLockService, private httpRequestService: HttpRequestService, private loggerService: LoggerService, private federatedInstanceService: FederatedInstanceService, - @Inject(DI.redis) - private redisClient: Redis.Redis, ) { this.logger = this.loggerService.getLogger('metadata', 'cyan'); } @bindThis - // public for test - public async tryLock(host: string): Promise { - // TODO: マイグレーションなのであとで消す (2024.3.1) - this.redisClient.del(`fetchInstanceMetadata:mutex:${host}`); - - return await this.redisClient.set( - `fetchInstanceMetadata:mutex:v2:${host}`, '1', - 'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395 - 'GET' // 古い値を返す(なかったらnull) - ); - } - - @bindThis - // public for test - public unlock(host: string): Promise { - return this.redisClient.del(`fetchInstanceMetadata:mutex:v2:${host}`); - } - - @bindThis - public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise { - const host = instance.host; - - // finallyでunlockされてしまうのでtry内でロックチェックをしない - // (returnであってもfinallyは実行される) - if (!force && await this.tryLock(host) === '1') { - // 1が返ってきていたらロックされているという意味なので、何もしない - return; - } - - try { - if (!force) { - const _instance = await this.federatedInstanceService.fetchOrRegister(host); - const now = Date.now(); - if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) { - // unlock at the finally caluse - return; - } + public async fetchInstanceMetadata(instance: Instance, force = false): Promise { + const unlock = await this.appLockService.getFetchInstanceMetadataLock(instance.host); + + if (!force) { + const _instance = await this.instancesRepository.findOneBy({ host: instance.host }); + const now = Date.now(); + if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) { + unlock(); + return; } - - this.logger.info(`Fetching metadata of ${instance.host} ...`); - + } + + this.logger.info(`Fetching metadata of ${instance.host} ...`); + + try { const [info, dom, manifest] = await Promise.all([ this.fetchNodeinfo(instance).catch(() => null), this.fetchDom(instance).catch(() => null), this.fetchManifest(instance).catch(() => null), ]); - + const [favicon, icon, themeColor, name, description] = await Promise.all([ this.fetchFaviconUrl(instance, dom).catch(() => null), this.fetchIconUrl(instance, dom, manifest).catch(() => null), @@ -105,13 +77,13 @@ export class FetchInstanceMetadataService { this.getSiteName(info, dom, manifest).catch(() => null), this.getDescription(info, dom, manifest).catch(() => null), ]); - + this.logger.succ(`Successfuly fetched metadata of ${instance.host}`); - + const updates = { infoUpdatedAt: new Date(), } as Record; - + if (info) { updates.softwareName = typeof info.software?.name === 'string' ? info.software.name.toLowerCase() : '?'; updates.softwareVersion = info.software?.version; @@ -119,27 +91,27 @@ export class FetchInstanceMetadataService { updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null; updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null; } - + if (name) updates.name = name; if (description) updates.description = description; - if (icon ?? favicon) updates.iconUrl = (icon && !icon.includes('data:image/png;base64')) ? icon : favicon; + if (icon || favicon) updates.iconUrl = icon ?? favicon; if (favicon) updates.faviconUrl = favicon; if (themeColor) updates.themeColor = themeColor; - + await this.federatedInstanceService.update(instance.id, updates); - + this.logger.succ(`Successfuly updated metadata of ${instance.host}`); } catch (e) { this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`); } finally { - await this.unlock(host); + unlock(); } } @bindThis - private async fetchNodeinfo(instance: MiInstance): Promise { + private async fetchNodeinfo(instance: Instance): Promise { this.logger.info(`Fetching nodeinfo of ${instance.host} ...`); - + try { const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo') .catch(err => { @@ -149,131 +121,131 @@ export class FetchInstanceMetadataService { throw err.statusCode ?? err.message; } }) as Record; - + if (wellknown.links == null || !Array.isArray(wellknown.links)) { throw new Error('No wellknown links'); } - - const links = wellknown.links as ({ rel: string, href: string; })[]; - - const link1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0'); - const link2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0'); - const link2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1'); - const link = link2_1 ?? link2_0 ?? link1_0; - + + const links = wellknown.links as any[]; + + const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0'); + const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0'); + const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1'); + const link = lnik2_1 ?? lnik2_0 ?? lnik1_0; + if (link == null) { throw new Error('No nodeinfo link provided'); } - + const info = await this.httpRequestService.getJson(link.href) .catch(err => { throw err.statusCode ?? err.message; }); - + this.logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`); - + return info as NodeInfo; } catch (err) { this.logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${err}`); - + throw err; } } @bindThis - private async fetchDom(instance: MiInstance): Promise { + private async fetchDom(instance: Instance): Promise { this.logger.info(`Fetching HTML of ${instance.host} ...`); - + const url = 'https://' + instance.host; - + const html = await this.httpRequestService.getHtml(url); - + const { window } = new JSDOM(html); const doc = window.document; - + return doc; } @bindThis - private async fetchManifest(instance: MiInstance): Promise | null> { + private async fetchManifest(instance: Instance): Promise | null> { const url = 'https://' + instance.host; - + const manifestUrl = url + '/manifest.json'; - + const manifest = await this.httpRequestService.getJson(manifestUrl) as Record; - + return manifest; } @bindThis - private async fetchFaviconUrl(instance: MiInstance, doc: Document | null): Promise { + private async fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise { const url = 'https://' + instance.host; - + if (doc) { // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href; - + if (href) { return (new URL(href, url)).href; } } - + const faviconUrl = url + '/favicon.ico'; - + const favicon = await this.httpRequestService.send(faviconUrl, { method: 'HEAD', }, { throwErrorWhenResponseNotOk: false }); - + if (favicon.ok) { return faviconUrl; } - + return null; } @bindThis - private async fetchIconUrl(instance: MiInstance, doc: Document | null, manifest: Record | null): Promise { + private async fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) { const url = 'https://' + instance.host; return (new URL(manifest.icons[0].src, url)).href; } - + if (doc) { const url = 'https://' + instance.host; - + // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 const links = Array.from(doc.getElementsByTagName('link')).reverse(); // https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559 - const href = + const href = [ links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href, links.find(link => link.relList.contains('apple-touch-icon'))?.href, links.find(link => link.relList.contains('icon'))?.href, ] .find(href => href); - + if (href) { return (new URL(href, url)).href; } } - + return null; } @bindThis - private async getThemeColor(info: NodeInfo | null, doc: Document | null, manifest: Record | null): Promise { + private async getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color; - + if (themeColor) { const color = new tinycolor(themeColor); if (color.isValid()) return color.toHexString(); } - + return null; } @bindThis - private async getSiteName(info: NodeInfo | null, doc: Document | null, manifest: Record | null): Promise { + private async getSiteName(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { if (info && info.metadata) { if (typeof info.metadata.nodeName === 'string') { return info.metadata.nodeName; @@ -281,24 +253,24 @@ export class FetchInstanceMetadataService { return info.metadata.name; } } - + if (doc) { const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content'); - + if (og) { return og; } } - + if (manifest) { return manifest.name ?? manifest.short_name; } - + return null; } @bindThis - private async getDescription(info: NodeInfo | null, doc: Document | null, manifest: Record | null): Promise { + private async getDescription(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { if (info && info.metadata) { if (typeof info.metadata.nodeDescription === 'string') { return info.metadata.nodeDescription; @@ -306,23 +278,23 @@ export class FetchInstanceMetadataService { return info.metadata.description; } } - + if (doc) { const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content'); if (meta) { return meta; } - + const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content'); if (og) { return og; } } - + if (manifest) { return manifest.name ?? manifest.short_name; } - + return null; } } diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index 6250d4d3a1..b6cae5ea75 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -1,26 +1,22 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as fs from 'node:fs'; import * as crypto from 'node:crypto'; import { join } from 'node:path'; -import * as stream from 'node:stream/promises'; +import * as stream from 'node:stream'; +import * as util from 'node:util'; import { Injectable } from '@nestjs/common'; import { FSWatcher } from 'chokidar'; import * as fileType from 'file-type'; import FFmpeg from 'fluent-ffmpeg'; import isSvg from 'is-svg'; import probeImageSize from 'probe-image-size'; -import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; -import * as blurhash from 'blurhash'; +import { type predictionType } from 'nsfwjs'; +import sharp from 'sharp'; +import { encode } from 'blurhash'; import { createTempDir } from '@/misc/create-temp.js'; import { AiService } from '@/core/AiService.js'; -import { LoggerService } from '@/core/LoggerService.js'; -import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import type { PredictionType } from 'nsfwjs'; + +const pipeline = util.promisify(stream.pipeline); export type FileInfo = { size: number; @@ -50,13 +46,9 @@ const TYPE_SVG = { @Injectable() export class FileInfoService { - private logger: Logger; - constructor( private aiService: AiService, - private loggerService: LoggerService, ) { - this.logger = this.loggerService.getLogger('file-info'); } /** @@ -64,7 +56,6 @@ export class FileInfoService { */ @bindThis public async getFileInfo(path: string, opts: { - fileName?: string | null; skipSensitiveDetection: boolean; sensitiveThreshold?: number; sensitiveThresholdForPorn?: number; @@ -77,26 +68,6 @@ export class FileInfoService { let type = await this.detectType(path); - if (type.mime === TYPE_OCTET_STREAM.mime && opts.fileName != null) { - const ext = opts.fileName.split('.').pop(); - if (ext === 'txt') { - type = { - mime: 'text/plain', - ext: 'txt', - }; - } else if (ext === 'csv') { - type = { - mime: 'text/csv', - ext: 'csv', - }; - } else if (ext === 'json') { - type = { - mime: 'application/json', - ext: 'json', - }; - } - } - // image dimensions let width: number | undefined; let height: number | undefined; @@ -149,7 +120,7 @@ export class FileInfoService { 'image/avif', 'image/svg+xml', ].includes(type.mime)) { - blurhash = await this.getBlurhash(path, type.mime).catch(e => { + blurhash = await this.getBlurhash(path).catch(e => { warnings.push(`getBlurhash failed: ${e}`); return undefined; }); @@ -190,20 +161,20 @@ export class FileInfoService { private async detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> { let sensitive = false; let porn = false; - - function judgePrediction(result: readonly PredictionType[]): [sensitive: boolean, porn: boolean] { + + function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] { let sensitive = false; let porn = false; - + if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true; if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true; if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true; - + if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true; - + return [sensitive, porn]; } - + if ([ 'image/jpeg', 'image/png', @@ -282,13 +253,14 @@ export class FileInfoService { disposeOutDir(); } } - + return [sensitive, porn]; } - + private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator { const watcher = new FSWatcher({ cwd, + disableGlobbing: true, }); let finished = false; command.once('end', () => { @@ -323,7 +295,7 @@ export class FileInfoService { } } } - + @bindThis private exists(path: string): Promise { return fs.promises.access(path).then(() => true, () => false); @@ -332,44 +304,16 @@ export class FileInfoService { @bindThis public fixMime(mime: string | fileType.MimeType): string { // see https://github.com/misskey-dev/misskey/pull/10686 - if (mime === 'audio/x-flac') { - return 'audio/flac'; + if (mime === "audio/x-flac") { + return "audio/flac"; } - if (mime === 'audio/vnd.wave') { - return 'audio/wav'; + if (mime === "audio/vnd.wave") { + return "audio/wav"; } return mime; } - /** - * ビデオファイルにビデオトラックがあるかどうかチェック - * (ない場合:m4a, webmなど) - * - * @param path ファイルパス - * @returns ビデオトラックがあるかどうか(エラー発生時は常に`true`を返す) - */ - @bindThis - private hasVideoTrackOnVideoFile(path: string): Promise { - const sublogger = this.logger.createSubLogger('ffprobe'); - sublogger.info(`Checking the video file. File path: ${path}`); - return new Promise((resolve) => { - try { - FFmpeg.ffprobe(path, (err, metadata) => { - if (err) { - sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err); - resolve(true); - return; - } - resolve(metadata.streams.some((stream) => stream.codec_type === 'video')); - }); - } catch (err) { - sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err as Error); - resolve(true); - } - }); - } - /** * Detect MIME Type and extension */ @@ -392,20 +336,6 @@ export class FileInfoService { return TYPE_SVG; } - if ((type.mime.startsWith('video') || type.mime === 'application/ogg') && !(await this.hasVideoTrackOnVideoFile(path))) { - const newMime = `audio/${type.mime.split('/')[1]}`; - if (newMime === 'audio/mp4') { - return { - mime: 'audio/mp4', - ext: 'm4a', - }; - } - return { - mime: newMime, - ext: type.ext, - }; - } - return { mime: this.fixMime(type.mime), ext: type.ext, @@ -425,12 +355,11 @@ export class FileInfoService { * Check the file is SVG or not */ @bindThis - public async checkSvg(path: string): Promise { + public async checkSvg(path: string) { try { const size = await this.getFileSize(path); if (size > 1 * 1024 * 1024) return false; - const buffer = await fs.promises.readFile(path); - return isSvg(buffer.toString()); + return isSvg(fs.readFileSync(path)); } catch { return false; } @@ -441,7 +370,8 @@ export class FileInfoService { */ @bindThis public async getFileSize(path: string): Promise { - return (await fs.promises.stat(path)).size; + const getStat = util.promisify(fs.stat); + return (await getStat(path)).size; } /** @@ -450,7 +380,7 @@ export class FileInfoService { @bindThis private async calcHash(path: string): Promise { const hash = crypto.createHash('md5').setEncoding('hex'); - await stream.pipeline(fs.createReadStream(path), hash); + await pipeline(fs.createReadStream(path), hash); return hash.read(); } @@ -459,12 +389,12 @@ export class FileInfoService { */ @bindThis private async detectImageSize(path: string): Promise<{ - width: number; - height: number; - wUnits: string; - hUnits: string; - orientation?: number; - }> { + width: number; + height: number; + wUnits: string; + hUnits: string; + orientation?: number; +}> { const readable = fs.createReadStream(path); const imageSize = await probeImageSize(readable); readable.destroy(); @@ -472,12 +402,12 @@ export class FileInfoService { } /** - * Calculate blurhash string of image + * Calculate average color of image */ @bindThis - private getBlurhash(path: string, type: string): Promise { - return new Promise(async (resolve, reject) => { - (await sharpBmp(path, type)) + private getBlurhash(path: string): Promise { + return new Promise((resolve, reject) => { + sharp(path) .raw() .ensureAlpha() .resize(64, 64, { fit: 'inside' }) @@ -487,7 +417,7 @@ export class FileInfoService { let hash; try { - hash = blurhash.encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5); + hash = encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5); } catch (e) { return reject(e); } diff --git a/packages/backend/src/core/FlashService.ts b/packages/backend/src/core/FlashService.ts deleted file mode 100644 index 2a98225382..0000000000 --- a/packages/backend/src/core/FlashService.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import { type FlashsRepository } from '@/models/_.js'; - -/** - * MisskeyPlay関係のService - */ -@Injectable() -export class FlashService { - constructor( - @Inject(DI.flashsRepository) - private flashRepository: FlashsRepository, - ) { - } - - /** - * 人気のあるPlay一覧を取得する. - */ - public async featured(opts?: { offset?: number, limit: number }) { - const builder = this.flashRepository.createQueryBuilder('flash') - .andWhere('flash.likedCount > 0') - .andWhere('flash.visibility = :visibility', { visibility: 'public' }) - .addOrderBy('flash.likedCount', 'DESC') - .addOrderBy('flash.updatedAt', 'DESC') - .addOrderBy('flash.id', 'DESC'); - - if (opts?.offset) { - builder.skip(opts.offset); - } - - builder.take(opts?.limit ?? 10); - - return await builder.getMany(); - } -} diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 3215b41c8d..19d9370083 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -1,340 +1,26 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import * as Reversi from 'misskey-reversi'; -import type { MiChannel } from '@/models/Channel.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiUserProfile } from '@/models/UserProfile.js'; -import type { MiNote } from '@/models/Note.js'; -import type { MiAntenna } from '@/models/Antenna.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiDriveFolder } from '@/models/DriveFolder.js'; -import type { MiUserList } from '@/models/UserList.js'; -import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; -import type { MiSignin } from '@/models/Signin.js'; -import type { MiPage } from '@/models/Page.js'; -import type { MiWebhook } from '@/models/Webhook.js'; -import type { MiSystemWebhook } from '@/models/SystemWebhook.js'; -import type { MiMeta } from '@/models/Meta.js'; -import { MiAvatarDecoration, MiChatMessage, MiChatRoom, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js'; +import type { User } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { UserList } from '@/models/entities/UserList.js'; +import type { Antenna } from '@/models/entities/Antenna.js'; +import type { + StreamChannels, + AdminStreamTypes, + AntennaStreamTypes, + BroadcastTypes, + DriveStreamTypes, + InternalStreamTypes, + MainStreamTypes, + NoteStreamTypes, + UserListStreamTypes, + RoleTimelineStreamTypes, +} from '@/server/api/stream/types.js'; import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; -import { Serialized } from '@/types.js'; -import type Emitter from 'strict-event-emitter-types'; -import type { EventEmitter } from 'events'; - -//#region Stream type-body definitions -export interface BroadcastTypes { - emojiAdded: { - emoji: Packed<'EmojiDetailed'>; - }; - emojiUpdated: { - emojis: Packed<'EmojiDetailed'>[]; - }; - emojiDeleted: { - emojis: { - id?: string; - name: string; - [other: string]: any; - }[]; - }; - announcementCreated: { - announcement: Packed<'Announcement'>; - }; -} - -export interface MainEventTypes { - notification: Packed<'Notification'>; - mention: Packed<'Note'>; - reply: Packed<'Note'>; - renote: Packed<'Note'>; - follow: Packed<'UserDetailedNotMe'>; - followed: Packed<'UserLite'>; - unfollow: Packed<'UserDetailedNotMe'>; - meUpdated: Packed<'MeDetailed'>; - pageEvent: { - pageId: MiPage['id']; - event: string; - var: any; - userId: MiUser['id']; - user: Packed<'UserDetailed'>; - }; - urlUploadFinished: { - marker?: string | null; - file: Packed<'DriveFile'>; - }; - readAllNotifications: undefined; - notificationFlushed: undefined; - unreadNotification: Packed<'Notification'>; - unreadAntenna: MiAntenna; - newChatMessage: Packed<'ChatMessage'>; - readAllAnnouncements: undefined; - myTokenRegenerated: undefined; - signin: { - id: MiSignin['id']; - createdAt: string; - ip: string; - headers: Record; - success: boolean; - }; - registryUpdated: { - scope?: string[]; - key: string; - value: any | null; - }; - driveFileCreated: Packed<'DriveFile'>; - readAntenna: MiAntenna; - receiveFollowRequest: Packed<'UserLite'>; - announcementCreated: { - announcement: Packed<'Announcement'>; - }; -} - -export interface DriveEventTypes { - fileCreated: Packed<'DriveFile'>; - fileDeleted: MiDriveFile['id']; - fileUpdated: Packed<'DriveFile'>; - folderCreated: Packed<'DriveFolder'>; - folderDeleted: MiDriveFolder['id']; - folderUpdated: Packed<'DriveFolder'>; -} - -export interface NoteEventTypes { - pollVoted: { - choice: number; - userId: MiUser['id']; - }; - deleted: { - deletedAt: Date; - }; - updated: { - cw: string | null; - text: string; - }; - reacted: { - reaction: string; - emoji?: { - name: string; - url: string; - } | null; - userId: MiUser['id']; - }; - unreacted: { - reaction: string; - userId: MiUser['id']; - }; -} -type NoteStreamEventTypes = { - [key in keyof NoteEventTypes]: { - id: MiNote['id']; - body: NoteEventTypes[key]; - }; -}; - -export interface UserListEventTypes { - userAdded: Packed<'UserLite'>; - userRemoved: Packed<'UserLite'>; -} - -export interface AntennaEventTypes { - note: MiNote; -} - -export interface RoleTimelineEventTypes { - note: Packed<'Note'>; -} - -export interface AdminEventTypes { - newAbuseUserReport: { - id: MiAbuseUserReport['id']; - targetUserId: MiUser['id'], - reporterId: MiUser['id'], - comment: string; - }; -} - -export interface ChatEventTypes { - message: Packed<'ChatMessageLite'>; - deleted: Packed<'ChatMessageLite'>['id']; - react: { - reaction: string; - user?: Packed<'UserLite'>; - messageId: MiChatMessage['id']; - }; - unreact: { - reaction: string; - user?: Packed<'UserLite'>; - messageId: MiChatMessage['id']; - }; -} - -export interface ReversiEventTypes { - matched: { - game: Packed<'ReversiGameDetailed'>; - }; - invited: { - user: Packed<'User'>; - }; -} - -export interface ReversiGameEventTypes { - changeReadyStates: { - user1: boolean; - user2: boolean; - }; - updateSettings: { - userId: MiUser['id']; - key: string; - value: any; - }; - log: Reversi.Serializer.Log & { id: string | null }; - started: { - game: Packed<'ReversiGameDetailed'>; - }; - ended: { - winnerId: MiUser['id'] | null; - game: Packed<'ReversiGameDetailed'>; - }; - canceled: { - userId: MiUser['id']; - }; -} -//#endregion - -// 辞書(interface or type)から{ type, body }ユニオンを定義 -// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type -// VS Codeの展開を防止するためにEvents型を定義 -type Events = { [K in keyof T]: { type: K; body: T[K]; } }; -type EventUnionFromDictionary< - T extends object, - U = Events, -> = U[keyof U]; - -type SerializedAll = { - [K in keyof T]: Serialized; -}; - -type UndefinedAsNullAll = { - [K in keyof T]: T[K] extends undefined ? null : T[K]; -}; - -export interface InternalEventTypes { - userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; }; - userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; }; - userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; }; - remoteUserUpdated: { id: MiUser['id']; }; - localUserUpdated: { id: MiUser['id']; }; - follow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; - unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; - blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; - blockingDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; - policiesUpdated: MiRole['policies']; - roleCreated: MiRole; - roleDeleted: MiRole; - roleUpdated: MiRole; - userRoleAssigned: MiRoleAssignment; - userRoleUnassigned: MiRoleAssignment; - webhookCreated: MiWebhook; - webhookDeleted: MiWebhook; - webhookUpdated: MiWebhook; - systemWebhookCreated: MiSystemWebhook; - systemWebhookDeleted: MiSystemWebhook; - systemWebhookUpdated: MiSystemWebhook; - antennaCreated: MiAntenna; - antennaDeleted: MiAntenna; - antennaUpdated: MiAntenna; - avatarDecorationCreated: MiAvatarDecoration; - avatarDecorationDeleted: MiAvatarDecoration; - avatarDecorationUpdated: MiAvatarDecoration; - metaUpdated: { before?: MiMeta; after: MiMeta; }; - followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; }; - unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; }; - updateUserProfile: MiUserProfile; - mute: { muterId: MiUser['id']; muteeId: MiUser['id']; }; - unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; }; - userListMemberAdded: { userListId: MiUserList['id']; memberId: MiUser['id']; }; - userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; }; -} - -type EventTypesToEventPayload = EventUnionFromDictionary>>; - -// name/messages(spec) pairs dictionary -export type GlobalEvents = { - internal: { - name: 'internal'; - payload: EventTypesToEventPayload; - }; - broadcast: { - name: 'broadcast'; - payload: EventTypesToEventPayload; - }; - main: { - name: `mainStream:${MiUser['id']}`; - payload: EventTypesToEventPayload; - }; - drive: { - name: `driveStream:${MiUser['id']}`; - payload: EventTypesToEventPayload; - }; - note: { - name: `noteStream:${MiNote['id']}`; - payload: EventTypesToEventPayload; - }; - userList: { - name: `userListStream:${MiUserList['id']}`; - payload: EventTypesToEventPayload; - }; - roleTimeline: { - name: `roleTimelineStream:${MiRole['id']}`; - payload: EventTypesToEventPayload; - }; - antenna: { - name: `antennaStream:${MiAntenna['id']}`; - payload: EventTypesToEventPayload; - }; - admin: { - name: `adminStream:${MiUser['id']}`; - payload: EventTypesToEventPayload; - }; - notes: { - name: 'notesStream'; - payload: Serialized>; - }; - chatUser: { - name: `chatUserStream:${MiUser['id']}-${MiUser['id']}`; - payload: EventTypesToEventPayload; - }; - chatRoom: { - name: `chatRoomStream:${MiChatRoom['id']}`; - payload: EventTypesToEventPayload; - }; - reversi: { - name: `reversiStream:${MiUser['id']}`; - payload: EventTypesToEventPayload; - }; - reversiGame: { - name: `reversiGameStream:${MiReversiGame['id']}`; - payload: EventTypesToEventPayload; - }; -}; - -// API event definitions -// ストリームごとのEmitterの辞書を用意 -type EventEmitterDictionary = { [x in keyof GlobalEvents]: Emitter.default void }> }; -// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; -// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする -export type StreamEventEmitter = UnionToIntersection; -// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる - -// provide stream channels union -export type StreamChannels = GlobalEvents[keyof GlobalEvents]['name']; +import { Role } from '@/models/index.js'; @Injectable() export class GlobalEventService { @@ -360,7 +46,7 @@ export class GlobalEventService { } @bindThis - public publishInternalEvent(type: K, value?: InternalEventTypes[K]): void { + public publishInternalEvent(type: K, value?: InternalStreamTypes[K]): void { this.publish('internal', type, typeof value === 'undefined' ? null : value); } @@ -370,17 +56,17 @@ export class GlobalEventService { } @bindThis - public publishMainStream(userId: MiUser['id'], type: K, value?: MainEventTypes[K]): void { + public publishMainStream(userId: User['id'], type: K, value?: MainStreamTypes[K]): void { this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value); } @bindThis - public publishDriveStream(userId: MiUser['id'], type: K, value?: DriveEventTypes[K]): void { + public publishDriveStream(userId: User['id'], type: K, value?: DriveStreamTypes[K]): void { this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value); } @bindThis - public publishNoteStream(noteId: MiNote['id'], type: K, value?: NoteEventTypes[K]): void { + public publishNoteStream(noteId: Note['id'], type: K, value?: NoteStreamTypes[K]): void { this.publish(`noteStream:${noteId}`, type, { id: noteId, body: value, @@ -388,17 +74,17 @@ export class GlobalEventService { } @bindThis - public publishUserListStream(listId: MiUserList['id'], type: K, value?: UserListEventTypes[K]): void { + public publishUserListStream(listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void { this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); } @bindThis - public publishAntennaStream(antennaId: MiAntenna['id'], type: K, value?: AntennaEventTypes[K]): void { + public publishAntennaStream(antennaId: Antenna['id'], type: K, value?: AntennaStreamTypes[K]): void { this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value); } @bindThis - public publishRoleTimelineStream(roleId: MiRole['id'], type: K, value?: RoleTimelineEventTypes[K]): void { + public publishRoleTimelineStream(roleId: Role['id'], type: K, value?: RoleTimelineStreamTypes[K]): void { this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value); } @@ -408,27 +94,7 @@ export class GlobalEventService { } @bindThis - public publishAdminStream(userId: MiUser['id'], type: K, value?: AdminEventTypes[K]): void { + public publishAdminStream(userId: User['id'], type: K, value?: AdminStreamTypes[K]): void { this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - - @bindThis - public publishChatUserStream(fromUserId: MiUser['id'], toUserId: MiUser['id'], type: K, value?: ChatEventTypes[K]): void { - this.publish(`chatUserStream:${fromUserId}-${toUserId}`, type, typeof value === 'undefined' ? null : value); - } - - @bindThis - public publishChatRoomStream(toRoomId: MiChatRoom['id'], type: K, value?: ChatEventTypes[K]): void { - this.publish(`chatRoomStream:${toRoomId}`, type, typeof value === 'undefined' ? null : value); - } - - @bindThis - public publishReversiStream(userId: MiUser['id'], type: K, value?: ReversiEventTypes[K]): void { - this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value); - } - - @bindThis - public publishReversiGameStream(gameId: MiReversiGame['id'], type: K, value?: ReversiGameEventTypes[K]): void { - this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value); - } } diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index 793bbeecb1..851e42e7ba 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -1,65 +1,49 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; -import type { MiUser } from '@/models/User.js'; +import type { User } from '@/models/entities/User.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { IdService } from '@/core/IdService.js'; -import type { MiHashtag } from '@/models/Hashtag.js'; -import type { HashtagsRepository, MiMeta } from '@/models/_.js'; +import type { Hashtag } from '@/models/entities/Hashtag.js'; +import type { HashtagsRepository, UsersRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import { FeaturedService } from '@/core/FeaturedService.js'; -import { UtilityService } from '@/core/UtilityService.js'; @Injectable() export class HashtagService { constructor( - @Inject(DI.meta) - private meta: MiMeta, - - @Inject(DI.redis) - private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, @Inject(DI.hashtagsRepository) private hashtagsRepository: HashtagsRepository, private userEntityService: UserEntityService, - private featuredService: FeaturedService, private idService: IdService, - private utilityService: UtilityService, ) { } @bindThis - public async updateHashtags(user: { id: MiUser['id']; host: MiUser['host']; }, tags: string[]) { + public async updateHashtags(user: { id: User['id']; host: User['host']; }, tags: string[]) { for (const tag of tags) { await this.updateHashtag(user, tag); } } @bindThis - public async updateUsertags(user: MiUser, tags: string[]) { + public async updateUsertags(user: User, tags: string[]) { for (const tag of tags) { await this.updateHashtag(user, tag, true, true); } - for (const tag of user.tags.filter(x => !tags.includes(x))) { + for (const tag of (user.tags ?? []).filter(x => !tags.includes(x))) { await this.updateHashtag(user, tag, true, false); } } @bindThis - public async updateHashtag(user: { id: MiUser['id']; host: MiUser['host']; }, tag: string, isUserAttached = false, inc = true) { + public async updateHashtag(user: { id: User['id']; host: User['host']; }, tag: string, isUserAttached = false, inc = true) { tag = normalizeForSearch(tag); - // TODO: サンプリング - this.updateHashtagsRanking(tag, user.id); - const index = await this.hashtagsRepository.findOneBy({ name: tag }); if (index == null && !inc) return; @@ -99,7 +83,7 @@ export class HashtagService { } } } else { - // 自分が初めてこのタグを使ったなら + // 自分が初めてこのタグを使ったなら if (!index.mentionedUserIds.some(id => id === user.id)) { set.mentionedUserIds = () => `array_append("mentionedUserIds", '${user.id}')`; set.mentionedUsersCount = () => '"mentionedUsersCount" + 1'; @@ -123,7 +107,7 @@ export class HashtagService { } else { if (isUserAttached) { this.hashtagsRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), name: tag, mentionedUserIds: [], mentionedUsersCount: 0, @@ -137,10 +121,10 @@ export class HashtagService { attachedLocalUsersCount: this.userEntityService.isLocalUser(user) ? 1 : 0, attachedRemoteUserIds: this.userEntityService.isRemoteUser(user) ? [user.id] : [], attachedRemoteUsersCount: this.userEntityService.isRemoteUser(user) ? 1 : 0, - } as MiHashtag); + } as Hashtag); } else { this.hashtagsRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), name: tag, mentionedUserIds: [user.id], mentionedUsersCount: 1, @@ -154,98 +138,8 @@ export class HashtagService { attachedLocalUsersCount: 0, attachedRemoteUserIds: [], attachedRemoteUsersCount: 0, - } as MiHashtag); + } as Hashtag); } } } - - @bindThis - public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise { - const hiddenTags = this.meta.hiddenTags.map(t => normalizeForSearch(t)); - if (hiddenTags.includes(hashtag)) return; - if (this.utilityService.isKeyWordIncluded(hashtag, this.meta.sensitiveWords)) return; - - // YYYYMMDDHHmm (10分間隔) - const now = new Date(); - now.setMinutes(Math.floor(now.getMinutes() / 10) * 10, 0, 0); - const window = `${now.getUTCFullYear()}${(now.getUTCMonth() + 1).toString().padStart(2, '0')}${now.getUTCDate().toString().padStart(2, '0')}${now.getUTCHours().toString().padStart(2, '0')}${now.getUTCMinutes().toString().padStart(2, '0')}`; - - const exist = await this.redisClient.sismember(`hashtagUsers:${hashtag}`, userId); - if (exist === 1) return; - - this.featuredService.updateHashtagsRanking(hashtag, 1); - - const redisPipeline = this.redisClient.pipeline(); - - // チャート用 - redisPipeline.pfadd(`hashtagUsers:${hashtag}:${window}`, userId); - redisPipeline.expire(`hashtagUsers:${hashtag}:${window}`, - 60 * 60 * 24 * 3, // 3日間 - 'NX', // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定 - ); - - // ユニークカウント用 - // TODO: Bloom Filter を使うようにしても良さそう - redisPipeline.sadd(`hashtagUsers:${hashtag}`, userId); - redisPipeline.expire(`hashtagUsers:${hashtag}`, - 60 * 60, // 1時間 - 'NX', // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定 - ); - - redisPipeline.exec(); - } - - @bindThis - public async getChart(hashtag: string, range: number): Promise { - const now = new Date(); - now.setMinutes(Math.floor(now.getMinutes() / 10) * 10, 0, 0); - - const redisPipeline = this.redisClient.pipeline(); - - for (let i = 0; i < range; i++) { - const window = `${now.getUTCFullYear()}${(now.getUTCMonth() + 1).toString().padStart(2, '0')}${now.getUTCDate().toString().padStart(2, '0')}${now.getUTCHours().toString().padStart(2, '0')}${now.getUTCMinutes().toString().padStart(2, '0')}`; - redisPipeline.pfcount(`hashtagUsers:${hashtag}:${window}`); - now.setMinutes(now.getMinutes() - (i * 10), 0, 0); - } - - const result = await redisPipeline.exec(); - - if (result == null) return []; - - return result.map(x => x[1]) as number[]; - } - - @bindThis - public async getCharts(hashtags: string[], range: number): Promise> { - const now = new Date(); - now.setMinutes(Math.floor(now.getMinutes() / 10) * 10, 0, 0); - - const redisPipeline = this.redisClient.pipeline(); - - for (let i = 0; i < range; i++) { - const window = `${now.getUTCFullYear()}${(now.getUTCMonth() + 1).toString().padStart(2, '0')}${now.getUTCDate().toString().padStart(2, '0')}${now.getUTCHours().toString().padStart(2, '0')}${now.getUTCMinutes().toString().padStart(2, '0')}`; - for (const hashtag of hashtags) { - redisPipeline.pfcount(`hashtagUsers:${hashtag}:${window}`); - } - now.setMinutes(now.getMinutes() - (i * 10), 0, 0); - } - - const result = await redisPipeline.exec(); - - if (result == null) return {}; - - // key is hashtag - const charts = {} as Record; - for (const hashtag of hashtags) { - charts[hashtag] = []; - } - - for (let i = 0; i < range; i++) { - for (let j = 0; j < hashtags.length; j++) { - charts[hashtags[j]].push(result[(i * hashtags.length) + j][1] as number); - } - } - - return charts; - } } diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 3ddfe52045..375aa846cb 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -1,12 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as http from 'node:http'; import * as https from 'node:https'; -import * as net from 'node:net'; -import ipaddr from 'ipaddr.js'; import CacheableLookup from 'cacheable-lookup'; import fetch from 'node-fetch'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; @@ -15,132 +8,30 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { StatusError } from '@/misc/status-error.js'; import { bindThis } from '@/decorators.js'; -import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; -import { assertActivityMatchesUrl, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; -import type { IObject } from '@/core/activitypub/type.js'; import type { Response } from 'node-fetch'; import type { URL } from 'node:url'; -export type HttpRequestSendOptions = { - throwErrorWhenResponseNotOk: boolean; - validators?: ((res: Response) => void)[]; -}; - -declare module 'node:http' { - interface Agent { - createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket; - } -} - -class HttpRequestServiceAgent extends http.Agent { - constructor( - private config: Config, - options?: http.AgentOptions, - ) { - super(options); - } - - @bindThis - public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket { - const socket = super.createConnection(options, callback) - .on('connect', () => { - const address = socket.remoteAddress; - if (process.env.NODE_ENV === 'production') { - if (address && ipaddr.isValid(address)) { - if (this.isPrivateIp(address)) { - socket.destroy(new Error(`Blocked address: ${address}`)); - } - } - } - }); - return socket; - } - - @bindThis - private isPrivateIp(ip: string): boolean { - const parsedIp = ipaddr.parse(ip); - - for (const net of this.config.allowedPrivateNetworks ?? []) { - const cidr = ipaddr.parseCIDR(net); - if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { - return false; - } - } - - return parsedIp.range() !== 'unicast'; - } -} - -class HttpsRequestServiceAgent extends https.Agent { - constructor( - private config: Config, - options?: https.AgentOptions, - ) { - super(options); - } - - @bindThis - public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket { - const socket = super.createConnection(options, callback) - .on('connect', () => { - const address = socket.remoteAddress; - if (process.env.NODE_ENV === 'production') { - if (address && ipaddr.isValid(address)) { - if (this.isPrivateIp(address)) { - socket.destroy(new Error(`Blocked address: ${address}`)); - } - } - } - }); - return socket; - } - - @bindThis - private isPrivateIp(ip: string): boolean { - const parsedIp = ipaddr.parse(ip); - - for (const net of this.config.allowedPrivateNetworks ?? []) { - const cidr = ipaddr.parseCIDR(net); - if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { - return false; - } - } - - return parsedIp.range() !== 'unicast'; - } -} - @Injectable() export class HttpRequestService { - /** - * Get http non-proxy agent (without local address filtering) - */ - private readonly httpNative: http.Agent; - - /** - * Get https non-proxy agent (without local address filtering) - */ - private readonly httpsNative: https.Agent; - /** * Get http non-proxy agent */ - private readonly http: http.Agent; + private http: http.Agent; /** * Get https non-proxy agent */ - private readonly https: https.Agent; + private https: https.Agent; /** * Get http proxy or non-proxy agent */ - public readonly httpAgent: http.Agent; + public httpAgent: http.Agent; /** * Get https proxy or non-proxy agent */ - public readonly httpsAgent: https.Agent; + public httpsAgent: https.Agent; constructor( @Inject(DI.config) @@ -151,24 +42,21 @@ export class HttpRequestService { errorTtl: 30, // 30secs lookup: false, // nativeのdns.lookupにfallbackしない }); - - const agentOption = { + + this.http = new http.Agent({ keepAlive: true, keepAliveMsecs: 30 * 1000, - lookup: cache.lookup as unknown as net.LookupFunction, - localAddress: config.outgoingAddress, - }; - - this.httpNative = new http.Agent(agentOption); - - this.httpsNative = new https.Agent(agentOption); - - this.http = new HttpRequestServiceAgent(config, agentOption); - - this.https = new HttpsRequestServiceAgent(config, agentOption); - + lookup: cache.lookup, + } as http.AgentOptions); + + this.https = new https.Agent({ + keepAlive: true, + keepAliveMsecs: 30 * 1000, + lookup: cache.lookup, + } as https.AgentOptions); + const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128); - + this.httpAgent = config.proxy ? new HttpProxyAgent({ keepAlive: true, @@ -177,7 +65,6 @@ export class HttpRequestService { maxFreeSockets: 256, scheduling: 'lifo', proxy: config.proxy, - localAddress: config.outgoingAddress, }) : this.http; @@ -189,7 +76,6 @@ export class HttpRequestService { maxFreeSockets: 256, scheduling: 'lifo', proxy: config.proxy, - localAddress: config.outgoingAddress, }) : this.https; } @@ -197,81 +83,19 @@ export class HttpRequestService { /** * Get agent by URL * @param url URL - * @param bypassProxy Always bypass proxy - * @param isLocalAddressAllowed + * @param bypassProxy Allways bypass proxy */ @bindThis - public getAgentByUrl(url: URL, bypassProxy = false, isLocalAddressAllowed = false): http.Agent | https.Agent { - if (bypassProxy || (this.config.proxyBypassHosts ?? []).includes(url.hostname)) { - if (isLocalAddressAllowed) { - return url.protocol === 'http:' ? this.httpNative : this.httpsNative; - } + public getAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent { + if (bypassProxy || (this.config.proxyBypassHosts || []).includes(url.hostname)) { return url.protocol === 'http:' ? this.http : this.https; } else { - if (isLocalAddressAllowed && (!this.config.proxy)) { - return url.protocol === 'http:' ? this.httpNative : this.httpsNative; - } return url.protocol === 'http:' ? this.httpAgent : this.httpsAgent; } } - /** - * Get agent for http by URL - * @param url URL - * @param isLocalAddressAllowed - */ @bindThis - public getAgentForHttp(url: URL, isLocalAddressAllowed = false): http.Agent { - if ((this.config.proxyBypassHosts ?? []).includes(url.hostname)) { - return isLocalAddressAllowed - ? this.httpNative - : this.http; - } else { - return this.httpAgent; - } - } - - /** - * Get agent for https by URL - * @param url URL - * @param isLocalAddressAllowed - */ - @bindThis - public getAgentForHttps(url: URL, isLocalAddressAllowed = false): https.Agent { - if ((this.config.proxyBypassHosts ?? []).includes(url.hostname)) { - return isLocalAddressAllowed - ? this.httpsNative - : this.https; - } else { - return this.httpsAgent; - } - } - - @bindThis - public async getActivityJson(url: string, isLocalAddressAllowed = false, allowSoftfail: FetchAllowSoftFailMask = FetchAllowSoftFailMask.Strict): Promise { - const res = await this.send(url, { - method: 'GET', - headers: { - Accept: 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - }, - timeout: 5000, - size: 1024 * 256, - isLocalAddressAllowed: isLocalAddressAllowed, - }, { - throwErrorWhenResponseNotOk: true, - validators: [validateContentTypeSetAsActivityPub], - }); - - const finalUrl = res.url; // redirects may have been involved - const activity = await res.json() as IObject; - - assertActivityMatchesUrl(url, activity, finalUrl, allowSoftfail); - - return activity; - } - - @bindThis - public async getJson(url: string, accept = 'application/json, */*', headers?: Record, isLocalAddressAllowed = false): Promise { + public async getJson(url: string, accept = 'application/json, */*', headers?: Record): Promise { const res = await this.send(url, { method: 'GET', headers: Object.assign({ @@ -279,42 +103,36 @@ export class HttpRequestService { }, headers ?? {}), timeout: 5000, size: 1024 * 256, - isLocalAddressAllowed: isLocalAddressAllowed, }); return await res.json() as T; } @bindThis - public async getHtml(url: string, accept = 'text/html, */*', headers?: Record, isLocalAddressAllowed = false): Promise { + public async getHtml(url: string, accept = 'text/html, */*', headers?: Record): Promise { const res = await this.send(url, { method: 'GET', headers: Object.assign({ Accept: accept, }, headers ?? {}), timeout: 5000, - isLocalAddressAllowed: isLocalAddressAllowed, }); return await res.text(); } @bindThis - public async send( - url: string, - args: { - method?: string, - body?: string, - headers?: Record, - timeout?: number, - size?: number, - isLocalAddressAllowed?: boolean, - } = {}, - extra: HttpRequestSendOptions = { - throwErrorWhenResponseNotOk: true, - validators: [], - }, - ): Promise { + public async send(url: string, args: { + method?: string, + body?: string, + headers?: Record, + timeout?: number, + size?: number, + } = {}, extra: { + throwErrorWhenResponseNotOk: boolean; + } = { + throwErrorWhenResponseNotOk: true, + }): Promise { const timeout = args.timeout ?? 5000; const controller = new AbortController(); @@ -322,17 +140,15 @@ export class HttpRequestService { controller.abort(); }, timeout); - const isLocalAddressAllowed = args.isLocalAddressAllowed ?? false; - const res = await fetch(url, { method: args.method ?? 'GET', headers: { 'User-Agent': this.config.userAgent, - ...(args.headers ?? {}), + ...(args.headers ?? {}) }, body: args.body, size: args.size ?? 10 * 1024 * 1024, - agent: (url) => this.getAgentByUrl(url, false, isLocalAddressAllowed), + agent: (url) => this.getAgentByUrl(url), signal: controller.signal, }); @@ -340,12 +156,6 @@ export class HttpRequestService { throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText); } - if (res.ok) { - for (const validator of (extra.validators ?? [])) { - validator(res); - } - } - return res; } } diff --git a/packages/backend/src/core/IdService.ts b/packages/backend/src/core/IdService.ts index 223a8de678..60098bc81c 100644 --- a/packages/backend/src/core/IdService.ts +++ b/packages/backend/src/core/IdService.ts @@ -1,19 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { ulid } from 'ulid'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import { genAid, isSafeAidT, parseAid, parseAidFull } from '@/misc/id/aid.js'; -import { genAidx, isSafeAidxT, parseAidx, parseAidxFull } from '@/misc/id/aidx.js'; -import { genMeid, isSafeMeidT, parseMeid, parseMeidFull } from '@/misc/id/meid.js'; -import { genMeidg, isSafeMeidgT, parseMeidg, parseMeidgFull } from '@/misc/id/meidg.js'; -import { genObjectId, isSafeObjectIdT, parseObjectId, parseObjectIdFull } from '@/misc/id/object-id.js'; +import { genAid, parseAid } from '@/misc/id/aid.js'; +import { genMeid, parseMeid } from '@/misc/id/meid.js'; +import { genMeidg, parseMeidg } from '@/misc/id/meidg.js'; +import { genObjectId, parseObjectId } from '@/misc/id/object-id.js'; import { bindThis } from '@/decorators.js'; -import { parseUlid, parseUlidFull } from '@/misc/id/ulid.js'; +import { parseUlid } from '@/misc/id/ulid.js'; @Injectable() export class IdService { @@ -27,33 +21,15 @@ export class IdService { } @bindThis - public isSafeT(t: number): boolean { + public genId(date?: Date): string { + if (!date || (date > new Date())) date = new Date(); + switch (this.method) { - case 'aid': return isSafeAidT(t); - case 'aidx': return isSafeAidxT(t); - case 'meid': return isSafeMeidT(t); - case 'meidg': return isSafeMeidgT(t); - case 'ulid': return t > 0; - case 'objectid': return isSafeObjectIdT(t); - default: throw new Error('unrecognized id generation method'); - } - } - - /** - * 時間を元にIDを生成します(省略時は現在日時) - * @param time 日時 - */ - @bindThis - public gen(time?: number): string { - const t = (!time || (time > Date.now())) ? Date.now() : time; - - switch (this.method) { - case 'aid': return genAid(t); - case 'aidx': return genAidx(t); - case 'meid': return genMeid(t); - case 'meidg': return genMeidg(t); - case 'ulid': return ulid(t); - case 'objectid': return genObjectId(t); + case 'aid': return genAid(date); + case 'meid': return genMeid(date); + case 'meidg': return genMeidg(date); + case 'ulid': return ulid(date.getTime()); + case 'objectid': return genObjectId(date); default: throw new Error('unrecognized id generation method'); } } @@ -62,7 +38,6 @@ export class IdService { public parse(id: string): { date: Date; } { switch (this.method) { case 'aid': return parseAid(id); - case 'aidx': return parseAidx(id); case 'objectid': return parseObjectId(id); case 'meid': return parseMeid(id); case 'meidg': return parseMeidg(id); @@ -70,18 +45,4 @@ export class IdService { default: throw new Error('unrecognized id generation method'); } } - - // Note: additional is at most 64 bits - @bindThis - public parseFull(id: string): { date: number; additional: bigint; } { - switch (this.method) { - case 'aid': return parseAidFull(id); - case 'aidx': return parseAidxFull(id); - case 'objectid': return parseObjectIdFull(id); - case 'meid': return parseMeidFull(id); - case 'meidg': return parseMeidgFull(id); - case 'ulid': return parseUlidFull(id); - default: throw new Error('unrecognized id generation method'); - } - } } diff --git a/packages/backend/src/core/ImageProcessingService.ts b/packages/backend/src/core/ImageProcessingService.ts index 6f60475442..3246475d12 100644 --- a/packages/backend/src/core/ImageProcessingService.ts +++ b/packages/backend/src/core/ImageProcessingService.ts @@ -1,10 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import sharp from 'sharp'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; export type IImage = { data: Buffer; @@ -34,7 +31,6 @@ export const webpDefault: sharp.WebpOptions = { smartSubsample: true, mixed: true, effort: 2, - loop: 0, }; export const avifDefault: sharp.AvifOptions = { @@ -49,6 +45,8 @@ import { Readable } from 'node:stream'; @Injectable() export class ImageProcessingService { constructor( + @Inject(DI.config) + private config: Config, ) { } diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts new file mode 100644 index 0000000000..4fb3fc5b4f --- /dev/null +++ b/packages/backend/src/core/InstanceActorService.ts @@ -0,0 +1,44 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull } from 'typeorm'; +import type { LocalUser } from '@/models/entities/User.js'; +import type { UsersRepository } from '@/models/index.js'; +import { MemorySingleCache } from '@/misc/cache.js'; +import { DI } from '@/di-symbols.js'; +import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; +import { bindThis } from '@/decorators.js'; + +const ACTOR_USERNAME = 'instance.actor' as const; + +@Injectable() +export class InstanceActorService { + private cache: MemorySingleCache; + + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private createSystemUserService: CreateSystemUserService, + ) { + this.cache = new MemorySingleCache(Infinity); + } + + @bindThis + public async getInstanceActor(): Promise { + const cached = this.cache.get(); + if (cached) return cached; + + const user = await this.usersRepository.findOneBy({ + host: IsNull(), + username: ACTOR_USERNAME, + }) as LocalUser | undefined; + + if (user) { + this.cache.set(user); + return user; + } else { + const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser; + this.cache.set(created); + return created; + } + } +} diff --git a/packages/backend/src/core/InternalStorageService.ts b/packages/backend/src/core/InternalStorageService.ts index 4fb8a93e49..7c03af7de7 100644 --- a/packages/backend/src/core/InternalStorageService.ts +++ b/packages/backend/src/core/InternalStorageService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as fs from 'node:fs'; import * as Path from 'node:path'; import { fileURLToPath } from 'node:url'; diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts index f102461a50..14df9aa40c 100644 --- a/packages/backend/src/core/LoggerService.ts +++ b/packages/backend/src/core/LoggerService.ts @@ -1,9 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; import Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import type { KEYWORD } from 'color-convert/conversions.js'; @@ -11,11 +8,13 @@ import type { KEYWORD } from 'color-convert/conversions.js'; @Injectable() export class LoggerService { constructor( + @Inject(DI.config) + private config: Config, ) { } @bindThis - public getLogger(domain: string, color?: KEYWORD | undefined) { - return new Logger(domain, color); + public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) { + return new Logger(domain, color, store); } } diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts index 40e7439f5f..5acc9ad9ad 100644 --- a/packages/backend/src/core/MetaService.ts +++ b/packages/backend/src/core/MetaService.ts @@ -1,23 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; -import { MiMeta } from '@/models/Meta.js'; +import { Meta } from '@/models/entities/Meta.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { bindThis } from '@/decorators.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import { FeaturedService } from '@/core/FeaturedService.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class MetaService implements OnApplicationShutdown { - private cache: MiMeta | undefined; - private intervalId: NodeJS.Timeout; + private cache: Meta | undefined; + private intervalId: NodeJS.Timer; constructor( @Inject(DI.redisForSub) @@ -26,7 +20,6 @@ export class MetaService implements OnApplicationShutdown { @Inject(DI.db) private db: DataSource, - private featuredService: FeaturedService, private globalEventService: GlobalEventService, ) { //this.onMessage = this.onMessage.bind(this); @@ -48,13 +41,10 @@ export class MetaService implements OnApplicationShutdown { const obj = JSON.parse(data); if (obj.channel === 'internal') { - const { type, body } = obj.message as GlobalEvents['internal']['payload']; + const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'metaUpdated': { - this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい - ...(body.after), - rootUser: null, // joinなカラムは通常取ってこないので - }; + this.cache = body; break; } default: @@ -64,12 +54,44 @@ export class MetaService implements OnApplicationShutdown { } @bindThis - public async fetch(noCache = false): Promise { + public async fetch(noCache = false): Promise { if (!noCache && this.cache) return this.cache; - + return await this.db.transaction(async transactionalEntityManager => { // 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する - const metas = await transactionalEntityManager.find(MiMeta, { + const metas = await transactionalEntityManager.find(Meta, { + order: { + id: 'DESC', + }, + }); + + const meta = metas[0]; + + if (meta) { + this.cache = meta; + return meta; + } else { + // metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う + const saved = await transactionalEntityManager + .upsert( + Meta, + { + id: 'x', + }, + ['id'], + ) + .then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0])); + + this.cache = saved; + return saved; + } + }); + } + + @bindThis + public async update(data: Partial): Promise { + const updated = await this.db.transaction(async transactionalEntityManager => { + const metas = await transactionalEntityManager.find(Meta, { order: { id: 'DESC', }, @@ -78,73 +100,21 @@ export class MetaService implements OnApplicationShutdown { const meta = metas[0]; if (meta) { - this.cache = meta; - return meta; - } else { - // metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う - const saved = await transactionalEntityManager - .upsert( - MiMeta, - { - id: 'x', - }, - ['id'], - ) - .then((x) => transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0])); + await transactionalEntityManager.update(Meta, meta.id, data); - this.cache = saved; - return saved; - } - }); - } - - @bindThis - public async update(data: Partial): Promise { - let before: MiMeta | undefined; - - const updated = await this.db.transaction(async transactionalEntityManager => { - const metas = await transactionalEntityManager.find(MiMeta, { - order: { - id: 'DESC', - }, - }); - - before = metas[0]; - - if (before) { - await transactionalEntityManager.update(MiMeta, before.id, data); - } else { - await transactionalEntityManager.save(MiMeta, { - ...data, - id: 'x', + const metas = await transactionalEntityManager.find(Meta, { + order: { + id: 'DESC', + }, }); + + return metas[0]; + } else { + return await transactionalEntityManager.save(Meta, data); } - - const afters = await transactionalEntityManager.find(MiMeta, { - order: { - id: 'DESC', - }, - }); - - return afters[0]; }); - if (data.hiddenTags) { - process.nextTick(() => { - const hiddenTags = new Set(data.hiddenTags); - if (before) { - for (const previousHiddenTag of before.hiddenTags) { - hiddenTags.delete(previousHiddenTag); - } - } - - for (const hiddenTag of hiddenTags) { - this.featuredService.removeHashtagsFromRanking(hiddenTag); - } - }); - } - - this.globalEventService.publishInternalEvent('metaUpdated', { before, after: updated }); + this.globalEventService.publishInternalEvent('metaUpdated', updated); return updated; } diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 28d980f718..dffee16e08 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -1,30 +1,20 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import * as parse5 from 'parse5'; -import { type Document, type HTMLParagraphElement, Window, XMLSerializer } from 'happy-dom'; +import { Window } from 'happy-dom'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { intersperse } from '@/misc/prelude/array.js'; -import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import type { IMentionedRemoteUsers } from '@/models/Note.js'; +import type { IMentionedRemoteUsers } from '@/models/entities/Note.js'; import { bindThis } from '@/decorators.js'; -import type { DefaultTreeAdapterMap } from 'parse5'; +import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; import type * as mfm from 'mfm-js'; -const treeAdapter = parse5.defaultTreeAdapter; -type Node = DefaultTreeAdapterMap['node']; -type ChildNode = DefaultTreeAdapterMap['childNode']; +const treeAdapter = TreeAdapter.defaultTreeAdapter; const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; -export type Appender = (document: Document, body: HTMLParagraphElement) => void; - @Injectable() export class MfmService { constructor( @@ -37,68 +27,65 @@ export class MfmService { public fromHtml(html: string, hashtagNames?: string[]): string { // some AP servers like Pixelfed use br tags as well as newlines html = html.replace(/\r?\n/gi, '\n'); - - const normalizedHashtagNames = hashtagNames == null ? undefined : new Set(hashtagNames.map(x => normalizeForSearch(x))); - + const dom = parse5.parseFragment(html); - + let text = ''; - + for (const n of dom.childNodes) { analyze(n); } - + return text.trim(); - - function getText(node: Node): string { + + function getText(node: TreeAdapter.Node): string { if (treeAdapter.isTextNode(node)) return node.value; if (!treeAdapter.isElementNode(node)) return ''; if (node.nodeName === 'br') return '\n'; - + if (node.childNodes) { return node.childNodes.map(n => getText(n)).join(''); } - + return ''; } - - function appendChildren(childNodes: ChildNode[]): void { + + function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { if (childNodes) { for (const n of childNodes) { analyze(n); } } } - - function analyze(node: Node) { + + function analyze(node: TreeAdapter.Node) { if (treeAdapter.isTextNode(node)) { text += node.value; return; } - + // Skip comment or document type node - if (!treeAdapter.isElementNode(node)) { - return; - } - + if (!treeAdapter.isElementNode(node)) return; + switch (node.nodeName) { case 'br': { text += '\n'; break; } - - case 'a': { + + case 'a': + { const txt = getText(node); const rel = node.attrs.find(x => x.name === 'rel'); const href = node.attrs.find(x => x.name === 'href'); - + // ハッシュタグ - if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) { + if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) { text += txt; - // メンション + // メンション } else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) { const part = txt.split('@'); - + if (part.length === 2 && href) { //#region ホスト名部分が省略されているので復元する const acct = `${txt}@${(new URL(href.value)).hostname}`; @@ -107,7 +94,7 @@ export class MfmService { } else if (part.length === 3) { text += txt; } - // その他 + // その他 } else { const generateLink = () => { if (!href && !txt) { @@ -129,83 +116,55 @@ export class MfmService { return `[${txt}](${href.value})`; } }; - + text += generateLink(); } break; } - - case 'h1': { + + case 'h1': + { text += '【'; appendChildren(node.childNodes); text += '】\n'; break; } - + case 'b': - case 'strong': { + case 'strong': + { text += '**'; appendChildren(node.childNodes); text += '**'; break; } - - case 'small': { + + case 'small': + { text += ''; appendChildren(node.childNodes); text += ''; break; } - + case 's': - case 'del': { + case 'del': + { text += '~~'; appendChildren(node.childNodes); text += '~~'; break; } - + case 'i': - case 'em': { + case 'em': + { text += ''; appendChildren(node.childNodes); text += ''; break; } - - case 'ruby': { - let ruby: [string, string][] = []; - for (const child of node.childNodes) { - if (child.nodeName === 'rp') { - continue; - } - if (treeAdapter.isTextNode(child) && !/\s|\[|\]/.test(child.value)) { - ruby.push([child.value, '']); - continue; - } - if (child.nodeName === 'rt' && ruby.length > 0) { - const rt = getText(child); - if (/\s|\[|\]/.test(rt)) { - // If any space is included in rt, it is treated as a normal text - ruby = []; - appendChildren(node.childNodes); - break; - } else { - ruby.at(-1)![1] = rt; - continue; - } - } - // If any other element is included in ruby, it is treated as a normal text - ruby = []; - appendChildren(node.childNodes); - break; - } - for (const [base, rt] of ruby) { - text += `$[ruby ${base} ${rt}]`; - } - break; - } - + // block code (
)
 				case 'pre': {
 					if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
@@ -217,7 +176,7 @@ export class MfmService {
 					}
 					break;
 				}
-
+	
 				// inline code ()
 				case 'code': {
 					text += '`';
@@ -225,7 +184,7 @@ export class MfmService {
 					text += '`';
 					break;
 				}
-
+	
 				case 'blockquote': {
 					const t = getText(node);
 					if (t) {
@@ -234,18 +193,19 @@ export class MfmService {
 					}
 					break;
 				}
-
+	
 				case 'p':
 				case 'h2':
 				case 'h3':
 				case 'h4':
 				case 'h5':
-				case 'h6': {
+				case 'h6':
+				{
 					text += '\n\n';
 					appendChildren(node.childNodes);
 					break;
 				}
-
+	
 				// other block elements
 				case 'div':
 				case 'header':
@@ -253,12 +213,13 @@ export class MfmService {
 				case 'article':
 				case 'li':
 				case 'dt':
-				case 'dd': {
+				case 'dd':
+				{
 					text += '\n';
 					appendChildren(node.childNodes);
 					break;
 				}
-
+	
 				default:	// includes inline elements
 				{
 					appendChildren(node.childNodes);
@@ -269,120 +230,52 @@ export class MfmService {
 	}
 
 	@bindThis
-	public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], additionalAppenders: Appender[] = []) {
+	public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) {
 		if (nodes == null) {
 			return null;
 		}
-
-		const { happyDOM, window } = new Window();
-
+	
+		const { window } = new Window();
+	
 		const doc = window.document;
-
-		const body = doc.createElement('p');
-
+	
 		function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
 			if (children) {
 				for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
 			}
 		}
-
-		function fnDefault(node: mfm.MfmFn) {
-			const el = doc.createElement('i');
-			appendChildren(node.children, el);
-			return el;
-		}
-
+	
 		const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => any } = {
 			bold: (node) => {
 				const el = doc.createElement('b');
 				appendChildren(node.children, el);
 				return el;
 			},
-
+	
 			small: (node) => {
 				const el = doc.createElement('small');
 				appendChildren(node.children, el);
 				return el;
 			},
-
+	
 			strike: (node) => {
 				const el = doc.createElement('del');
 				appendChildren(node.children, el);
 				return el;
 			},
-
+	
 			italic: (node) => {
 				const el = doc.createElement('i');
 				appendChildren(node.children, el);
 				return el;
 			},
-
+	
 			fn: (node) => {
-				switch (node.props.name) {
-					case 'unixtime': {
-						const text = node.children[0].type === 'text' ? node.children[0].props.text : '';
-						try {
-							const date = new Date(parseInt(text, 10) * 1000);
-							const el = doc.createElement('time');
-							el.setAttribute('datetime', date.toISOString());
-							el.textContent = date.toISOString();
-							return el;
-						} catch (err) {
-							return fnDefault(node);
-						}
-					}
-
-					case 'ruby': {
-						if (node.children.length === 1) {
-							const child = node.children[0];
-							const text = child.type === 'text' ? child.props.text : '';
-							const rubyEl = doc.createElement('ruby');
-							const rtEl = doc.createElement('rt');
-
-							// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
-							const rpStartEl = doc.createElement('rp');
-							rpStartEl.appendChild(doc.createTextNode('('));
-							const rpEndEl = doc.createElement('rp');
-							rpEndEl.appendChild(doc.createTextNode(')'));
-
-							rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
-							rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
-							rubyEl.appendChild(rpStartEl);
-							rubyEl.appendChild(rtEl);
-							rubyEl.appendChild(rpEndEl);
-							return rubyEl;
-						} else {
-							const rt = node.children.at(-1);
-
-							if (!rt) {
-								return fnDefault(node);
-							}
-
-							const text = rt.type === 'text' ? rt.props.text : '';
-							const rubyEl = doc.createElement('ruby');
-							const rtEl = doc.createElement('rt');
-
-							// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
-							const rpStartEl = doc.createElement('rp');
-							rpStartEl.appendChild(doc.createTextNode('('));
-							const rpEndEl = doc.createElement('rp');
-							rpEndEl.appendChild(doc.createTextNode(')'));
-
-							appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
-							rtEl.appendChild(doc.createTextNode(text.trim()));
-							rubyEl.appendChild(rpStartEl);
-							rubyEl.appendChild(rtEl);
-							rubyEl.appendChild(rpEndEl);
-							return rubyEl;
-						}
-					}
-
-					default: {
-						return fnDefault(node);
-					}
-				}
+				const el = doc.createElement('i');
+				appendChildren(node.children, el);
+				return el;
 			},
-
+	
 			blockCode: (node) => {
 				const pre = doc.createElement('pre');
 				const inner = doc.createElement('code');
@@ -390,21 +283,21 @@ export class MfmService {
 				pre.appendChild(inner);
 				return pre;
 			},
-
+	
 			center: (node) => {
 				const el = doc.createElement('div');
 				appendChildren(node.children, el);
 				return el;
 			},
-
+	
 			emojiCode: (node) => {
 				return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
 			},
-
+	
 			unicodeEmoji: (node) => {
 				return doc.createTextNode(node.props.emoji);
 			},
-
+	
 			hashtag: (node) => {
 				const a = doc.createElement('a');
 				a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
@@ -412,97 +305,82 @@ export class MfmService {
 				a.setAttribute('rel', 'tag');
 				return a;
 			},
-
+	
 			inlineCode: (node) => {
 				const el = doc.createElement('code');
 				el.textContent = node.props.code;
 				return el;
 			},
-
+	
 			mathInline: (node) => {
 				const el = doc.createElement('code');
 				el.textContent = node.props.formula;
 				return el;
 			},
-
+	
 			mathBlock: (node) => {
 				const el = doc.createElement('code');
 				el.textContent = node.props.formula;
 				return el;
 			},
-
+	
 			link: (node) => {
 				const a = doc.createElement('a');
 				a.setAttribute('href', node.props.url);
 				appendChildren(node.children, a);
 				return a;
 			},
-
+	
 			mention: (node) => {
 				const a = doc.createElement('a');
 				const { username, host, acct } = node.props;
-				const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
-				a.setAttribute('href', remoteUserInfo
-					? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri)
-					: `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`);
+				const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
+				a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`);
 				a.className = 'u-url mention';
 				a.textContent = acct;
 				return a;
 			},
-
+	
 			quote: (node) => {
 				const el = doc.createElement('blockquote');
 				appendChildren(node.children, el);
 				return el;
 			},
-
+	
 			text: (node) => {
-				if (!node.props.text.match(/[\r\n]/)) {
-					return doc.createTextNode(node.props.text);
-				}
-
 				const el = doc.createElement('span');
 				const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
-
+	
 				for (const x of intersperse('br', nodes)) {
 					el.appendChild(x === 'br' ? doc.createElement('br') : x);
 				}
-
+	
 				return el;
 			},
-
+	
 			url: (node) => {
 				const a = doc.createElement('a');
 				a.setAttribute('href', node.props.url);
 				a.textContent = node.props.url;
 				return a;
 			},
-
+	
 			search: (node) => {
 				const a = doc.createElement('a');
 				a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
 				a.textContent = node.props.content;
 				return a;
 			},
-
+	
 			plain: (node) => {
 				const el = doc.createElement('span');
 				appendChildren(node.children, el);
 				return el;
 			},
 		};
-
-		appendChildren(nodes, body);
-
-		for (const additionalAppender of additionalAppenders) {
-			additionalAppender(doc, body);
-		}
-
-		// Remove the unnecessary namespace
-		const serialized = new XMLSerializer().serializeToString(body).replace(/^\s*

/, '

'); - - happyDOM.close().catch(err => {}); - - return serialized; - } + + appendChildren(nodes, doc.body); + + return `

${doc.body.innerHTML}

`; + } } diff --git a/packages/backend/src/core/ModerationLogService.ts b/packages/backend/src/core/ModerationLogService.ts index 2c02af217d..80e8cb9e52 100644 --- a/packages/backend/src/core/ModerationLogService.ts +++ b/packages/backend/src/core/ModerationLogService.ts @@ -1,16 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { ModerationLogsRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; +import type { ModerationLogsRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { bindThis } from '@/decorators.js'; -import type { ModerationLogPayloads } from '@/types.js'; -import { moderationLogTypes } from '@/types.js'; @Injectable() export class ModerationLogService { @@ -23,12 +16,13 @@ export class ModerationLogService { } @bindThis - public async log(moderator: { id: MiUser['id'] }, type: T, info?: ModerationLogPayloads[T]) { + public async insertModerationLog(moderator: { id: User['id'] }, type: string, info?: Record) { await this.moderationLogsRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), userId: moderator.id, type: type, - info: (info as any) ?? {}, + info: info ?? {}, }); } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index bcb12ba0bf..1c8491bf57 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -1,29 +1,28 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { setImmediate } from 'node:timers/promises'; import * as mfm from 'mfm-js'; -import { In, DataSource, IsNull, LessThan } from 'typeorm'; +import { In, DataSource } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import RE2 from 're2'; import { extractMentions } from '@/misc/extract-mentions.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; -import type { IMentionedRemoteUsers } from '@/models/Note.js'; -import { MiNote } from '@/models/Note.js'; -import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiApp } from '@/models/App.js'; +import type { IMentionedRemoteUsers } from '@/models/entities/Note.js'; +import { Note } from '@/models/entities/Note.js'; +import type { ChannelFollowingsRepository, ChannelsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { App } from '@/models/entities/App.js'; import { concat } from '@/misc/prelude/array.js'; import { IdService } from '@/core/IdService.js'; -import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; -import type { IPoll } from '@/models/Poll.js'; -import { MiPoll } from '@/models/Poll.js'; +import type { User, LocalUser, RemoteUser } from '@/models/entities/User.js'; +import type { IPoll } from '@/models/entities/Poll.js'; +import { Poll } from '@/models/entities/Poll.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import type { MiChannel } from '@/models/Channel.js'; +import { checkWordMute } from '@/misc/check-word-mute.js'; +import type { Channel } from '@/models/entities/Channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; +import { MemorySingleCache } from '@/misc/cache.js'; +import type { UserProfile } from '@/models/entities/UserProfile.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -34,7 +33,7 @@ import InstanceChart from '@/core/chart/charts/instance.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { NotificationService } from '@/core/NotificationService.js'; -import { UserWebhookService } from '@/core/UserWebhookService.js'; +import { WebhookService } from '@/core/WebhookService.js'; import { HashtagService } from '@/core/HashtagService.js'; import { AntennaService } from '@/core/AntennaService.js'; import { QueueService } from '@/core/QueueService.js'; @@ -42,36 +41,31 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; +import { NoteReadService } from '@/core/NoteReadService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { bindThis } from '@/decorators.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; +import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; -import { FeaturedService } from '@/core/FeaturedService.js'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import { UserBlockingService } from '@/core/UserBlockingService.js'; -import { isReply } from '@/misc/is-reply.js'; -import { trackPromise } from '@/misc/promise-tracker.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { CollapsedQueue } from '@/misc/collapsed-queue.js'; -import { CacheService } from '@/core/CacheService.js'; + +const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; class NotificationManager { - private notifier: { id: MiUser['id']; }; - private note: MiNote; + private notifier: { id: User['id']; }; + private note: Note; private queue: { - target: MiLocalUser['id']; + target: LocalUser['id']; reason: NotificationType; }[]; constructor( private mutingsRepository: MutingsRepository, private notificationService: NotificationService, - notifier: { id: MiUser['id']; }, - note: MiNote, + notifier: { id: User['id']; }, + note: Note, ) { this.notifier = notifier; this.note = note; @@ -79,7 +73,7 @@ class NotificationManager { } @bindThis - public push(notifiee: MiLocalUser['id'], reason: NotificationType) { + public push(notifiee: LocalUser['id'], reason: NotificationType) { // 自分自身へは通知しない if (this.notifier.id === notifiee) return; @@ -99,87 +93,68 @@ class NotificationManager { } @bindThis - public async notify() { + public async deliver() { for (const x of this.queue) { - if (x.reason === 'renote') { - this.notificationService.createNotification(x.target, 'renote', { - noteId: this.note.id, - targetNoteId: this.note.renoteId!, - }, this.notifier.id); - } else { + // ミュート情報を取得 + const mentioneeMutes = await this.mutingsRepository.findBy({ + muterId: x.target, + }); + + const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId); + + // 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する + if (!mentioneesMutedUserIds.includes(this.notifier.id)) { this.notificationService.createNotification(x.target, x.reason, { + notifierId: this.notifier.id, noteId: this.note.id, - }, this.notifier.id); + }); } } } } type MinimumUser = { - id: MiUser['id']; - host: MiUser['host']; - username: MiUser['username']; - uri: MiUser['uri']; + id: User['id']; + host: User['host']; + username: User['username']; + uri: User['uri']; }; type Option = { createdAt?: Date | null; name?: string | null; text?: string | null; - reply?: MiNote | null; - renote?: MiNote | null; - files?: MiDriveFile[] | null; + reply?: Note | null; + renote?: Note | null; + files?: DriveFile[] | null; poll?: IPoll | null; localOnly?: boolean | null; - reactionAcceptance?: MiNote['reactionAcceptance']; + reactionAcceptance?: Note['reactionAcceptance']; cw?: string | null; visibility?: string; visibleUsers?: MinimumUser[] | null; - channel?: MiChannel | null; + channel?: Channel | null; apMentions?: MinimumUser[] | null; apHashtags?: string[] | null; apEmojis?: string[] | null; uri?: string | null; url?: string | null; - app?: MiApp | null; + app?: App | null; }; -// BEGIN comfy.social -type NoteFilterResult = { - verdict: boolean; // true = block - reason?: string; -}; - -type NoteFilterPluginContext = { - data: Option; - user: MiUser; - mentionedUsers: MiUser[]; - remoteUserResolveService: RemoteUserResolveService; - idService: IdService; -} -// END comfy.social - @Injectable() export class NoteCreateService implements OnApplicationShutdown { #shutdownController = new AbortController(); - private updateNotesCountQueue: CollapsedQueue; - - // BEGIN comfy.social - private noteFilterPluginFn?: (context: NoteFilterPluginContext) => Promise = null; - // END comfy.social constructor( @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.db) private db: DataSource, - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, + @Inject(DI.redis) + private redisClient: Redis.Redis, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -196,65 +171,50 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, + @Inject(DI.mutedNotesRepository) + private mutedNotesRepository: MutedNotesRepository, @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, - @Inject(DI.noteThreadMutingsRepository) - private noteThreadMutingsRepository: NoteThreadMutingsRepository, - - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - @Inject(DI.channelFollowingsRepository) private channelFollowingsRepository: ChannelFollowingsRepository, + @Inject(DI.noteThreadMutingsRepository) + private noteThreadMutingsRepository: NoteThreadMutingsRepository, + private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private idService: IdService, private globalEventService: GlobalEventService, private queueService: QueueService, - private fanoutTimelineService: FanoutTimelineService, + private noteReadService: NoteReadService, private notificationService: NotificationService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, private hashtagService: HashtagService, private antennaService: AntennaService, - private webhookService: UserWebhookService, - private featuredService: FeaturedService, + private webhookService: WebhookService, private remoteUserResolveService: RemoteUserResolveService, private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, private roleService: RoleService, + private metaService: MetaService, private searchService: SearchService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private activeUsersChart: ActiveUsersChart, private instanceChart: InstanceChart, - private utilityService: UtilityService, - private userBlockingService: UserBlockingService, - private cacheService: CacheService, - ) { - this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount); - // BEGIN comfy.social - if (this.config.noteFilterPlugin != null) { - import(this.config.noteFilterPlugin).then((m) => { - this.noteFilterPluginFn = m.default; - }); - } - // END comfy.social - } + ) { } @bindThis public async create(user: { - id: MiUser['id']; - username: MiUser['username']; - host: MiUser['host']; - isBot: MiUser['isBot']; - isCat: MiUser['isCat']; - }, data: Option, silent = false): Promise { + id: User['id']; + username: User['username']; + host: User['host']; + createdAt: User['createdAt']; + isBot: User['isBot']; + }, data: Option, silent = false): Promise { // チャンネル外にリプライしたら対象のスコープに合わせる // (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで) if (data.reply && data.channel && data.reply.channelId !== data.channel.id) { @@ -279,66 +239,27 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.channel != null) data.localOnly = true; if (data.visibility === 'public' && data.channel == null) { - const sensitiveWords = this.meta.sensitiveWords; - if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { + const sensitiveWords = (await this.metaService.fetch()).sensitiveWords; + if (this.isSensitive(data, sensitiveWords)) { data.visibility = 'home'; } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { data.visibility = 'home'; } } - const hasProhibitedWords = this.checkProhibitedWordsContain({ - cw: data.cw, - text: data.text, - pollChoices: data.poll?.choices, - }, this.meta.prohibitedWords); - - if (hasProhibitedWords) { - throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); + // Renote対象が「ホームまたは全体」以外の公開範囲ならreject + if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) { + throw new Error('Renote target is not public or home'); } - const inSilencedInstance = this.utilityService.isSilencedHost(this.meta.silencedHosts, user.host); - - if (data.visibility === 'public' && inSilencedInstance && user.host !== null) { + // Renote対象がpublicではないならhomeにする + if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') { data.visibility = 'home'; } - if (data.renote) { - switch (data.renote.visibility) { - case 'public': - // public noteは無条件にrenote可能 - break; - case 'home': - // home noteはhome以下にrenote可能 - if (data.visibility === 'public') { - data.visibility = 'home'; - } - break; - case 'followers': - // 他人のfollowers noteはreject - if (data.renote.userId !== user.id) { - throw new Error('Renote target is not public or home'); - } - - // Renote対象がfollowersならfollowersにする - data.visibility = 'followers'; - break; - case 'specified': - // specified / direct noteはreject - throw new Error('Renote target is not public or home'); - } - } - - // Check blocking - if (this.isRenote(data) && !this.isQuote(data)) { - if (data.renote.userHost === null) { - if (data.renote.userId !== user.id) { - const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id); - if (blocked) { - throw new Error('blocked'); - } - } - } + // Renote対象がfollowersならfollowersにする + if (data.renote && data.renote.visibility === 'followers') { + data.visibility = 'followers'; } // 返信対象がpublicではないならhomeにする @@ -361,9 +282,6 @@ export class NoteCreateService implements OnApplicationShutdown { data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); } data.text = data.text.trim(); - if (data.text === '') { - data.text = null; - } } else { data.text = null; } @@ -374,7 +292,7 @@ export class NoteCreateService implements OnApplicationShutdown { // Parse MFM if needed if (!tags || !emojis || !mentionedUsers) { - const tokens = (data.text ? mfm.parse(data.text)! : []); + const tokens = data.text ? mfm.parse(data.text)! : []; const cwTokens = data.cw ? mfm.parse(data.cw)! : []; const choiceTokens = data.poll && data.poll.choices ? concat(data.poll.choices.map(choice => mfm.parse(choice)!)) @@ -389,10 +307,7 @@ export class NoteCreateService implements OnApplicationShutdown { mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens); } - // if the host is media-silenced, custom emojis are not allowed - if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = []; - - tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32); + tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32); if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) { mentionedUsers.push(await this.usersRepository.findOneByOrFail({ id: data.reply!.userId })); @@ -412,29 +327,16 @@ export class NoteCreateService implements OnApplicationShutdown { } } - if (mentionedUsers.length > 0 && mentionedUsers.length > (await this.roleService.getUserPolicies(user.id)).mentionLimit) { - throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', 'Note contains too many mentions'); - } - - // BEGIN comfy.social - // Invoke customizable filter policy - if (this.noteFilterPluginFn != null) { - const filterResult = await this.noteFilterPluginFn({ - data: data, - user: user, - mentionedUsers: mentionedUsers, - remoteUserResolveService: this.remoteUserResolveService, - idService: this.idService, - }); - - if (filterResult.verdict) { - throw new Error(`Blocked by custom filter policy, reason: ${filterResult.reason}`); - } - } - // END comfy.social - const note = await this.insertNote(user, data, tags, emojis, mentionedUsers); + if (data.channel) { + this.redisClient.xadd( + `channelTimeline:${data.channel.id}`, + 'MAXLEN', '~', '1000', + '*', + 'note', note.id); + } + setImmediate('post created', { signal: this.#shutdownController.signal }).then( () => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!), () => { /* aborted, ignore this */ }, @@ -444,9 +346,10 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private async insertNote(user: { id: MiUser['id']; host: MiUser['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]) { - const insert = new MiNote({ - id: this.idService.gen(data.createdAt?.getTime()), + private async insertNote(user: { id: User['id']; host: User['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]) { + const insert = new Note({ + id: this.idService.genId(data.createdAt!), + createdAt: data.createdAt!, fileIds: data.files ? data.files.map(file => file.id) : [], replyId: data.reply ? data.reply.id : null, renoteId: data.renote ? data.renote.id : null, @@ -459,7 +362,7 @@ export class NoteCreateService implements OnApplicationShutdown { name: data.name, text: data.text, hasPoll: data.poll != null, - cw: data.cw ?? null, + cw: data.cw == null ? null : data.cw, tags: tags.map(tag => normalizeForSearch(tag)), emojis, userId: user.id, @@ -494,7 +397,7 @@ export class NoteCreateService implements OnApplicationShutdown { const url = profile != null ? profile.url : null; return { uri: u.uri, - url: url ?? undefined, + url: url == null ? undefined : url, username: u.username, host: u.host, } as IMentionedRemoteUsers[0]; @@ -506,9 +409,9 @@ export class NoteCreateService implements OnApplicationShutdown { if (insert.hasPoll) { // Start transaction await this.db.transaction(async transactionalEntityManager => { - await transactionalEntityManager.insert(MiNote, insert); + await transactionalEntityManager.insert(Note, insert); - const poll = new MiPoll({ + const poll = new Poll({ noteId: insert.id, choices: data.poll!.choices, expiresAt: data.poll!.expiresAt, @@ -517,10 +420,9 @@ export class NoteCreateService implements OnApplicationShutdown { noteVisibility: insert.visibility, userId: user.id, userHost: user.host, - channelId: insert.channelId, }); - await transactionalEntityManager.insert(MiPoll, poll); + await transactionalEntityManager.insert(Poll, poll); }); } else { await this.notesRepository.insert(insert); @@ -542,27 +444,28 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private async postNoteCreated(note: MiNote, user: { - id: MiUser['id']; - username: MiUser['username']; - host: MiUser['host']; - isBot: MiUser['isBot']; + private async postNoteCreated(note: Note, user: { + id: User['id']; + username: User['username']; + host: User['host']; + createdAt: User['createdAt']; + isBot: User['isBot']; }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { + const meta = await this.metaService.fetch(); + this.notesChart.update(note, true); - if (note.visibility !== 'specified' && (this.meta.enableChartsForRemoteUser || (user.host == null))) { + if (meta.enableChartsForRemoteUser || (user.host == null)) { this.perUserNotesChart.update(user, note, true); } // Register host - if (this.meta.enableStatsForFederatedInstances) { - if (this.userEntityService.isRemoteUser(user)) { - this.federatedInstanceService.fetchOrRegister(user.host).then(async i => { - this.updateNotesCountQueue.enqueue(i.id, 1); - if (this.meta.enableChartsForFederatedInstances) { - this.instanceChart.updateNote(i.host, note, true); - } - }); - } + if (this.userEntityService.isRemoteUser(user)) { + this.federatedInstanceService.fetch(user.host).then(async i => { + this.instancesRepository.increment({ id: i.id }, 'notesCount', 1); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateNote(i.host, note, true); + } + }); } // ハッシュタグ更新 @@ -573,44 +476,36 @@ export class NoteCreateService implements OnApplicationShutdown { // Increment notes count (user) this.incNotesCountOfUser(user); - this.pushToTl(note, user); + // Word mute + mutedWordsCache.fetch(() => this.userProfilesRepository.find({ + where: { + enableWordMute: true, + }, + select: ['userId', 'mutedWords'], + })).then(us => { + for (const u of us) { + checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => { + if (shouldMute) { + this.mutedNotesRepository.insert({ + id: this.idService.genId(), + userId: u.userId, + noteId: note.id, + reason: 'word', + }); + } + }); + } + }); - this.antennaService.addNoteToAntennas({ - ...note, - channel: data.channel ?? null, - }, user); + this.antennaService.addNoteToAntennas(note, user); if (data.reply) { this.saveReply(data.reply, note); } - if (data.reply == null) { - // TODO: キャッシュ - this.followingsRepository.findBy({ - followeeId: user.id, - notify: 'normal', - }).then(async followings => { - if (note.visibility !== 'specified') { - const isPureRenote = this.isRenote(data) && !this.isQuote(data) ? true : false; - for (const following of followings) { - // TODO: ワードミュート考慮 - let isRenoteMuted = false; - if (isPureRenote) { - const userIdsWhoMeMutingRenotes = await this.cacheService.renoteMutingsCache.fetch(following.followerId); - isRenoteMuted = userIdsWhoMeMutingRenotes.has(user.id); - } - if (!isRenoteMuted) { - this.notificationService.createNotification(following.followerId, 'note', { - noteId: note.id, - }, user.id); - } - } - } - }); - } - - if (data.renote && data.renote.userId !== user.id && !user.isBot) { - this.incRenoteCount(data.renote); + // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき + if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) { + if (!user.isBot) this.incRenoteCount(data.renote); } if (data.poll && data.poll.expiresAt) { @@ -619,28 +514,53 @@ export class NoteCreateService implements OnApplicationShutdown { noteId: note.id, }, { delay, - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, }); } if (!silent) { if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user); + // 未読通知を作成 + if (data.visibility === 'specified') { + if (data.visibleUsers == null) throw new Error('invalid param'); + + for (const u of data.visibleUsers) { + // ローカルユーザーのみ + if (!this.userEntityService.isLocalUser(u)) continue; + + this.noteReadService.insertNoteUnread(u.id, note, { + isSpecified: true, + isMentioned: false, + }); + } + } else { + for (const u of mentionedUsers) { + // ローカルユーザーのみ + if (!this.userEntityService.isLocalUser(u)) continue; + + this.noteReadService.insertNoteUnread(u.id, note, { + isSpecified: false, + isMentioned: true, + }); + } + } + // Pack the note - const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true }); + const noteObj = await this.noteEntityService.pack(note); this.globalEventService.publishNotesStream(noteObj); this.roleService.addNoteToRoleTimeline(noteObj); - this.webhookService.enqueueUserWebhook(user.id, 'note', { note: noteObj }); + this.webhookService.getActiveWebhooks().then(webhooks => { + webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'note', { + note: noteObj, + }); + } + }); const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note); @@ -650,24 +570,28 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.reply) { // 通知 if (data.reply.userHost === null) { - const isThreadMuted = await this.noteThreadMutingsRepository.exists({ - where: { - userId: data.reply.userId, - threadId: data.reply.threadId ?? data.reply.id, - }, + const threadMuted = await this.noteThreadMutingsRepository.findOneBy({ + userId: data.reply.userId, + threadId: data.reply.threadId ?? data.reply.id, }); - if (!isThreadMuted) { + if (!threadMuted) { nm.push(data.reply.userId, 'reply'); this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj); - this.webhookService.enqueueUserWebhook(data.reply.userId, 'reply', { note: noteObj }); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'reply', { + note: noteObj, + }); + } } } } // If it is renote - if (this.isRenote(data)) { - const type = this.isQuote(data) ? 'quote' : 'renote'; + if (data.renote) { + const type = data.text ? 'quote' : 'renote'; // Notify if (data.renote.userHost === null) { @@ -677,21 +601,27 @@ export class NoteCreateService implements OnApplicationShutdown { // Publish event if ((user.id !== data.renote.userId) && data.renote.userHost === null) { this.globalEventService.publishMainStream(data.renote.userId, 'renote', noteObj); - this.webhookService.enqueueUserWebhook(data.renote.userId, 'renote', { note: noteObj }); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'renote', { + note: noteObj, + }); + } } } - nm.notify(); + nm.deliver(); //#region AP deliver - if (!data.localOnly && this.userEntityService.isLocalUser(user)) { + if (this.userEntityService.isLocalUser(user)) { (async () => { const noteActivity = await this.renderNoteOrRenoteActivity(data, note); const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity); // メンションされたリモートユーザーに配送 for (const u of mentionedUsers.filter(u => this.userEntityService.isRemoteUser(u))) { - dm.addDirectRecipe(u as MiRemoteUser); + dm.addDirectRecipe(u as RemoteUser); } // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 @@ -715,7 +645,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.relayService.deliverToRelays(user, noteActivity); } - trackPromise(dm.execute()); + dm.execute(); })(); } //#endregion @@ -742,59 +672,52 @@ export class NoteCreateService implements OnApplicationShutdown { // Register to search database this.index(note); } - + @bindThis - private isRenote(note: Option): note is Option & { renote: MiNote } { - return note.renote != null; + private isSensitive(note: Option, sensitiveWord: string[]): boolean { + if (sensitiveWord.length > 0) { + const text = note.cw ?? note.text ?? ''; + if (text === '') return false; + const matched = sensitiveWord.some(filter => { + // represents RegExp + const regexp = filter.match(/^\/(.+)\/(.*)$/); + // This should never happen due to input sanitisation. + if (!regexp) { + const words = filter.split(' '); + return words.every(keyword => text.includes(keyword)); + } + try { + return new RE2(regexp[1], regexp[2]).test(text); + } catch (err) { + // This should never happen due to input sanitisation. + return false; + } + }); + if (matched) return true; + } + return false; } @bindThis - private isQuote(note: Option & { renote: MiNote }): note is Option & { renote: MiNote } & ( - { text: string } | { cw: string } | { reply: MiNote } | { poll: IPoll } | { files: MiDriveFile[] } - ) { - // NOTE: SYNC WITH misc/is-quote.ts - return note.text != null || - note.reply != null || - note.cw != null || - note.poll != null || - (note.files != null && note.files.length > 0); - } - - @bindThis - private incRenoteCount(renote: MiNote) { + private incRenoteCount(renote: Note) { this.notesRepository.createQueryBuilder().update() .set({ renoteCount: () => '"renoteCount" + 1', + score: () => '"score" + 1', }) .where('id = :id', { id: renote.id }) .execute(); - - // 30%の確率、3日以内に投稿されたノートの場合ハイライト用ランキング更新 - if (Math.random() < 0.3 && (Date.now() - this.idService.parse(renote.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3) { - if (renote.channelId != null) { - if (renote.replyId == null) { - this.featuredService.updateInChannelNotesRanking(renote.channelId, renote.id, 5); - } - } else { - if (renote.visibility === 'public' && renote.userHost == null && renote.replyId == null) { - this.featuredService.updateGlobalNotesRanking(renote.id, 5); - this.featuredService.updatePerUserNotesRanking(renote.userId, renote.id, 5); - } - } - } } @bindThis - private async createMentionedEvents(mentionedUsers: MinimumUser[], note: MiNote, nm: NotificationManager) { + private async createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager) { for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) { - const isThreadMuted = await this.noteThreadMutingsRepository.exists({ - where: { - userId: u.id, - threadId: note.threadId ?? note.id, - }, + const threadMuted = await this.noteThreadMutingsRepository.findOneBy({ + userId: u.id, + threadId: note.threadId ?? note.id, }); - if (isThreadMuted) { + if (threadMuted) { continue; } @@ -803,7 +726,13 @@ export class NoteCreateService implements OnApplicationShutdown { }); this.globalEventService.publishMainStream(u.id, 'mention', detailPackedNote); - this.webhookService.enqueueUserWebhook(u.id, 'mention', { note: detailPackedNote }); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'mention', { + note: detailPackedNote, + }); + } // Create notification nm.push(u.id, 'mention'); @@ -811,15 +740,15 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private saveReply(reply: MiNote, note: MiNote) { + private saveReply(reply: Note, note: Note) { this.notesRepository.increment({ id: reply.id }, 'repliesCount', 1); } @bindThis - private async renderNoteOrRenoteActivity(data: Option, note: MiNote) { + private async renderNoteOrRenoteActivity(data: Option, note: Note) { if (data.localOnly) return null; - const content = this.isRenote(data) && !this.isQuote(data) + const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0) ? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note) : this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note); @@ -827,14 +756,14 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private index(note: MiNote) { + private index(note: Note) { if (note.text == null && note.cw == null) return; - + this.searchService.indexNote(note); } @bindThis - private incNotesCountOfUser(user: { id: MiUser['id']; }) { + private incNotesCountOfUser(user: { id: User['id']; }) { this.usersRepository.createQueryBuilder().update() .set({ updatedAt: new Date(), @@ -845,13 +774,13 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private async extractMentionedUsers(user: { host: MiUser['host']; }, tokens: mfm.MfmNode[]): Promise { + private async extractMentionedUsers(user: { host: User['host']; }, tokens: mfm.MfmNode[]): Promise { if (tokens == null) return []; const mentions = extractMentions(tokens); let mentionedUsers = (await Promise.all(mentions.map(m => this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null), - ))).filter(x => x != null); + ))).filter(x => x != null) as User[]; // Drop duplicate users mentionedUsers = mentionedUsers.filter((u, i, self) => @@ -862,206 +791,12 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) { - if (!this.meta.enableFanoutTimeline) return; - - const r = this.redisForTimelines.pipeline(); - - if (note.channelId) { - this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); - - this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); - - const channelFollowings = await this.channelFollowingsRepository.find({ - where: { - followeeId: note.channelId, - }, - select: ['followerId'], - }); - - for (const channelFollowing of channelFollowings) { - this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); - if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); - } - } - } else { - // TODO: キャッシュ? - // eslint-disable-next-line prefer-const - let [followings, userListMemberships] = await Promise.all([ - this.followingsRepository.find({ - where: { - followeeId: user.id, - followerHost: IsNull(), - isFollowerHibernated: false, - }, - select: ['followerId', 'withReplies'], - }), - this.userListMembershipsRepository.find({ - where: { - userId: user.id, - }, - select: ['userListId', 'userListUserId', 'withReplies'], - }), - ]); - - if (note.visibility === 'followers') { - // TODO: 重そうだから何とかしたい Set 使う? - userListMemberships = userListMemberships.filter(x => x.userListUserId === user.id || followings.some(f => f.followerId === x.userListUserId)); - } - - // TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする - for (const following of followings) { - // 基本的にvisibleUserIdsには自身のidが含まれている前提であること - if (note.visibility === 'specified' && !note.visibleUserIds.some(v => v === following.followerId)) continue; - - // 「自分自身への返信 or そのフォロワーへの返信」のどちらでもない場合 - if (isReply(note, following.followerId)) { - if (!following.withReplies) continue; - } - - this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); - if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); - } - } - - for (const userListMembership of userListMemberships) { - // ダイレクトのとき、そのリストが対象外のユーザーの場合 - if ( - note.visibility === 'specified' && - note.userId !== userListMembership.userListUserId && - !note.visibleUserIds.some(v => v === userListMembership.userListUserId) - ) continue; - - // 「自分自身への返信 or そのリストの作成者への返信」のどちらでもない場合 - if (isReply(note, userListMembership.userListUserId)) { - if (!userListMembership.withReplies) continue; - } - - this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax, r); - if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax / 2, r); - } - } - - // 自分自身のHTL - if (note.userHost == null) { - if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { - this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); - if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); - } - } - } - - // 自分自身以外への返信 - if (isReply(note)) { - this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); - - if (note.visibility === 'public' && note.userHost == null) { - this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); - if (note.replyUserHost == null) { - this.fanoutTimelineService.push(`localTimelineWithReplyTo:${note.replyUserId}`, note.id, 300 / 10, r); - } - } - } else { - this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); - if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax / 2 : this.meta.perRemoteUserUserTimelineCacheMax / 2, r); - } - - if (note.visibility === 'public' && note.userHost == null) { - this.fanoutTimelineService.push('localTimeline', note.id, 1000, r); - if (note.fileIds.length > 0) { - this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); - } - } - } - - if (Math.random() < 0.1) { - process.nextTick(() => { - this.checkHibernation(followings); - }); - } - } - - r.exec(); - } - - @bindThis - public async checkHibernation(followings: MiFollowing[]) { - if (followings.length === 0) return; - - const shuffle = (array: MiFollowing[]) => { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } - return array; - }; - - // ランダムに最大1000件サンプリング - const samples = shuffle(followings).slice(0, Math.min(followings.length, 1000)); - - const hibernatedUsers = await this.usersRepository.find({ - where: { - id: In(samples.map(x => x.followerId)), - lastActiveDate: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 50))), - }, - select: ['id'], - }); - - if (hibernatedUsers.length > 0) { - this.usersRepository.update({ - id: In(hibernatedUsers.map(x => x.id)), - }, { - isHibernated: true, - }); - - this.followingsRepository.update({ - followerId: In(hibernatedUsers.map(x => x.id)), - }, { - isFollowerHibernated: true, - }); - } - } - - public checkProhibitedWordsContain(content: Parameters[0], prohibitedWords?: string[]) { - if (prohibitedWords == null) { - prohibitedWords = this.meta.prohibitedWords; - } - - if ( - this.utilityService.isKeyWordIncluded( - this.utilityService.concatNoteContentsForKeyWordCheck(content), - prohibitedWords, - ) - ) { - return true; - } - - return false; - } - - @bindThis - private collapseNotesCount(oldValue: number, newValue: number) { - return oldValue + newValue; - } - - @bindThis - private async performUpdateNotesCount(id: MiNote['id'], incrBy: number) { - await this.instancesRepository.increment({ id: id }, 'notesCount', incrBy); - } - - @bindThis - public async dispose(): Promise { + public dispose(): void { this.#shutdownController.abort(); - await this.updateNotesCountQueue.performAllNow(); } @bindThis - public async onApplicationShutdown(signal?: string | undefined): Promise { - await this.dispose(); + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); } } diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index e394506a44..dd878f7bba 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Brackets, In, IsNull, Not } from 'typeorm'; +import { Brackets, In } from 'typeorm'; import { Injectable, Inject } from '@nestjs/common'; -import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; -import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; -import type { InstancesRepository, MiMeta, NotesRepository, UsersRepository } from '@/models/_.js'; +import type { User, LocalUser, RemoteUser } from '@/models/entities/User.js'; +import type { Note, IMentionedRemoteUsers } from '@/models/entities/Note.js'; +import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/index.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -19,10 +14,9 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import { SearchService } from '@/core/SearchService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; +import { MetaService } from '@/core/MetaService.js'; @Injectable() export class NoteDeleteService { @@ -30,9 +24,6 @@ export class NoteDeleteService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -43,26 +34,31 @@ export class NoteDeleteService { private instancesRepository: InstancesRepository, private userEntityService: UserEntityService, + private noteEntityService: NoteEntityService, private globalEventService: GlobalEventService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, - private searchService: SearchService, - private moderationLogService: ModerationLogService, + private metaService: MetaService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private instanceChart: InstanceChart, ) {} - + /** * 投稿を削除します。 * @param user 投稿者 * @param note 投稿 */ - async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) { + async delete(user: { id: User['id']; uri: User['uri']; host: User['host']; isBot: User['isBot']; }, note: Note, quiet = false) { const deletedAt = new Date(); - const cascadingNotes = await this.findCascadingNotes(note); + + // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき + if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) { + this.notesRepository.decrement({ id: note.renoteId }, 'renoteCount', 1); + if (!user.isBot) this.notesRepository.decrement({ id: note.renoteId }, 'score', 1); + } if (note.replyId) { await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1); @@ -75,10 +71,10 @@ export class NoteDeleteService { //#region ローカルの投稿なら削除アクティビティを配送 if (this.userEntityService.isLocalUser(user) && !note.localOnly) { - let renote: MiNote | null = null; + let renote: Note | null = null; - // if deleted note is renote - if (isRenote(note) && !isQuote(note)) { + // if deletd note is renote + if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) { renote = await this.notesRepository.findOneBy({ id: note.renoteId, }); @@ -91,9 +87,9 @@ export class NoteDeleteService { this.deliverToConcerned(user, note, content); } - // also deliver delete activity to cascaded notes - const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes - for (const cascadingNote of federatedLocalCascadingNotes) { + // also deliever delete activity to cascaded notes + const cascadingNotes = (await this.findCascadingNotes(note)).filter(note => !note.localOnly); // filter out local-only notes + for (const cascadingNote of cascadingNotes) { if (!cascadingNote.user) continue; if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue; const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); @@ -101,48 +97,34 @@ export class NoteDeleteService { } //#endregion + const meta = await this.metaService.fetch(); + this.notesChart.update(note, false); - if (this.meta.enableChartsForRemoteUser || (user.host == null)) { + if (meta.enableChartsForRemoteUser || (user.host == null)) { this.perUserNotesChart.update(user, note, false); } - if (this.meta.enableStatsForFederatedInstances) { - if (this.userEntityService.isRemoteUser(user)) { - this.federatedInstanceService.fetchOrRegister(user.host).then(async i => { - this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1); - if (this.meta.enableChartsForFederatedInstances) { - this.instanceChart.updateNote(i.host, note, false); - } - }); - } + if (this.userEntityService.isRemoteUser(user)) { + this.federatedInstanceService.fetch(user.host).then(async i => { + this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateNote(i.host, note, false); + } + }); } } - for (const cascadingNote of cascadingNotes) { - this.searchService.unindexNote(cascadingNote); - } - this.searchService.unindexNote(note); - await this.notesRepository.delete({ id: note.id, userId: user.id, }); - - if (deleter && (note.userId !== deleter.id)) { - const user = await this.usersRepository.findOneByOrFail({ id: note.userId }); - this.moderationLogService.log(deleter, 'deleteNote', { - noteId: note.id, - noteUserId: note.userId, - noteUserUsername: user.username, - noteUserHost: user.host, - note: note, - }); - } } @bindThis - private async findCascadingNotes(note: MiNote): Promise { - const recursive = async (noteId: string): Promise => { + private async findCascadingNotes(note: Note) { + const cascadingNotes: Note[] = []; + + const recursive = async (noteId: string) => { const query = this.notesRepository.createQueryBuilder('note') .where('note.replyId = :noteId', { noteId }) .orWhere(new Brackets(q => { @@ -151,20 +133,18 @@ export class NoteDeleteService { })) .leftJoinAndSelect('note.user', 'user'); const replies = await query.getMany(); - - return [ - replies, - ...await Promise.all(replies.map(reply => recursive(reply.id))), - ].flat(); + for (const reply of replies) { + cascadingNotes.push(reply); + await recursive(reply.id); + } }; + await recursive(note.id); - const cascadingNotes: MiNote[] = await recursive(note.id); - - return cascadingNotes; + return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users } @bindThis - private async getMentionedRemoteUsers(note: MiNote) { + private async getMentionedRemoteUsers(note: Note) { const where = [] as any[]; // mention / reply / dm @@ -186,30 +166,16 @@ export class NoteDeleteService { return await this.usersRepository.find({ where, - }) as MiRemoteUser[]; + }) as RemoteUser[]; } @bindThis - private async getRenotedOrRepliedRemoteUsers(note: MiNote) { - const query = this.notesRepository.createQueryBuilder('note') - .leftJoinAndSelect('note.user', 'user') - .where(new Brackets(qb => { - qb.orWhere('note.renoteId = :renoteId', { renoteId: note.id }); - qb.orWhere('note.replyId = :replyId', { replyId: note.id }); - })) - .andWhere({ userHost: Not(IsNull()) }); - const notes = await query.getMany() as (MiNote & { user: MiRemoteUser })[]; - const remoteUsers = notes.map(({ user }) => user); - return remoteUsers; - } - - @bindThis - private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) { + private async deliverToConcerned(user: { id: LocalUser['id']; host: null; }, note: Note, content: any) { this.apDeliverManagerService.deliverToFollowers(user, content); this.relayService.deliverToRelays(user, content); - this.apDeliverManagerService.deliverToUsers(user, content, [ - ...await this.getMentionedRemoteUsers(note), - ...await this.getRenotedOrRepliedRemoteUsers(note), - ]); + const remoteUsers = await this.getMentionedRemoteUsers(note); + for (const remoteUser of remoteUsers) { + this.apDeliverManagerService.deliverToUser(user, content, remoteUser); + } } } diff --git a/packages/backend/src/core/NotePiningService.ts b/packages/backend/src/core/NotePiningService.ts index d38b48b65d..3a9f832ac0 100644 --- a/packages/backend/src/core/NotePiningService.ts +++ b/packages/backend/src/core/NotePiningService.ts @@ -1,16 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UserNotePiningsRepository, UsersRepository } from '@/models/_.js'; +import type { NotesRepository, UserNotePiningsRepository, UsersRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiNote } from '@/models/Note.js'; +import type { User } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; import { IdService } from '@/core/IdService.js'; -import type { MiUserNotePining } from '@/models/UserNotePining.js'; +import type { UserNotePining } from '@/models/entities/UserNotePining.js'; import { RelayService } from '@/core/RelayService.js'; import type { Config } from '@/config.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -49,7 +44,7 @@ export class NotePiningService { * @param noteId */ @bindThis - public async addPinned(user: { id: MiUser['id']; host: MiUser['host']; }, noteId: MiNote['id']) { + public async addPinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) { // Fetch pinee const note = await this.notesRepository.findOneBy({ id: noteId, @@ -71,13 +66,14 @@ export class NotePiningService { } await this.userNotePiningsRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), userId: user.id, noteId: note.id, - } as MiUserNotePining); + } as UserNotePining); // Deliver to remote followers - if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) { + if (this.userEntityService.isLocalUser(user)) { this.deliverPinnedChange(user.id, note.id, true); } } @@ -88,7 +84,7 @@ export class NotePiningService { * @param noteId */ @bindThis - public async removePinned(user: { id: MiUser['id']; host: MiUser['host']; }, noteId: MiNote['id']) { + public async removePinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) { // Fetch unpinee const note = await this.notesRepository.findOneBy({ id: noteId, @@ -105,13 +101,13 @@ export class NotePiningService { }); // Deliver to remote followers - if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) { + if (this.userEntityService.isLocalUser(user)) { this.deliverPinnedChange(user.id, noteId, false); } } @bindThis - public async deliverPinnedChange(userId: MiUser['id'], noteId: MiNote['id'], isAddition: boolean) { + public async deliverPinnedChange(userId: User['id'], noteId: Note['id'], isAddition: boolean) { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) throw new Error('user not found'); diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts new file mode 100644 index 0000000000..e57e57d310 --- /dev/null +++ b/packages/backend/src/core/NoteReadService.ts @@ -0,0 +1,134 @@ +import { setTimeout } from 'node:timers/promises'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import { In } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { User } from '@/models/entities/User.js'; +import type { Packed } from '@/misc/json-schema.js'; +import type { Note } from '@/models/entities/Note.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/index.js'; +import { bindThis } from '@/decorators.js'; + +@Injectable() +export class NoteReadService implements OnApplicationShutdown { + #shutdownController = new AbortController(); + + constructor( + @Inject(DI.noteUnreadsRepository) + private noteUnreadsRepository: NoteUnreadsRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + @Inject(DI.noteThreadMutingsRepository) + private noteThreadMutingsRepository: NoteThreadMutingsRepository, + + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + } + + @bindThis + public async insertNoteUnread(userId: User['id'], note: Note, params: { + // NOTE: isSpecifiedがtrueならisMentionedは必ずfalse + isSpecified: boolean; + isMentioned: boolean; + }): Promise { + //#region ミュートしているなら無視 + const mute = await this.mutingsRepository.findBy({ + muterId: userId, + }); + if (mute.map(m => m.muteeId).includes(note.userId)) return; + //#endregion + + // スレッドミュート + const threadMute = await this.noteThreadMutingsRepository.findOneBy({ + userId: userId, + threadId: note.threadId ?? note.id, + }); + if (threadMute) return; + + const unread = { + id: this.idService.genId(), + noteId: note.id, + userId: userId, + isSpecified: params.isSpecified, + isMentioned: params.isMentioned, + noteUserId: note.userId, + }; + + await this.noteUnreadsRepository.insert(unread); + + // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する + setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { + const exist = await this.noteUnreadsRepository.findOneBy({ id: unread.id }); + + if (exist == null) return; + + if (params.isMentioned) { + this.globalEventService.publishMainStream(userId, 'unreadMention', note.id); + } + if (params.isSpecified) { + this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id); + } + }, () => { /* aborted, ignore it */ }); + } + + @bindThis + public async read( + userId: User['id'], + notes: (Note | Packed<'Note'>)[], + ): Promise { + const readMentions: (Note | Packed<'Note'>)[] = []; + const readSpecifiedNotes: (Note | Packed<'Note'>)[] = []; + + for (const note of notes) { + if (note.mentions && note.mentions.includes(userId)) { + readMentions.push(note); + } else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) { + readSpecifiedNotes.push(note); + } + } + + if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0)) { + // Remove the record + await this.noteUnreadsRepository.delete({ + userId: userId, + noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]), + }); + + // TODO: ↓まとめてクエリしたい + + this.noteUnreadsRepository.countBy({ + userId: userId, + isMentioned: true, + }).then(mentionsCount => { + if (mentionsCount === 0) { + // 全て既読になったイベントを発行 + this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions'); + } + }); + + this.noteUnreadsRepository.countBy({ + userId: userId, + isSpecified: true, + }).then(specifiedCount => { + if (specifiedCount === 0) { + // 全て既読になったイベントを発行 + this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); + } + }); + } + } + + @bindThis + public dispose(): void { + this.#shutdownController.abort(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } +} diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index eeade4569b..ed47165f7b 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -1,58 +1,52 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { setTimeout } from 'node:timers/promises'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { In } from 'typeorm'; -import { ReplyError } from 'ioredis'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiNotification } from '@/models/Notification.js'; +import type { MutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import type { Notification } from '@/models/entities/Notification.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; -import type { Config } from '@/config.js'; -import { UserListService } from '@/core/UserListService.js'; -import { FilterUnionByProperty, groupedNotificationTypes, obsoleteNotificationTypes } from '@/types.js'; -import { trackPromise } from '@/misc/promise-tracker.js'; @Injectable() export class NotificationService implements OnApplicationShutdown { #shutdownController = new AbortController(); 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, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + private notificationEntityService: NotificationEntityService, + private userEntityService: UserEntityService, private idService: IdService, private globalEventService: GlobalEventService, private pushNotificationService: PushNotificationService, private cacheService: CacheService, - private userListService: UserListService, ) { } @bindThis public async readAllNotification( - userId: MiUser['id'], + userId: User['id'], force = false, ) { const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`); - + const latestNotificationIdsRes = await this.redisClient.xrevrange( `notificationTimeline:${userId}`, '+', @@ -70,132 +64,60 @@ export class NotificationService implements OnApplicationShutdown { } @bindThis - private postReadAllNotifications(userId: MiUser['id']) { + private postReadAllNotifications(userId: User['id']) { this.globalEventService.publishMainStream(userId, 'readAllNotifications'); this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined); } @bindThis - public createNotification( - notifieeId: MiUser['id'], - type: T, - data: Omit, 'type' | 'id' | 'createdAt' | 'notifierId'>, - notifierId?: MiUser['id'] | null, - ) { - trackPromise( - this.#createNotificationInternal(notifieeId, type, data, notifierId), - ); - } - - async #createNotificationInternal( - notifieeId: MiUser['id'], - type: T, - data: Omit, 'type' | 'id' | 'createdAt' | 'notifierId'>, - notifierId?: MiUser['id'] | null, - ): Promise { + public async createNotification( + notifieeId: User['id'], + type: Notification['type'], + data: Partial, + ): Promise { const profile = await this.cacheService.userProfileCache.fetch(notifieeId); + const isMuted = profile.mutingNotificationTypes.includes(type); + if (isMuted) return null; - // 古いMisskeyバージョンのキャッシュが残っている可能性がある - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const recieveConfig = (profile.notificationRecieveConfig ?? {})[type]; - if (recieveConfig?.type === 'never') { - return null; - } - - if (notifierId) { - if (notifieeId === notifierId) { + if (data.notifierId) { + if (notifieeId === data.notifierId) { return null; } const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId); - if (mutings.has(notifierId)) { + if (mutings.has(data.notifierId)) { return null; } - - if (recieveConfig?.type === 'following') { - const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)); - if (!isFollowing) { - return null; - } - } else if (recieveConfig?.type === 'follower') { - const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)); - if (!isFollower) { - return null; - } - } else if (recieveConfig?.type === 'mutualFollow') { - const [isFollowing, isFollower] = await Promise.all([ - this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)), - this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)), - ]); - if (!(isFollowing && isFollower)) { - return null; - } - } else if (recieveConfig?.type === 'followingOrFollower') { - const [isFollowing, isFollower] = await Promise.all([ - this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)), - this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)), - ]); - if (!isFollowing && !isFollower) { - return null; - } - } else if (recieveConfig?.type === 'list') { - const isMember = await this.userListService.membersCache.fetch(recieveConfig.userListId).then(members => members.has(notifierId)); - if (!isMember) { - return null; - } - } } - const createdAt = new Date(); - let notification: FilterUnionByProperty; - let redisId: string; + const notification = { + id: this.idService.genId(), + createdAt: new Date(), + type: type, + ...data, + } as Notification; - do { - notification = { - id: this.idService.gen(), - createdAt, - type: type, - ...(notifierId ? { - notifierId, - } : {}), - ...data, - } as unknown as FilterUnionByProperty; - - try { - redisId = (await this.redisClient.xadd( - `notificationTimeline:${notifieeId}`, - 'MAXLEN', '~', this.config.perUserNotificationsMaxCount.toString(), - this.toXListId(notification.id), - 'data', JSON.stringify(notification)))!; - } catch (e) { - // The ID specified in XADD is equal or smaller than the target stream top item で失敗することがあるのでリトライ - if (e instanceof ReplyError) continue; - throw e; - } - - break; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - } while (true); + const redisIdPromise = this.redisClient.xadd( + `notificationTimeline:${notifieeId}`, + 'MAXLEN', '~', '300', + '*', + 'data', JSON.stringify(notification)); const packed = await this.notificationEntityService.pack(notification, notifieeId, {}); - if (packed == null) return null; - // Publish notification event this.globalEventService.publishMainStream(notifieeId, 'notification', packed); // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する - // テスト通知の場合は即時発行 - const interval = notification.type === 'test' ? 0 : 2000; - setTimeout(interval, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => { + setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => { const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`); - if (latestReadNotificationId && (latestReadNotificationId >= redisId)) return; + if (latestReadNotificationId && (latestReadNotificationId >= (await redisIdPromise)!)) return; this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed); this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); - if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! })); - if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! })); + if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); + if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); }, () => { /* aborted, ignore it */ }); return notification; @@ -207,7 +129,7 @@ export class NotificationService implements OnApplicationShutdown { // TODO: locale ファイルをクライアント用とサーバー用で分けたい @bindThis - private async emailNotificationFollow(userId: MiUser['id'], follower: MiUser) { + private async emailNotificationFollow(userId: User['id'], follower: User) { /* const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return; @@ -219,7 +141,7 @@ export class NotificationService implements OnApplicationShutdown { } @bindThis - private async emailNotificationReceiveFollowRequest(userId: MiUser['id'], follower: MiUser) { + private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) { /* const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return; @@ -230,93 +152,11 @@ export class NotificationService implements OnApplicationShutdown { */ } - @bindThis - public async flushAllNotifications(userId: MiUser['id']) { - await Promise.all([ - this.redisClient.del(`notificationTimeline:${userId}`), - this.redisClient.del(`latestReadNotification:${userId}`), - ]); - this.globalEventService.publishMainStream(userId, 'notificationFlushed'); - } - @bindThis public dispose(): void { this.#shutdownController.abort(); } - private toXListId(id: string): string { - const { date, additional } = this.idService.parseFull(id); - return date.toString() + '-' + additional.toString(); - } - - @bindThis - public async getNotifications( - userId: MiUser['id'], - { - sinceId, - untilId, - limit = 20, - includeTypes, - excludeTypes, - }: { - sinceId?: string, - untilId?: string, - limit?: number, - // any extra types are allowed, those are no-op - includeTypes?: (MiNotification['type'] | string)[], - excludeTypes?: (MiNotification['type'] | string)[], - }, - ): Promise { - let sinceTime = sinceId ? this.toXListId(sinceId) : null; - let untilTime = untilId ? this.toXListId(untilId) : null; - - let notifications: MiNotification[]; - for (;;) { - let notificationsRes: [id: string, fields: string[]][]; - - // sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照 - if (sinceTime && !untilTime) { - notificationsRes = await this.redisClient.xrange( - `notificationTimeline:${userId}`, - '(' + sinceTime, - '+', - 'COUNT', limit); - } else { - notificationsRes = await this.redisClient.xrevrange( - `notificationTimeline:${userId}`, - untilTime ? '(' + untilTime : '+', - sinceTime ? '(' + sinceTime : '-', - 'COUNT', limit); - } - - if (notificationsRes.length === 0) { - return []; - } - - notifications = notificationsRes.map(x => JSON.parse(x[1][1])) as MiNotification[]; - - if (includeTypes && includeTypes.length > 0) { - notifications = notifications.filter(notification => includeTypes.includes(notification.type)); - } else if (excludeTypes && excludeTypes.length > 0) { - notifications = notifications.filter(notification => !excludeTypes.includes(notification.type)); - } - - if (notifications.length !== 0) { - // 通知が1件以上ある場合は返す - break; - } - - // フィルタしたことで通知が0件になった場合、次のページを取得する - if (sinceId && !untilId) { - sinceTime = notificationsRes[notificationsRes.length - 1][0]; - } else { - untilTime = notificationsRes[notificationsRes.length - 1][0]; - } - } - - return notifications; - } - @bindThis public onApplicationShutdown(signal?: string | undefined): void { this.dispose(); diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts index 6c96ab16cf..368753d9a7 100644 --- a/packages/backend/src/core/PollService.ts +++ b/packages/backend/src/core/PollService.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UsersRepository, PollsRepository, PollVotesRepository, MiUser } from '@/models/_.js'; -import type { MiNote } from '@/models/Note.js'; +import type { NotesRepository, UsersRepository, PollsRepository, PollVotesRepository, User } from '@/models/index.js'; +import type { Note } from '@/models/entities/Note.js'; import { RelayService } from '@/core/RelayService.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -42,14 +37,14 @@ export class PollService { } @bindThis - public async vote(user: MiUser, note: MiNote, choice: number) { + public async vote(user: User, note: Note, choice: number) { const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); - + if (poll == null) throw new Error('poll not found'); - + // Check whether is valid choice if (poll.choices[choice] == null) throw new Error('invalid choice param'); - + // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); @@ -57,13 +52,13 @@ export class PollService { throw new Error('blocked'); } } - + // if already voted const exist = await this.pollVotesRepository.findBy({ noteId: note.id, userId: user.id, }); - + if (poll.multiple) { if (exist.some(x => x.choice === choice)) { throw new Error('already voted'); @@ -71,18 +66,20 @@ export class PollService { } else if (exist.length !== 0) { throw new Error('already voted'); } - + + // Create vote await this.pollVotesRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), noteId: note.id, userId: user.id, choice: choice, }); - + // Increment votes count const index = choice + 1; // In SQL, array index is 1 based await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); - + this.globalEventService.publishNoteStream(note.id, 'pollVoted', { choice: choice, userId: user.id, @@ -90,15 +87,13 @@ export class PollService { } @bindThis - public async deliverQuestionUpdate(noteId: MiNote['id']) { + public async deliverQuestionUpdate(noteId: Note['id']) { const note = await this.notesRepository.findOneBy({ id: noteId }); if (note == null) throw new Error('note not found'); - - if (note.localOnly) return; - + const user = await this.usersRepository.findOneBy({ id: note.userId }); if (user == null) throw new Error('note not found'); - + if (this.userEntityService.isLocalUser(user)) { const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user)); this.apDeliverManagerService.deliverToFollowers(user, content); diff --git a/packages/backend/src/core/ProxyAccountService.ts b/packages/backend/src/core/ProxyAccountService.ts new file mode 100644 index 0000000000..780e56ef10 --- /dev/null +++ b/packages/backend/src/core/ProxyAccountService.ts @@ -0,0 +1,24 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository } from '@/models/index.js'; +import type { LocalUser } from '@/models/entities/User.js'; +import { DI } from '@/di-symbols.js'; +import { MetaService } from '@/core/MetaService.js'; +import { bindThis } from '@/decorators.js'; + +@Injectable() +export class ProxyAccountService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private metaService: MetaService, + ) { + } + + @bindThis + public async fetch(): Promise { + const meta = await this.metaService.fetch(); + if (meta.proxyAccountId == null) return null; + return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as LocalUser; + } +} diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index 9333c1ebc5..9ee83df644 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import push from 'web-push'; import * as Redis from 'ioredis'; @@ -10,7 +5,8 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { Packed } from '@/misc/json-schema.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; -import type { MiMeta, MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js'; +import type { SwSubscription, SwSubscriptionsRepository } from '@/models/index.js'; +import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { RedisKVCache } from '@/misc/cache.js'; @@ -22,7 +18,6 @@ type PushNotificationsTypes = { note: Packed<'Note'>; }; 'readAllNotifications': undefined; - newChatMessage: Packed<'ChatMessage'>; }; // Reduce length because push message servers have character limits @@ -36,7 +31,7 @@ function truncateBody(type: T, body: Pus ...body.note, // textをgetNoteSummaryしたものに置き換える text: getNoteSummary(('type' in body && body.type === 'renote') ? body.note.renote as Packed<'Note'> : body.note), - + cw: undefined, reply: undefined, renote: undefined, @@ -48,22 +43,21 @@ function truncateBody(type: T, body: Pus @Injectable() export class PushNotificationService implements OnApplicationShutdown { - private subscriptionsCache: RedisKVCache; + private subscriptionsCache: RedisKVCache; constructor( @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.redis) private redisClient: Redis.Redis, @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, + + private metaService: MetaService, ) { - this.subscriptionsCache = new RedisKVCache(this.redisClient, 'userSwSubscriptions', { + this.subscriptionsCache = new RedisKVCache(this.redisClient, 'userSwSubscriptions', { lifetime: 1000 * 60 * 60 * 1, // 1h memoryCacheLifetime: 1000 * 60 * 3, // 3m fetcher: (key) => this.swSubscriptionsRepository.findBy({ userId: key }), @@ -74,15 +68,17 @@ export class PushNotificationService implements OnApplicationShutdown { @bindThis public async pushNotification(userId: string, type: T, body: PushNotificationsTypes[T]) { - if (!this.meta.enableServiceWorker || this.meta.swPublicKey == null || this.meta.swPrivateKey == null) return; - + const meta = await this.metaService.fetch(); + + if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; + // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 push.setVapidDetails(this.config.url, - this.meta.swPublicKey, - this.meta.swPrivateKey); - + meta.swPublicKey, + meta.swPrivateKey); + const subscriptions = await this.subscriptionsCache.fetch(userId); - + for (const subscription of subscriptions) { if ([ 'readAllNotifications', @@ -100,33 +96,26 @@ export class PushNotificationService implements OnApplicationShutdown { type, body: (type === 'notification' || type === 'unreadAntennaNote') ? truncateBody(type, body) : body, userId, - dateTime: Date.now(), + dateTime: (new Date()).getTime(), }), { proxy: this.config.proxy, }).catch((err: any) => { //swLogger.info(err.statusCode); //swLogger.info(err.headers); //swLogger.info(err.body); - + if (err.statusCode === 410) { this.swSubscriptionsRepository.delete({ userId: userId, endpoint: subscription.endpoint, auth: subscription.auth, publickey: subscription.publickey, - }).then(() => { - this.refreshCache(userId); }); } }); } } - @bindThis - public refreshCache(userId: string): void { - this.subscriptionsCache.refresh(userId); - } - @bindThis public dispose(): void { this.subscriptionsCache.dispose(); diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index d398e83230..bf50a1cded 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -1,15 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Brackets, ObjectLiteral } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { MiUser } from '@/models/User.js'; -import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository, MiMeta } from '@/models/_.js'; +import type { User } from '@/models/entities/User.js'; +import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; import type { SelectQueryBuilder } from 'typeorm'; @Injectable() @@ -24,6 +18,9 @@ export class QueryService { @Inject(DI.channelFollowingsRepository) private channelFollowingsRepository: ChannelFollowingsRepository, + @Inject(DI.mutedNotesRepository) + private mutedNotesRepository: MutedNotesRepository, + @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, @@ -35,93 +32,39 @@ export class QueryService { @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, - - @Inject(DI.meta) - private meta: MiMeta, - - private idService: IdService, ) { } - public makePaginationQuery( - q: SelectQueryBuilder, - sinceId?: string | null, - untilId?: string | null, - sinceDate?: number | null, - untilDate?: number | null, - targetColumn = 'id', - ): SelectQueryBuilder { + public makePaginationQuery(q: SelectQueryBuilder, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder { if (sinceId && untilId) { - q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: sinceId }); - q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: untilId }); - q.orderBy(`${q.alias}.${targetColumn}`, 'DESC'); + q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); + q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); + q.orderBy(`${q.alias}.id`, 'DESC'); } else if (sinceId) { - q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: sinceId }); - q.orderBy(`${q.alias}.${targetColumn}`, 'ASC'); + q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); + q.orderBy(`${q.alias}.id`, 'ASC'); } else if (untilId) { - q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: untilId }); - q.orderBy(`${q.alias}.${targetColumn}`, 'DESC'); + q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); + q.orderBy(`${q.alias}.id`, 'DESC'); } else if (sinceDate && untilDate) { - q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: this.idService.gen(sinceDate) }); - q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: this.idService.gen(untilDate) }); - q.orderBy(`${q.alias}.${targetColumn}`, 'DESC'); + q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); + q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); + q.orderBy(`${q.alias}.createdAt`, 'DESC'); } else if (sinceDate) { - q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: this.idService.gen(sinceDate) }); - q.orderBy(`${q.alias}.${targetColumn}`, 'ASC'); + q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); + q.orderBy(`${q.alias}.createdAt`, 'ASC'); } else if (untilDate) { - q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: this.idService.gen(untilDate) }); - q.orderBy(`${q.alias}.${targetColumn}`, 'DESC'); + q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); + q.orderBy(`${q.alias}.createdAt`, 'DESC'); } else { - q.orderBy(`${q.alias}.${targetColumn}`, 'DESC'); + q.orderBy(`${q.alias}.id`, 'DESC'); } return q; - } - - /** - * ミュートやブロックのようにすべてのタイムラインで共通に使用するフィルターを定義します。 - * - * 特別な事情がない限り、各タイムラインはこの関数を呼び出してフィルターを適用してください。 - * - * Notes for future maintainers: - * 1) この関数で生成するクエリと同等の処理が FanoutTimelineEndpointService にあります。 - * この関数を変更した場合、FanoutTimelineEndpointService の方も変更する必要があります。 - * 2) 以下のエンドポイントでは特別な事情があるため queryService のそれぞれの関数を呼び出しています。 - * この関数を変更した場合、以下のエンドポイントの方も変更する必要があることがあります。 - * - packages/backend/src/server/api/endpoints/clips/notes.ts - */ - @bindThis - public generateBaseNoteFilteringQuery( - query: SelectQueryBuilder, - me: { id: MiUser['id'] } | null, - { - excludeUserFromMute, - excludeAuthor, - }: { - excludeUserFromMute?: MiUser['id'], - excludeAuthor?: boolean, - } = {}, - ): void { - this.generateBlockedHostQueryForNote(query, excludeAuthor); - this.generateSuspendedUserQueryForNote(query, excludeAuthor); - if (me) { - this.generateMutedUserQueryForNotes(query, me, { excludeUserFromMute }); - this.generateBlockedUserQueryForNotes(query, me); - this.generateMutedUserQueryForNotes(query, me, { noteColumn: 'renote', excludeUserFromMute }); - this.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' }); - } - } - + } + // ここでいうBlockedは被Blockedの意 @bindThis - public generateBlockedUserQueryForNotes( - q: SelectQueryBuilder, - me: { id: MiUser['id'] }, - { - noteColumn = 'note', - }: { - noteColumn?: string, - } = {}, - ): void { + public generateBlockedUserQuery(q: SelectQueryBuilder, me: { id: User['id'] }): void { const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking') .select('blocking.blockerId') .where('blocking.blockeeId = :blockeeId', { blockeeId: me.id }); @@ -130,27 +73,21 @@ export class QueryService { // 投稿の返信先の作者にブロックされていない かつ // 投稿の引用元の作者にブロックされていない q - .andWhere(new Brackets(qb => { - qb - .where(`${noteColumn}.userId IS NULL`) - .orWhere(`${noteColumn}.userId NOT IN (${ blockingQuery.getQuery() })`); + .andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`) + .andWhere(new Brackets(qb => { qb + .where('note.replyUserId IS NULL') + .orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`); })) - .andWhere(new Brackets(qb => { - qb - .where(`${noteColumn}.replyUserId IS NULL`) - .orWhere(`${noteColumn}.replyUserId NOT IN (${ blockingQuery.getQuery() })`); - })) - .andWhere(new Brackets(qb => { - qb - .where(`${noteColumn}.renoteUserId IS NULL`) - .orWhere(`${noteColumn}.renoteUserId NOT IN (${ blockingQuery.getQuery() })`); + .andWhere(new Brackets(qb => { qb + .where('note.renoteUserId IS NULL') + .orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`); })); q.setParameters(blockingQuery.getParameters()); } @bindThis - public generateBlockQueryForUsers(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { + public generateBlockQueryForUsers(q: SelectQueryBuilder, me: { id: User['id'] }): void { const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking') .select('blocking.blockeeId') .where('blocking.blockerId = :blockerId', { blockerId: me.id }); @@ -167,150 +104,184 @@ export class QueryService { } @bindThis - public generateMutedNoteThreadQuery(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { - const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted') - .select('threadMuted.threadId') - .where('threadMuted.userId = :userId', { userId: me.id }); + public generateChannelQuery(q: SelectQueryBuilder, me?: { id: User['id'] } | null): void { + if (me == null) { + q.andWhere('note.channelId IS NULL'); + } else { + q.leftJoinAndSelect('note.channel', 'channel'); + + const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing') + .select('channelFollowing.followeeId') + .where('channelFollowing.followerId = :followerId', { followerId: me.id }); + + q.andWhere(new Brackets(qb => { qb + // チャンネルのノートではない + .where('note.channelId IS NULL') + // または自分がフォローしているチャンネルのノート + .orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`); + })); + + q.setParameters(channelFollowingQuery.getParameters()); + } + } + @bindThis + public generateMutedNoteQuery(q: SelectQueryBuilder, me: { id: User['id'] }): void { + const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted') + .select('muted.noteId') + .where('muted.userId = :userId', { userId: me.id }); + q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); - q.andWhere(new Brackets(qb => { - qb - .where('note.threadId IS NULL') - .orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`); - })); - + q.setParameters(mutedQuery.getParameters()); } @bindThis - public generateMutedUserQueryForNotes( - q: SelectQueryBuilder, - me: { id: MiUser['id'] }, - { - excludeUserFromMute, - noteColumn = 'note', - }: { - excludeUserFromMute?: MiUser['id'], - noteColumn?: string, - } = {}, - ): void { + public generateMutedNoteThreadQuery(q: SelectQueryBuilder, me: { id: User['id'] }): void { + const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted') + .select('threadMuted.threadId') + .where('threadMuted.userId = :userId', { userId: me.id }); + + q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); + q.andWhere(new Brackets(qb => { qb + .where('note.threadId IS NULL') + .orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`); + })); + + q.setParameters(mutedQuery.getParameters()); + } + + @bindThis + public generateMutedUserQuery(q: SelectQueryBuilder, me: { id: User['id'] }, exclude?: User): void { const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') .select('muting.muteeId') .where('muting.muterId = :muterId', { muterId: me.id }); - - if (excludeUserFromMute) { - mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: excludeUserFromMute }); + + if (exclude) { + mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id }); } - + const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile') .select('user_profile.mutedInstances') .where('user_profile.userId = :muterId', { muterId: me.id }); - + // 投稿の作者をミュートしていない かつ // 投稿の返信先の作者をミュートしていない かつ // 投稿の引用元の作者をミュートしていない q - .andWhere(new Brackets(qb => { - qb - .where(`${noteColumn}.userId IS NULL`) - .orWhere(`${noteColumn}.userId NOT IN (${ mutingQuery.getQuery() })`); + .andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`) + .andWhere(new Brackets(qb => { qb + .where('note.replyUserId IS NULL') + .orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`); })) - .andWhere(new Brackets(qb => { - qb - .where(`${noteColumn}.replyUserId IS NULL`) - .orWhere(`${noteColumn}.replyUserId NOT IN (${ mutingQuery.getQuery() })`); - })) - .andWhere(new Brackets(qb => { - qb - .where(`${noteColumn}.renoteUserId IS NULL`) - .orWhere(`${noteColumn}.renoteUserId NOT IN (${ mutingQuery.getQuery() })`); + .andWhere(new Brackets(qb => { qb + .where('note.renoteUserId IS NULL') + .orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`); })) // mute instances - .andWhere(new Brackets(qb => { - qb - .andWhere(`${noteColumn}.userHost IS NULL`) - .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.userHost)`); + .andWhere(new Brackets(qb => { qb + .andWhere('note.userHost IS NULL') + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`); })) - .andWhere(new Brackets(qb => { - qb - .where(`${noteColumn}.replyUserHost IS NULL`) - .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.replyUserHost)`); + .andWhere(new Brackets(qb => { qb + .where('note.replyUserHost IS NULL') + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`); })) - .andWhere(new Brackets(qb => { - qb - .where(`${noteColumn}.renoteUserHost IS NULL`) - .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.renoteUserHost)`); + .andWhere(new Brackets(qb => { qb + .where('note.renoteUserHost IS NULL') + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`); })); - + q.setParameters(mutingQuery.getParameters()); q.setParameters(mutingInstanceQuery.getParameters()); } @bindThis - public generateMutedUserQueryForUsers(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { + public generateMutedUserQueryForUsers(q: SelectQueryBuilder, me: { id: User['id'] }): void { const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') .select('muting.muteeId') .where('muting.muterId = :muterId', { muterId: me.id }); - + q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`); - + q.setParameters(mutingQuery.getParameters()); } @bindThis - public generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: MiUser['id'] } | null): void { + public generateRepliesQuery(q: SelectQueryBuilder, withReplies: boolean, me?: Pick | null): void { + if (me == null) { + q.andWhere(new Brackets(qb => { qb + .where('note.replyId IS NULL') // 返信ではない + .orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信 + .where('note.replyId IS NOT NULL') + .andWhere('note.replyUserId = note.userId'); + })); + })); + } else if (!withReplies) { + q.andWhere(new Brackets(qb => { qb + .where('note.replyId IS NULL') // 返信ではない + .orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信 + .orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信 + .where('note.replyId IS NOT NULL') + .andWhere('note.userId = :meId', { meId: me.id }); + })) + .orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信 + .where('note.replyId IS NOT NULL') + .andWhere('note.replyUserId = note.userId'); + })); + })); + } + } + + @bindThis + public generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: User['id'] } | null): void { // This code must always be synchronized with the checks in Notes.isVisibleForMe. if (me == null) { - q.andWhere(new Brackets(qb => { - qb - .where('note.visibility = \'public\'') - .orWhere('note.visibility = \'home\''); + q.andWhere(new Brackets(qb => { qb + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); })); } else { const followingQuery = this.followingsRepository.createQueryBuilder('following') .select('following.followeeId') .where('following.followerId = :meId'); - - q.andWhere(new Brackets(qb => { - qb + + q.andWhere(new Brackets(qb => { qb // 公開投稿である - .where(new Brackets(qb => { - qb - .where('note.visibility = \'public\'') - .orWhere('note.visibility = \'home\''); - })) + .where(new Brackets(qb => { qb + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); + })) // または 自分自身 - .orWhere('note.userId = :meId') + .orWhere('note.userId = :meId') // または 自分宛て - .orWhere(':meIdAsList <@ note.visibleUserIds') - .orWhere(':meIdAsList <@ note.mentions') - .orWhere(new Brackets(qb => { - qb - // または フォロワー宛ての投稿であり、 - .where('note.visibility = \'followers\'') - .andWhere(new Brackets(qb => { - qb - // 自分がフォロワーである - .where(`note.userId IN (${ followingQuery.getQuery() })`) - // または 自分の投稿へのリプライ - .orWhere('note.replyUserId = :meId'); - })); + .orWhere(':meId = ANY(note.visibleUserIds)') + .orWhere(':meId = ANY(note.mentions)') + .orWhere(new Brackets(qb => { qb + // または フォロワー宛ての投稿であり、 + .where('note.visibility = \'followers\'') + .andWhere(new Brackets(qb => { qb + // 自分がフォロワーである + .where(`note.userId IN (${ followingQuery.getQuery() })`) + // または 自分の投稿へのリプライ + .orWhere('note.replyUserId = :meId'); })); + })); })); - - q.setParameters({ meId: me.id, meIdAsList: [me.id] }); + + q.setParameters({ meId: me.id }); } } @bindThis - public generateMutedUserRenotesQueryForNotes(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { + public generateMutedUserRenotesQueryForNotes(q: SelectQueryBuilder, me: { id: User['id'] }): void { const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting') .select('renote_muting.muteeId') .where('renote_muting.muterId = :muterId', { muterId: me.id }); - + q.andWhere(new Brackets(qb => { qb - .where(new Brackets(qb => { + .where(new Brackets(qb => { qb.where('note.renoteId IS NOT NULL'); qb.andWhere('note.text IS NULL'); qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`); @@ -318,62 +289,7 @@ export class QueryService { .orWhere('note.renoteId IS NULL') .orWhere('note.text IS NOT NULL'); })); - + q.setParameters(mutingQuery.getParameters()); } - - @bindThis - public generateBlockedHostQueryForNote(q: SelectQueryBuilder, excludeAuthor?: boolean): void { - let nonBlockedHostQuery: (part: string) => string; - if (this.meta.blockedHosts.length === 0) { - nonBlockedHostQuery = () => '1=1'; - } else { - nonBlockedHostQuery = (match: string) => `${match} NOT ILIKE ALL(ARRAY[:...blocked])`; - q.setParameters({ blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }); - } - - if (excludeAuthor) { - const instanceSuspension = (user: string) => new Brackets(qb => qb - .where(`note.${user}Id IS NULL`) // no corresponding user - .orWhere(`note.userId = note.${user}Id`) - .orWhere(`note.${user}Host IS NULL`) // local - .orWhere(nonBlockedHostQuery(`note.${user}Host`))); - - q - .andWhere(instanceSuspension('replyUser')) - .andWhere(instanceSuspension('renoteUser')); - } else { - const instanceSuspension = (user: string) => new Brackets(qb => qb - .where(`note.${user}Id IS NULL`) // no corresponding user - .orWhere(`note.${user}Host IS NULL`) // local - .orWhere(nonBlockedHostQuery(`note.${user}Host`))); - - q - .andWhere(instanceSuspension('user')) - .andWhere(instanceSuspension('replyUser')) - .andWhere(instanceSuspension('renoteUser')); - } - } - - // Requirements: user replyUser renoteUser must be joined - @bindThis - public generateSuspendedUserQueryForNote(q: SelectQueryBuilder, excludeAuthor?: boolean): void { - if (excludeAuthor) { - const brakets = (user: string) => new Brackets(qb => qb - .where(`note.${user}Id IS NULL`) - .orWhere(`user.id = ${user}.id`) - .orWhere(`${user}.isSuspended = FALSE`)); - q - .andWhere(brakets('replyUser')) - .andWhere(brakets('renoteUser')); - } else { - const brakets = (user: string) => new Brackets(qb => qb - .where(`note.${user}Id IS NULL`) - .orWhere(`${user}.isSuspended = FALSE`)); - q - .andWhere('user.isSuspended = FALSE') - .andWhere(brakets('replyUser')) - .andWhere(brakets('renoteUser')); - } - } } diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index b10b8e5899..3384ca4577 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -1,23 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - +import { setTimeout } from 'node:timers/promises'; import { Inject, Module, OnApplicationShutdown } from '@nestjs/common'; import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import { baseQueueOptions, QUEUE } from '@/queue/const.js'; -import { allSettled } from '@/misc/promise-tracker.js'; -import { - DeliverJobData, - EndedPollNotificationJobData, - InboxJobData, - RelationshipJobData, - UserWebhookDeliverJobData, - SystemWebhookDeliverJobData, -} from '../queue/types.js'; +import { QUEUE, baseQueueOptions } from '@/queue/const.js'; import type { Provider } from '@nestjs/common'; +import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js'; export type SystemQueue = Bull.Queue>; export type EndedPollNotificationQueue = Bull.Queue; @@ -26,8 +14,7 @@ export type InboxQueue = Bull.Queue; export type DbQueue = Bull.Queue; export type RelationshipQueue = Bull.Queue; export type ObjectStorageQueue = Bull.Queue; -export type UserWebhookDeliverQueue = Bull.Queue; -export type SystemWebhookDeliverQueue = Bull.Queue; +export type WebhookDeliverQueue = Bull.Queue; const $system: Provider = { provide: 'queue:system', @@ -71,15 +58,9 @@ const $objectStorage: Provider = { inject: [DI.config], }; -const $userWebhookDeliver: Provider = { - provide: 'queue:userWebhookDeliver', - useFactory: (config: Config) => new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER)), - inject: [DI.config], -}; - -const $systemWebhookDeliver: Provider = { - provide: 'queue:systemWebhookDeliver', - useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER)), +const $webhookDeliver: Provider = { + provide: 'queue:webhookDeliver', + useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)), inject: [DI.config], }; @@ -94,8 +75,7 @@ const $systemWebhookDeliver: Provider = { $db, $relationship, $objectStorage, - $userWebhookDeliver, - $systemWebhookDeliver, + $webhookDeliver, ], exports: [ $system, @@ -105,8 +85,7 @@ const $systemWebhookDeliver: Provider = { $db, $relationship, $objectStorage, - $userWebhookDeliver, - $systemWebhookDeliver, + $webhookDeliver, ], }) export class QueueModule implements OnApplicationShutdown { @@ -118,14 +97,18 @@ export class QueueModule implements OnApplicationShutdown { @Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:relationship') public relationshipQueue: RelationshipQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, - @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, - @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, + @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, ) {} public async dispose(): Promise { - // Wait for all potential queue jobs - await allSettled(); - // And then close all queues + if (process.env.NODE_ENV === 'test') { + // XXX: + // Shutting down the existing connections causes errors on Jest as + // Misskey has asynchronous postgres/redis connections that are not + // awaited. + // Let's wait for some random time for them to finish. + await setTimeout(5000); + } await Promise.all([ this.systemQueue.close(), this.endedPollNotificationQueue.close(), @@ -134,8 +117,7 @@ export class QueueModule implements OnApplicationShutdown { this.dbQueue.close(), this.relationshipQueue.close(), this.objectStorageQueue.close(), - this.userWebhookDeliverQueue.close(), - this.systemWebhookDeliverQueue.close(), + this.webhookDeliverQueue.close(), ]); } diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 04bbc7e38a..5b7359074e 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -1,57 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; -import { MetricsTime, type JobType } from 'bullmq'; -import { parse as parseRedisInfo } from 'redis-info'; +import { v4 as uuid } from 'uuid'; import type { IActivity } from '@/core/activitypub/type.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js'; -import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; -import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; -import { type SystemWebhookPayload } from '@/core/SystemWebhookService.js'; -import { type UserWebhookPayload } from './UserWebhookService.js'; -import type { - DbJobData, - DeliverJobData, - RelationshipJobData, - SystemWebhookDeliverJobData, - ThinUser, - UserWebhookDeliverJobData, -} from '../queue/types.js'; -import type { - DbQueue, - DeliverQueue, - EndedPollNotificationQueue, - InboxQueue, - ObjectStorageQueue, - RelationshipQueue, - SystemQueue, - SystemWebhookDeliverQueue, - UserWebhookDeliverQueue, -} from './QueueModule.js'; +import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; +import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; -import type { Packed } from '@/misc/json-schema.js'; - -export const QUEUE_TYPES = [ - 'system', - 'endedPollNotification', - 'deliver', - 'inbox', - 'db', - 'relationship', - 'objectStorage', - 'userWebhookDeliver', - 'systemWebhookDeliver', -] as const; @Injectable() export class QueueService { @@ -66,64 +25,42 @@ export class QueueService { @Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:relationship') public relationshipQueue: RelationshipQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, - @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, - @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, + @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, ) { this.systemQueue.add('tickCharts', { }, { repeat: { pattern: '55 * * * *' }, - removeOnComplete: 10, - removeOnFail: 30, + removeOnComplete: true, }); this.systemQueue.add('resyncCharts', { }, { repeat: { pattern: '0 0 * * *' }, - removeOnComplete: 10, - removeOnFail: 30, + removeOnComplete: true, }); this.systemQueue.add('cleanCharts', { }, { repeat: { pattern: '0 0 * * *' }, - removeOnComplete: 10, - removeOnFail: 30, + removeOnComplete: true, }); this.systemQueue.add('aggregateRetention', { }, { repeat: { pattern: '0 0 * * *' }, - removeOnComplete: 10, - removeOnFail: 30, + removeOnComplete: true, }); this.systemQueue.add('clean', { }, { repeat: { pattern: '0 0 * * *' }, - removeOnComplete: 10, - removeOnFail: 30, + removeOnComplete: true, }); this.systemQueue.add('checkExpiredMutings', { }, { repeat: { pattern: '*/5 * * * *' }, - removeOnComplete: 10, - removeOnFail: 30, - }); - - this.systemQueue.add('bakeBufferedReactions', { - }, { - repeat: { pattern: '0 0 * * *' }, - removeOnComplete: 10, - removeOnFail: 30, - }); - - this.systemQueue.add('checkModeratorsActivity', { - }, { - // 毎時30分に起動 - repeat: { pattern: '30 * * * *' }, - removeOnComplete: 10, - removeOnFail: 30, + removeOnComplete: true, }); } @@ -132,80 +69,25 @@ export class QueueService { if (content == null) return null; if (to == null) return null; - const contentBody = JSON.stringify(content); - const digest = ApRequestCreator.createDigest(contentBody); - - const data: DeliverJobData = { + const data = { user: { id: user.id, }, - content: contentBody, - digest, + content, to, isSharedInbox, }; - const label = to.replace('https://', '').replace('/inbox', ''); - - return this.deliverQueue.add(label, data, { + return this.deliverQueue.add(to, data, { attempts: this.config.deliverJobMaxAttempts ?? 12, backoff: { type: 'custom', }, - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } - /** - * ApDeliverManager-DeliverManager.execute()からinboxesを突っ込んでaddBulkしたい - * @param user `{ id: string; }` この関数ではThinUserに変換しないので前もって変換してください - * @param content IActivity | null - * @param inboxes `Map` / key: to (inbox url), value: isSharedInbox (whether it is sharedInbox) - * @returns void - */ - @bindThis - public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map) { - if (content == null) return null; - const contentBody = JSON.stringify(content); - const digest = ApRequestCreator.createDigest(contentBody); - - const opts = { - attempts: this.config.deliverJobMaxAttempts ?? 12, - backoff: { - type: 'custom', - }, - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, - }; - - await this.deliverQueue.addBulk(Array.from(inboxes.entries(), d => ({ - name: d[0].replace('https://', '').replace('/inbox', ''), - data: { - user, - content: contentBody, - digest, - to: d[0], - isSharedInbox: d[1], - } as DeliverJobData, - opts, - }))); - - return; - } - @bindThis public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) { const data = { @@ -213,21 +95,13 @@ export class QueueService { signature, }; - const label = (activity.id ?? '').replace('https://', '').replace('/activity', ''); - - return this.inboxQueue.add(label, data, { + return this.inboxQueue.add('', data, { attempts: this.config.inboxJobMaxAttempts ?? 8, backoff: { type: 'custom', }, - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @@ -236,14 +110,8 @@ export class QueueService { return this.dbQueue.add('deleteDriveFiles', { user: { id: user.id }, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @@ -252,14 +120,8 @@ export class QueueService { return this.dbQueue.add('exportCustomEmojis', { user: { id: user.id }, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @@ -268,30 +130,8 @@ export class QueueService { return this.dbQueue.add('exportNotes', { user: { id: user.id }, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, - }); - } - - @bindThis - public createExportClipsJob(user: ThinUser) { - return this.dbQueue.add('exportClips', { - user: { id: user.id }, - }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @@ -300,14 +140,8 @@ export class QueueService { return this.dbQueue.add('exportFavorites', { user: { id: user.id }, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @@ -318,14 +152,8 @@ export class QueueService { excludeMuting, excludeInactive, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @@ -334,14 +162,8 @@ export class QueueService { return this.dbQueue.add('exportMuting', { user: { id: user.id }, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @@ -350,14 +172,8 @@ export class QueueService { return this.dbQueue.add('exportBlocking', { user: { id: user.id }, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @@ -366,14 +182,8 @@ export class QueueService { return this.dbQueue.add('exportUserLists', { user: { id: user.id }, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @@ -382,72 +192,47 @@ export class QueueService { return this.dbQueue.add('exportAntennas', { user: { id: user.id }, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @bindThis - public createImportFollowingJob(user: ThinUser, fileId: MiDriveFile['id'], withReplies?: boolean) { + public createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importFollowing', { user: { id: user.id }, fileId: fileId, - withReplies, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @bindThis - public createImportFollowingToDbJob(user: ThinUser, targets: string[], withReplies?: boolean) { - const jobs = targets.map(rel => this.generateToDbJobData('importFollowingToDb', { user, target: rel, withReplies })); + public createImportFollowingToDbJob(user: ThinUser, targets: string[]) { + const jobs = targets.map(rel => this.generateToDbJobData('importFollowingToDb', { user, target: rel })); return this.dbQueue.addBulk(jobs); } @bindThis - public createImportMutingJob(user: ThinUser, fileId: MiDriveFile['id']) { + public createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importMuting', { user: { id: user.id }, fileId: fileId, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @bindThis - public createImportBlockingJob(user: ThinUser, fileId: MiDriveFile['id']) { + public createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importBlocking', { user: { id: user.id }, fileId: fileId, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @@ -467,49 +252,31 @@ export class QueueService { name, data, opts: { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }, }; } @bindThis - public createImportUserListsJob(user: ThinUser, fileId: MiDriveFile['id']) { + public createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importUserLists', { user: { id: user.id }, fileId: fileId, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @bindThis - public createImportCustomEmojisJob(user: ThinUser, fileId: MiDriveFile['id']) { + public createImportCustomEmojisJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importCustomEmojis', { user: { id: user.id }, fileId: fileId, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @@ -519,14 +286,8 @@ export class QueueService { user: { id: user.id }, antenna, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @@ -536,19 +297,13 @@ export class QueueService { user: { id: user.id }, soft: opts.soft, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @bindThis - public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean, withReplies?: boolean }[]) { + public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean }[]) { const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel)); return this.relationshipQueue.addBulk(jobs); } @@ -590,17 +345,10 @@ export class QueueService { to: { id: data.to.id }, silent: data.silent, requestId: data.requestId, - withReplies: data.withReplies, }, opts: { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, ...opts, }, }; @@ -611,43 +359,22 @@ export class QueueService { return this.objectStorageQueue.add('deleteFile', { key: key, }, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @bindThis public createCleanRemoteFilesJob() { return this.objectStorageQueue.add('cleanRemoteFiles', {}, { - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } - /** - * @see UserWebhookDeliverJobData - * @see UserWebhookDeliverProcessorService - */ @bindThis - public userWebhookDeliver( - webhook: MiWebhook, - type: T, - content: UserWebhookPayload, - opts?: { attempts?: number }, - ) { - const data: UserWebhookDeliverJobData = { + public webhookDeliver(webhook: Webhook, type: typeof webhookEventTypes[number], content: unknown) { + const data = { type, content, webhookId: webhook.id, @@ -655,246 +382,29 @@ export class QueueService { to: webhook.url, secret: webhook.secret, createdAt: Date.now(), - eventId: randomUUID(), + eventId: uuid(), }; - return this.userWebhookDeliverQueue.add(webhook.id, data, { - attempts: opts?.attempts ?? 4, + return this.webhookDeliverQueue.add(webhook.id, data, { + attempts: 4, backoff: { type: 'custom', }, - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, - }); - } - - /** - * @see SystemWebhookDeliverJobData - * @see SystemWebhookDeliverProcessorService - */ - @bindThis - public systemWebhookDeliver( - webhook: MiSystemWebhook, - type: T, - content: SystemWebhookPayload, - opts?: { attempts?: number }, - ) { - const data: SystemWebhookDeliverJobData = { - type, - content, - webhookId: webhook.id, - to: webhook.url, - secret: webhook.secret, - createdAt: Date.now(), - eventId: randomUUID(), - }; - - return this.systemWebhookDeliverQueue.add(webhook.id, data, { - attempts: opts?.attempts ?? 4, - backoff: { - type: 'custom', - }, - removeOnComplete: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 30, - }, - removeOnFail: { - age: 3600 * 24 * 7, // keep up to 7 days - count: 100, - }, + removeOnComplete: true, + removeOnFail: true, }); } @bindThis - private getQueue(type: typeof QUEUE_TYPES[number]): Bull.Queue { - switch (type) { - case 'system': return this.systemQueue; - case 'endedPollNotification': return this.endedPollNotificationQueue; - case 'deliver': return this.deliverQueue; - case 'inbox': return this.inboxQueue; - case 'db': return this.dbQueue; - case 'relationship': return this.relationshipQueue; - case 'objectStorage': return this.objectStorageQueue; - case 'userWebhookDeliver': return this.userWebhookDeliverQueue; - case 'systemWebhookDeliver': return this.systemWebhookDeliverQueue; - default: throw new Error(`Unrecognized queue type: ${type}`); - } - } - - @bindThis - public async queueClear(queueType: typeof QUEUE_TYPES[number], state: '*' | 'completed' | 'wait' | 'active' | 'paused' | 'prioritized' | 'delayed' | 'failed') { - const queue = this.getQueue(queueType); - - if (state === '*') { - await Promise.all([ - queue.clean(0, 0, 'completed'), - queue.clean(0, 0, 'wait'), - queue.clean(0, 0, 'active'), - queue.clean(0, 0, 'paused'), - queue.clean(0, 0, 'prioritized'), - queue.clean(0, 0, 'delayed'), - queue.clean(0, 0, 'failed'), - ]); - } else { - await queue.clean(0, 0, state); - } - } - - @bindThis - public async queuePromoteJobs(queueType: typeof QUEUE_TYPES[number]) { - const queue = this.getQueue(queueType); - await queue.promoteJobs(); - } - - @bindThis - public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) { - const queue = this.getQueue(queueType); - const job: Bull.Job | null = await queue.getJob(jobId); - if (job) { - if (job.finishedOn != null) { - await job.retry(); - } else { - await job.promote(); - } - } - } - - @bindThis - public async queueRemoveJob(queueType: typeof QUEUE_TYPES[number], jobId: string) { - const queue = this.getQueue(queueType); - const job: Bull.Job | null = await queue.getJob(jobId); - if (job) { - await job.remove(); - } - } - - @bindThis - private packJobData(job: Bull.Job): Packed<'QueueJob'> { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const stacktrace = job.stacktrace ? job.stacktrace.filter(Boolean) : []; - stacktrace.reverse(); - - return { - id: job.id!, - name: job.name, - data: job.data, - opts: job.opts, - timestamp: job.timestamp, - processedOn: job.processedOn, - processedBy: job.processedBy, - finishedOn: job.finishedOn, - progress: job.progress, - attempts: job.attemptsMade, - delay: job.delay, - failedReason: job.failedReason, - stacktrace: stacktrace, - returnValue: job.returnvalue, - isFailed: !!job.failedReason || (Array.isArray(stacktrace) && stacktrace.length > 0), - }; - } - - @bindThis - public async queueGetJob(queueType: typeof QUEUE_TYPES[number], jobId: string) { - const queue = this.getQueue(queueType); - const job: Bull.Job | null = await queue.getJob(jobId); - if (job) { - return this.packJobData(job); - } else { - throw new Error(`Job not found: ${jobId}`); - } - } - - @bindThis - public async queueGetJobs(queueType: typeof QUEUE_TYPES[number], jobTypes: JobType[], search?: string) { - const RETURN_LIMIT = 100; - const queue = this.getQueue(queueType); - let jobs: Bull.Job[]; - - if (search) { - jobs = await queue.getJobs(jobTypes, 0, 1000); - - jobs = jobs.filter(job => { - const jobString = JSON.stringify(job).toLowerCase(); - return search.toLowerCase().split(' ').every(term => { - return jobString.includes(term); - }); - }); - - jobs = jobs.slice(0, RETURN_LIMIT); - } else { - jobs = await queue.getJobs(jobTypes, 0, RETURN_LIMIT); - } - - return jobs.map(job => this.packJobData(job)); - } - - @bindThis - public async queueGetQueues() { - const fetchings = QUEUE_TYPES.map(async type => { - const queue = this.getQueue(type); - - const counts = await queue.getJobCounts(); - const isPaused = await queue.isPaused(); - const metrics_completed = await queue.getMetrics('completed', 0, MetricsTime.ONE_WEEK); - const metrics_failed = await queue.getMetrics('failed', 0, MetricsTime.ONE_WEEK); - - return { - name: type, - counts: counts, - isPaused, - metrics: { - completed: metrics_completed, - failed: metrics_failed, - }, - }; + public destroy() { + this.deliverQueue.once('cleaned', (jobs, status) => { + //deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); }); + this.deliverQueue.clean(0, 0, 'delayed'); - return await Promise.all(fetchings); - } - - @bindThis - public async queueGetQueue(queueType: typeof QUEUE_TYPES[number]) { - const queue = this.getQueue(queueType); - const counts = await queue.getJobCounts(); - const isPaused = await queue.isPaused(); - const metrics_completed = await queue.getMetrics('completed', 0, MetricsTime.ONE_WEEK); - const metrics_failed = await queue.getMetrics('failed', 0, MetricsTime.ONE_WEEK); - const db = parseRedisInfo(await (await queue.client).info()); - - return { - name: queueType, - qualifiedName: queue.qualifiedName, - counts: counts, - isPaused, - metrics: { - completed: metrics_completed, - failed: metrics_failed, - }, - db: { - version: db.redis_version, - mode: db.redis_mode, - runId: db.run_id, - processId: db.process_id, - port: parseInt(db.tcp_port), - os: db.os, - uptime: parseInt(db.uptime_in_seconds), - memory: { - total: parseInt(db.total_system_memory) || parseInt(db.maxmemory), - used: parseInt(db.used_memory), - fragmentationRatio: parseInt(db.mem_fragmentation_ratio), - peak: parseInt(db.used_memory_peak), - }, - clients: { - connected: parseInt(db.connected_clients), - blocked: parseInt(db.blocked_clients), - }, - }, - }; + this.inboxQueue.once('cleaned', (jobs, status) => { + //inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); + }); + this.inboxQueue.clean(0, 0, 'delayed'); } } diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 6f9fe53937..4b01b6af7e 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -1,16 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, MiMeta } from '@/models/_.js'; +import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import type { MiRemoteUser, MiUser } from '@/models/User.js'; -import type { MiNote } from '@/models/Note.js'; +import type { RemoteUser, User } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; import { IdService } from '@/core/IdService.js'; -import type { MiNoteReaction } from '@/models/NoteReaction.js'; +import type { NoteReaction } from '@/models/entities/NoteReaction.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { NotificationService } from '@/core/NotificationService.js'; @@ -20,22 +15,18 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; +import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { RoleService } from '@/core/RoleService.js'; -import { FeaturedService } from '@/core/FeaturedService.js'; -import { trackPromise } from '@/misc/promise-tracker.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; -import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; -import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js'; -const FALLBACK = '\u2764'; +const FALLBACK = '❤'; const legacies: Record = { 'like': '👍', - 'love': '\u2764', // ハート、異体字セレクタを入れない + 'love': '❤', // ここに記述する場合は異体字セレクタを入れない 'laugh': '😆', 'hmm': '🤔', 'surprise': '😮', @@ -70,9 +61,6 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/; @Injectable() export class ReactionService { constructor( - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -86,14 +74,13 @@ export class ReactionService { private emojisRepository: EmojisRepository, private utilityService: UtilityService, + private metaService: MetaService, private customEmojiService: CustomEmojiService, private roleService: RoleService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private userBlockingService: UserBlockingService, - private reactionsBufferingService: ReactionsBufferingService, private idService: IdService, - private featuredService: FeaturedService, private globalEventService: GlobalEventService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, @@ -103,7 +90,7 @@ export class ReactionService { } @bindThis - public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) { + public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, _reaction?: string | null) { // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); @@ -117,16 +104,11 @@ export class ReactionService { throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); } - // Check if note is Renote - if (isRenote(note) && !isQuote(note)) { - throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.'); - } - let reaction = _reaction ?? FALLBACK; if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) { - reaction = '\u2764'; - } else if (_reaction != null) { + reaction = '❤️'; + } else if (_reaction) { const custom = reaction.match(isCustomEmojiRegexp); if (custom) { const reacterHost = this.utilityService.toPunyNullable(user.host); @@ -144,12 +126,7 @@ export class ReactionService { reaction = reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; // センシティブ - if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) { - reaction = FALLBACK; - } - - // for media silenced host, custom emoji reactions are not allowed - if (reacterHost != null && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, reacterHost)) { + if ((note.reactionAcceptance === 'nonSensitiveOnly') && emoji.isSensitive) { reaction = FALLBACK; } } else { @@ -160,17 +137,19 @@ export class ReactionService { reaction = FALLBACK; } } else { - reaction = this.normalize(reaction); + reaction = this.normalize(reaction ?? null); } } - const record: MiNoteReaction = { - id: this.idService.gen(), + const record: NoteReaction = { + id: this.idService.genId(), + createdAt: new Date(), noteId: note.id, userId: user.id, reaction, }; + // Create reaction try { await this.noteReactionsRepository.insert(record); } catch (e) { @@ -194,40 +173,18 @@ export class ReactionService { } // Increment reactions count - if (this.meta.enableReactionsBuffering) { - await this.reactionsBufferingService.create(note.id, user.id, reaction, note.reactionAndUserPairCache); - } else { - const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; - await this.notesRepository.createQueryBuilder().update() - .set({ - reactions: () => sql, - ...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? { - reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`, - } : {}), - }) - .where('id = :id', { id: note.id }) - .execute(); - } + const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; + await this.notesRepository.createQueryBuilder().update() + .set({ + reactions: () => sql, + ... (!user.isBot ? { score: () => '"score" + 1' } : {}), + }) + .where('id = :id', { id: note.id }) + .execute(); - // 30%の確率、セルフではない、3日以内に投稿されたノートの場合ハイライト用ランキング更新 - if ( - Math.random() < 0.3 && - note.userId !== user.id && - (Date.now() - this.idService.parse(note.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3 - ) { - if (note.channelId != null) { - if (note.replyId == null) { - this.featuredService.updateInChannelNotesRanking(note.channelId, note.id, 1); - } - } else { - if (note.visibility === 'public' && note.userHost == null && note.replyId == null) { - this.featuredService.updateGlobalNotesRanking(note.id, 1); - this.featuredService.updatePerUserNotesRanking(note.userId, note.id, 1); - } - } - } + const meta = await this.metaService.fetch(); - if (this.meta.enableChartsForRemoteUser || (user.host == null)) { + if (meta.enableChartsForRemoteUser || (user.host == null)) { this.perUserReactionsChart.update(user, note); } @@ -257,9 +214,10 @@ export class ReactionService { // リアクションされたユーザーがローカルユーザーなら通知を作成 if (note.userHost === null) { this.notificationService.createNotification(note.userId, 'reaction', { + notifierId: user.id, noteId: note.id, reaction: reaction, - }, user.id); + }); } //#region 配信 @@ -268,7 +226,7 @@ export class ReactionService { const dm = this.apDeliverManagerService.createDeliverManager(user, content); if (note.userHost !== null) { const reactee = await this.usersRepository.findOneBy({ id: note.userId }); - dm.addDirectRecipe(reactee as MiRemoteUser); + dm.addDirectRecipe(reactee as RemoteUser); } if (['public', 'home', 'followers'].includes(note.visibility)) { @@ -276,17 +234,17 @@ export class ReactionService { } else if (note.visibility === 'specified') { const visibleUsers = await Promise.all(note.visibleUserIds.map(id => this.usersRepository.findOneBy({ id }))); for (const u of visibleUsers.filter(u => u && this.userEntityService.isRemoteUser(u))) { - dm.addDirectRecipe(u as MiRemoteUser); + dm.addDirectRecipe(u as RemoteUser); } } - trackPromise(dm.execute()); + dm.execute(); } //#endregion } @bindThis - public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote) { + public async delete(user: { id: User['id']; host: User['host']; isBot: User['isBot']; }, note: Note) { // if already unreacted const exist = await this.noteReactionsRepository.findOneBy({ noteId: note.id, @@ -305,18 +263,15 @@ export class ReactionService { } // Decrement reactions count - if (this.meta.enableReactionsBuffering) { - await this.reactionsBufferingService.delete(note.id, user.id, exist.reaction); - } else { - const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; - await this.notesRepository.createQueryBuilder().update() - .set({ - reactions: () => sql, - reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`, - }) - .where('id = :id', { id: note.id }) - .execute(); - } + const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; + await this.notesRepository.createQueryBuilder().update() + .set({ + reactions: () => sql, + }) + .where('id = :id', { id: note.id }) + .execute(); + + if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1); this.globalEventService.publishNoteStream(note.id, 'unreacted', { reaction: this.decodeReaction(exist.reaction).reaction, @@ -329,54 +284,43 @@ export class ReactionService { const dm = this.apDeliverManagerService.createDeliverManager(user, content); if (note.userHost !== null) { const reactee = await this.usersRepository.findOneBy({ id: note.userId }); - dm.addDirectRecipe(reactee as MiRemoteUser); + dm.addDirectRecipe(reactee as RemoteUser); } dm.addFollowersRecipe(); - trackPromise(dm.execute()); + dm.execute(); } //#endregion } - /** - * - 文字列タイプのレガシーな形式のリアクションを現在の形式に変換する - * - ローカルのリアクションのホストを `@.` にする(`decodeReaction()`の効果) - */ @bindThis - public convertLegacyReaction(reaction: string): string { - reaction = this.decodeReaction(reaction).reaction; - if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; - return reaction; - } + public convertLegacyReactions(reactions: Record) { + const _reactions = {} as Record; - // TODO: 廃止 - /** - * - 文字列タイプのレガシーな形式のリアクションを現在の形式に変換する - * - ローカルのリアクションのホストを `@.` にする(`decodeReaction()`の効果) - * - データベース上には存在する「0個のリアクションがついている」という情報を削除する - */ - @bindThis - public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] { - return Object.entries(reactions) - .filter(([, count]) => { - // `ReactionService.prototype.delete`ではリアクション削除時に、 - // `MiNote['reactions']`のエントリの値をデクリメントしているが、 - // デクリメントしているだけなのでエントリ自体は0を値として持つ形で残り続ける。 - // そのため、この処理がなければ、「0個のリアクションがついている」ということになってしまう。 - return count > 0; - }) - .map(([reaction, count]) => { - const key = this.convertLegacyReaction(reaction); + for (const reaction of Object.keys(reactions)) { + if (reactions[reaction] <= 0) continue; - return [key, count] as const; - }) - .reduce((acc, [key, count]) => { - // unchecked indexed access - const prevCount = acc[key] as number | undefined; + if (Object.keys(legacies).includes(reaction)) { + if (_reactions[legacies[reaction]]) { + _reactions[legacies[reaction]] += reactions[reaction]; + } else { + _reactions[legacies[reaction]] = reactions[reaction]; + } + } else { + if (_reactions[reaction]) { + _reactions[reaction] += reactions[reaction]; + } else { + _reactions[reaction] = reactions[reaction]; + } + } + } - acc[key] = (prevCount ?? 0) + count; + const _reactions2 = {} as Record; - return acc; - }, {}); + for (const reaction of Object.keys(_reactions)) { + _reactions2[this.decodeReaction(reaction).reaction] = _reactions[reaction]; + } + + return _reactions2; } @bindThis @@ -420,4 +364,11 @@ export class ReactionService { host: undefined, }; } + + @bindThis + public convertLegacyReaction(reaction: string): string { + reaction = this.decodeReaction(reaction).reaction; + if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; + return reaction; + } } diff --git a/packages/backend/src/core/ReactionsBufferingService.ts b/packages/backend/src/core/ReactionsBufferingService.ts deleted file mode 100644 index b4207c5106..0000000000 --- a/packages/backend/src/core/ReactionsBufferingService.ts +++ /dev/null @@ -1,211 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import { DI } from '@/di-symbols.js'; -import type { MiNote } from '@/models/Note.js'; -import { bindThis } from '@/decorators.js'; -import type { MiUser, NotesRepository } from '@/models/_.js'; -import type { Config } from '@/config.js'; -import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; - -const REDIS_DELTA_PREFIX = 'reactionsBufferDeltas'; -const REDIS_PAIR_PREFIX = 'reactionsBufferPairs'; - -@Injectable() -export class ReactionsBufferingService implements OnApplicationShutdown { - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - - @Inject(DI.redisForReactions) - private redisForReactions: Redis.Redis, // TODO: 専用のRedisインスタンスにする - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - ) { - this.redisForSub.on('message', this.onMessage); - } - - @bindThis - private async onMessage(_: string, data: string) { - const obj = JSON.parse(data); - - if (obj.channel === 'internal') { - const { type, body } = obj.message as GlobalEvents['internal']['payload']; - switch (type) { - case 'metaUpdated': { - // リアクションバッファリングが有効→無効になったら即bake - if (body.before != null && body.before.enableReactionsBuffering && !body.after.enableReactionsBuffering) { - this.bake(); - } - break; - } - default: - break; - } - } - } - - @bindThis - public async create(noteId: MiNote['id'], userId: MiUser['id'], reaction: string, currentPairs: string[]): Promise { - const pipeline = this.redisForReactions.pipeline(); - pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, 1); - for (let i = 0; i < currentPairs.length; i++) { - pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, i, currentPairs[i]); - } - pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, Date.now(), `${userId}/${reaction}`); - pipeline.zremrangebyrank(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -(PER_NOTE_REACTION_USER_PAIR_CACHE_MAX + 1)); - await pipeline.exec(); - } - - @bindThis - public async delete(noteId: MiNote['id'], userId: MiUser['id'], reaction: string): Promise { - const pipeline = this.redisForReactions.pipeline(); - pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, -1); - pipeline.zrem(`${REDIS_PAIR_PREFIX}:${noteId}`, `${userId}/${reaction}`); - // TODO: 「消した要素一覧」も持っておかないとcreateされた時に上書きされて復活する - await pipeline.exec(); - } - - @bindThis - public async get(noteId: MiNote['id']): Promise<{ - deltas: Record; - pairs: ([MiUser['id'], string])[]; - }> { - const pipeline = this.redisForReactions.pipeline(); - pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`); - pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1); - const results = await pipeline.exec(); - - const resultDeltas = results![0][1] as Record; - const resultPairs = results![1][1] as string[]; - - const deltas = {} as Record; - for (const [name, count] of Object.entries(resultDeltas)) { - deltas[name] = parseInt(count); - } - - const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]); - - return { - deltas, - pairs, - }; - } - - @bindThis - public async getMany(noteIds: MiNote['id'][]): Promise; - pairs: ([MiUser['id'], string])[]; - }>> { - const map = new Map; - pairs: ([MiUser['id'], string])[]; - }>(); - - const pipeline = this.redisForReactions.pipeline(); - for (const noteId of noteIds) { - pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`); - pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1); - } - const results = await pipeline.exec(); - - const opsForEachNotes = 2; - for (let i = 0; i < noteIds.length; i++) { - const noteId = noteIds[i]; - const resultDeltas = results![i * opsForEachNotes][1] as Record; - const resultPairs = results![i * opsForEachNotes + 1][1] as string[]; - - const deltas = {} as Record; - for (const [name, count] of Object.entries(resultDeltas)) { - deltas[name] = parseInt(count); - } - - const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]); - - map.set(noteId, { - deltas, - pairs, - }); - } - - return map; - } - - // TODO: scanは重い可能性があるので、別途 bufferedNoteIds を直接Redis上に持っておいてもいいかもしれない - @bindThis - public async bake(): Promise { - const bufferedNoteIds = []; - let cursor = '0'; - do { - // https://github.com/redis/ioredis#transparent-key-prefixing - const result = await this.redisForReactions.scan( - cursor, - 'MATCH', - `${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:*`, - 'COUNT', - '1000'); - - cursor = result[0]; - bufferedNoteIds.push(...result[1].map(x => x.replace(`${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:`, ''))); - } while (cursor !== '0'); - - const bufferedMap = await this.getMany(bufferedNoteIds); - - // clear - const pipeline = this.redisForReactions.pipeline(); - for (const noteId of bufferedNoteIds) { - pipeline.del(`${REDIS_DELTA_PREFIX}:${noteId}`); - pipeline.del(`${REDIS_PAIR_PREFIX}:${noteId}`); - } - await pipeline.exec(); - - // TODO: SQL一個にまとめたい - for (const [noteId, buffered] of bufferedMap) { - const sql = Object.entries(buffered.deltas) - .map(([reaction, count]) => - `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + ${count})::text::jsonb)`) - .join(' || '); - - this.notesRepository.createQueryBuilder().update() - .set({ - reactions: () => sql, - reactionAndUserPairCache: buffered.pairs.map(x => x.join('/')), - }) - .where('id = :id', { id: noteId }) - .execute(); - } - } - - @bindThis - public mergeReactions(src: MiNote['reactions'], delta: Record): MiNote['reactions'] { - const reactions = { ...src }; - for (const [name, count] of Object.entries(delta)) { - if (reactions[name] != null) { - reactions[name] += count; - } else { - reactions[name] = count; - } - } - return reactions; - } - - @bindThis - public dispose(): void { - this.redisForSub.off('message', this.onMessage); - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); - } -} diff --git a/packages/backend/src/core/RegistryApiService.ts b/packages/backend/src/core/RegistryApiService.ts deleted file mode 100644 index 2c8877d8a8..0000000000 --- a/packages/backend/src/core/RegistryApiService.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { MiRegistryItem, RegistryItemsRepository } from '@/models/_.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import type { MiUser } from '@/models/User.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { bindThis } from '@/decorators.js'; - -@Injectable() -export class RegistryApiService { - constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, - - private idService: IdService, - private globalEventService: GlobalEventService, - ) { - } - - @bindThis - public async set(userId: MiUser['id'], domain: string | null, scope: string[], key: string, value: any) { - // TODO: 作成できるキーの数を制限する - - const query = this.registryItemsRepository.createQueryBuilder('item'); - if (domain) { - query.where('item.domain = :domain', { domain: domain }); - } else { - query.where('item.domain IS NULL'); - } - query.andWhere('item.userId = :userId', { userId: userId }); - query.andWhere('item.key = :key', { key: key }); - query.andWhere('item.scope = :scope', { scope: scope }); - - const existingItem = await query.getOne(); - - if (existingItem) { - await this.registryItemsRepository.update(existingItem.id, { - updatedAt: new Date(), - value: value, - }); - } else { - await this.registryItemsRepository.insert({ - id: this.idService.gen(), - updatedAt: new Date(), - userId: userId, - domain: domain, - scope: scope, - key: key, - value: value, - }); - } - - if (domain == null) { - // TODO: サードパーティアプリが傍受出来てしまうのでどうにかする - this.globalEventService.publishMainStream(userId, 'registryUpdated', { - scope: scope, - key: key, - value: value, - }); - } - } - - @bindThis - public async getItem(userId: MiUser['id'], domain: string | null, scope: string[], key: string): Promise { - const query = this.registryItemsRepository.createQueryBuilder('item') - .where(domain == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: domain }) - .andWhere('item.userId = :userId', { userId: userId }) - .andWhere('item.key = :key', { key: key }) - .andWhere('item.scope = :scope', { scope: scope }); - - const item = await query.getOne(); - - return item; - } - - @bindThis - public async getAllItemsOfScope(userId: MiUser['id'], domain: string | null, scope: string[]): Promise { - const query = this.registryItemsRepository.createQueryBuilder('item'); - query.where(domain == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: domain }); - query.andWhere('item.userId = :userId', { userId: userId }); - query.andWhere('item.scope = :scope', { scope: scope }); - - const items = await query.getMany(); - - return items; - } - - @bindThis - public async getAllKeysOfScope(userId: MiUser['id'], domain: string | null, scope: string[]): Promise { - const query = this.registryItemsRepository.createQueryBuilder('item'); - query.select('item.key'); - query.where(domain == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: domain }); - query.andWhere('item.userId = :userId', { userId: userId }); - query.andWhere('item.scope = :scope', { scope: scope }); - - const items = await query.getMany(); - - return items.map(x => x.key); - } - - @bindThis - public async getAllScopeAndDomains(userId: MiUser['id']): Promise<{ domain: string | null; scopes: string[][] }[]> { - const query = this.registryItemsRepository.createQueryBuilder('item') - .select(['item.scope', 'item.domain']) - .where('item.userId = :userId', { userId: userId }); - - const items = await query.getMany(); - - const res = [] as { domain: string | null; scopes: string[][] }[]; - - for (const item of items) { - const target = res.find(x => x.domain === item.domain); - if (target) { - if (target.scopes.some(scope => scope.join('.') === item.scope.join('.'))) continue; - target.scopes.push(item.scope); - } else { - res.push({ - domain: item.domain, - scopes: [item.scope], - }); - } - } - - return res; - } - - @bindThis - public async remove(userId: MiUser['id'], domain: string | null, scope: string[], key: string) { - const query = this.registryItemsRepository.createQueryBuilder().delete(); - if (domain) { - query.where('domain = :domain', { domain: domain }); - } else { - query.where('domain IS NULL'); - } - query.andWhere('userId = :userId', { userId: userId }); - query.andWhere('key = :key', { key: key }); - query.andWhere('scope = :scope', { scope: scope }); - - await query.execute(); - } -} diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 9120de1f9f..9d34d82be2 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -1,50 +1,64 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { MiUser } from '@/models/User.js'; -import type { RelaysRepository } from '@/models/_.js'; +import { IsNull } from 'typeorm'; +import type { LocalUser, User } from '@/models/entities/User.js'; +import type { RelaysRepository, UsersRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { MemorySingleCache } from '@/misc/cache.js'; -import type { MiRelay } from '@/models/Relay.js'; +import type { Relay } from '@/models/entities/Relay.js'; import { QueueService } from '@/core/QueueService.js'; +import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { DI } from '@/di-symbols.js'; import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; -import { SystemAccountService } from '@/core/SystemAccountService.js'; + +const ACTOR_USERNAME = 'relay.actor' as const; @Injectable() export class RelayService { - private relaysCache: MemorySingleCache; + private relaysCache: MemorySingleCache; constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.relaysRepository) private relaysRepository: RelaysRepository, private idService: IdService, private queueService: QueueService, - private systemAccountService: SystemAccountService, + private createSystemUserService: CreateSystemUserService, private apRendererService: ApRendererService, ) { - this.relaysCache = new MemorySingleCache(1000 * 60 * 10); // 10m + this.relaysCache = new MemorySingleCache(1000 * 60 * 10); } @bindThis - public async addRelay(inbox: string): Promise { - const relay = await this.relaysRepository.insertOne({ - id: this.idService.gen(), + private async getRelayActor(): Promise { + const user = await this.usersRepository.findOneBy({ + host: IsNull(), + username: ACTOR_USERNAME, + }); + + if (user) return user as LocalUser; + + const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME); + return created as LocalUser; + } + + @bindThis + public async addRelay(inbox: string): Promise { + const relay = await this.relaysRepository.insert({ + id: this.idService.genId(), inbox, status: 'requesting', - }); - - const relayActor = await this.systemAccountService.fetch('relay'); - const follow = this.apRendererService.renderFollowRelay(relay, relayActor); + }).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0])); + + const relayActor = await this.getRelayActor(); + const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); const activity = this.apRendererService.addContext(follow); this.queueService.deliver(relayActor, activity, relay.inbox, false); - + return relay; } @@ -53,32 +67,32 @@ export class RelayService { const relay = await this.relaysRepository.findOneBy({ inbox, }); - + if (relay == null) { throw new Error('relay not found'); } - - const relayActor = await this.systemAccountService.fetch('relay'); + + const relayActor = await this.getRelayActor(); const follow = this.apRendererService.renderFollowRelay(relay, relayActor); const undo = this.apRendererService.renderUndo(follow, relayActor); const activity = this.apRendererService.addContext(undo); this.queueService.deliver(relayActor, activity, relay.inbox, false); - + await this.relaysRepository.delete(relay.id); } @bindThis - public async listRelay(): Promise { + public async listRelay(): Promise { const relays = await this.relaysRepository.find(); return relays; } - + @bindThis public async relayAccepted(id: string): Promise { const result = await this.relaysRepository.update(id, { status: 'accepted', }); - + return JSON.stringify(result); } @@ -87,24 +101,24 @@ export class RelayService { const result = await this.relaysRepository.update(id, { status: 'rejected', }); - + return JSON.stringify(result); } @bindThis - public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any): Promise { + public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise { if (activity == null) return; - + const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({ status: 'accepted', })); if (relays.length === 0) return; - + const copy = deepClone(activity); if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; - + const signed = await this.apRendererService.attachLdSignature(copy, user); - + for (const relay of relays) { this.queueService.deliver(user, signed, relay.inbox, false); } diff --git a/packages/backend/src/core/RemoteLoggerService.ts b/packages/backend/src/core/RemoteLoggerService.ts index 413b03bb56..3d45605836 100644 --- a/packages/backend/src/core/RemoteLoggerService.ts +++ b/packages/backend/src/core/RemoteLoggerService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index a2f1b73cdb..ff68c24219 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -1,21 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import chalk from 'chalk'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository } from '@/models/_.js'; -import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; +import type { UsersRepository } from '@/models/index.js'; +import type { LocalUser, RemoteUser } from '@/models/entities/User.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { ILink, WebfingerService } from '@/core/WebfingerService.js'; +import { WebfingerService } from '@/core/WebfingerService.js'; import { RemoteLoggerService } from '@/core/RemoteLoggerService.js'; -import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { bindThis } from '@/decorators.js'; @@ -33,16 +27,15 @@ export class RemoteUserResolveService { private utilityService: UtilityService, private webfingerService: WebfingerService, private remoteLoggerService: RemoteLoggerService, - private apDbResolverService: ApDbResolverService, private apPersonService: ApPersonService, ) { this.logger = this.remoteLoggerService.logger.createSubLogger('resolve-user'); } @bindThis - public async resolveUser(username: string, host: string | null): Promise { + public async resolveUser(username: string, host: string | null): Promise { const usernameLower = username.toLowerCase(); - + if (host == null) { this.logger.info(`return local user: ${usernameLower}`); return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { @@ -51,12 +44,12 @@ export class RemoteUserResolveService { } else { return u; } - }) as MiLocalUser; + }) as LocalUser; } - + host = this.utilityService.toPuny(host); - - if (host === this.utilityService.toPuny(this.config.host)) { + + if (this.config.host === host) { this.logger.info(`return local user: ${usernameLower}`); return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { if (u == null) { @@ -64,57 +57,41 @@ export class RemoteUserResolveService { } else { return u; } - }) as MiLocalUser; + }) as LocalUser; } - - const user = await this.usersRepository.findOneBy({ usernameLower, host }) as MiRemoteUser | null; - + + const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null; + const acctLower = `${usernameLower}@${host}`; - + if (user == null) { const self = await this.resolveSelf(acctLower); - - if (this.utilityService.isUriLocal(self.href)) { - const local = this.apDbResolverService.parseUri(self.href); - if (local.local && local.type === 'users') { - // the LR points to local - return (await this.apDbResolverService - .getUserFromApId(self.href) - .then((u) => { - if (u == null) { - throw new Error('local user not found'); - } else { - return u; - } - })) as MiLocalUser; - } - } - + this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); return await this.apPersonService.createPerson(self.href); } - - // ユーザー情報が古い場合は、WebFingerからやりなおして返す + + // ユーザー情報が古い場合は、WebFilgerからやりなおして返す if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する await this.usersRepository.update(user.id, { lastFetchedAt: new Date(), }); - + this.logger.info(`try resync: ${acctLower}`); const self = await this.resolveSelf(acctLower); - + if (user.uri !== self.href) { // if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping. this.logger.info(`uri missmatch: ${acctLower}`); this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`); - + // validate uri const uri = new URL(self.href); if (uri.hostname !== host) { throw new Error('Invalid uri'); } - + await this.usersRepository.update({ usernameLower, host: host, @@ -124,25 +101,25 @@ export class RemoteUserResolveService { } else { this.logger.info(`uri is fine: ${acctLower}`); } - + await this.apPersonService.updatePerson(self.href); - + this.logger.info(`return resynced remote user: ${acctLower}`); return await this.usersRepository.findOneBy({ uri: self.href }).then(u => { if (u == null) { throw new Error('user not found'); } else { - return u as MiLocalUser | MiRemoteUser; + return u as LocalUser | RemoteUser; } }); } - + this.logger.info(`return existing remote user: ${acctLower}`); return user; } @bindThis - private async resolveSelf(acctLower: string): Promise { + private async resolveSelf(acctLower: string) { this.logger.info(`WebFinger for ${chalk.yellow(acctLower)}`); const finger = await this.webfingerService.webfinger(acctLower).catch(err => { this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ err.statusCode ?? err.message }`); diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts deleted file mode 100644 index 51dca3da59..0000000000 --- a/packages/backend/src/core/ReversiService.ts +++ /dev/null @@ -1,633 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import { ModuleRef } from '@nestjs/core'; -import { reversiUpdateKeys } from 'misskey-js'; -import * as Reversi from 'misskey-reversi'; -import { IsNull, LessThan, MoreThan } from 'typeorm'; -import type { - MiReversiGame, - ReversiGamesRepository, -} from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { CacheService } from '@/core/CacheService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { IdService } from '@/core/IdService.js'; -import { NotificationService } from '@/core/NotificationService.js'; -import { Serialized } from '@/types.js'; -import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; -import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; - -const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec - -@Injectable() -export class ReversiService implements OnApplicationShutdown, OnModuleInit { - private notificationService: NotificationService; - - constructor( - private moduleRef: ModuleRef, - - @Inject(DI.redis) - private redisClient: Redis.Redis, - - @Inject(DI.reversiGamesRepository) - private reversiGamesRepository: ReversiGamesRepository, - - private cacheService: CacheService, - private userEntityService: UserEntityService, - private globalEventService: GlobalEventService, - private reversiGameEntityService: ReversiGameEntityService, - private idService: IdService, - ) { - } - - async onModuleInit() { - this.notificationService = this.moduleRef.get(NotificationService.name); - } - - @bindThis - private async cacheGame(game: MiReversiGame) { - await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 60, JSON.stringify(game)); - } - - @bindThis - private async deleteGameCache(gameId: MiReversiGame['id']) { - await this.redisClient.del(`reversi:game:cache:${gameId}`); - } - - @bindThis - private getBakeProps(game: MiReversiGame) { - return { - startedAt: game.startedAt, - endedAt: game.endedAt, - // ゲームの途中からユーザーが変わることは無いので - //user1Id: game.user1Id, - //user2Id: game.user2Id, - user1Ready: game.user1Ready, - user2Ready: game.user2Ready, - black: game.black, - isStarted: game.isStarted, - isEnded: game.isEnded, - winnerId: game.winnerId, - surrenderedUserId: game.surrenderedUserId, - timeoutUserId: game.timeoutUserId, - isLlotheo: game.isLlotheo, - canPutEverywhere: game.canPutEverywhere, - loopedBoard: game.loopedBoard, - timeLimitForEachTurn: game.timeLimitForEachTurn, - logs: game.logs, - map: game.map, - bw: game.bw, - crc32: game.crc32, - noIrregularRules: game.noIrregularRules, - } satisfies Partial; - } - - @bindThis - public async matchSpecificUser(me: MiUser, targetUser: MiUser, multiple = false): Promise { - if (targetUser.id === me.id) { - throw new Error('You cannot match yourself.'); - } - - if (!multiple) { - // 既にマッチしている対局が無いか探す(3分以内) - const games = await this.reversiGamesRepository.find({ - where: [ - { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, user2Id: targetUser.id, isStarted: false }, - { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: targetUser.id, user2Id: me.id, isStarted: false }, - ], - relations: ['user1', 'user2'], - order: { id: 'DESC' }, - }); - if (games.length > 0) { - return games[0]; - } - } - - //#region 相手から既に招待されてないか確認 - const invitations = await this.redisClient.zrange( - `reversi:matchSpecific:${me.id}`, - Date.now() - INVITATION_TIMEOUT_MS, - '+inf', - 'BYSCORE'); - - if (invitations.includes(targetUser.id)) { - await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id); - - const game = await this.matched(targetUser.id, me.id, { - noIrregularRules: false, - }); - - return game; - } - //#endregion - - const redisPipeline = this.redisClient.pipeline(); - redisPipeline.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id); - redisPipeline.expire(`reversi:matchSpecific:${targetUser.id}`, 120, 'NX'); - await redisPipeline.exec(); - - this.globalEventService.publishReversiStream(targetUser.id, 'invited', { - user: await this.userEntityService.pack(me, targetUser), - }); - - return null; - } - - @bindThis - public async matchAnyUser(me: MiUser, options: { noIrregularRules: boolean }, multiple = false): Promise { - if (!multiple) { - // 既にマッチしている対局が無いか探す(3分以内) - const games = await this.reversiGamesRepository.find({ - where: [ - { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, isStarted: false }, - { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user2Id: me.id, isStarted: false }, - ], - relations: ['user1', 'user2'], - order: { id: 'DESC' }, - }); - if (games.length > 0) { - return games[0]; - } - } - - //#region まず自分宛ての招待を探す - const invitations = await this.redisClient.zrange( - `reversi:matchSpecific:${me.id}`, - Date.now() - INVITATION_TIMEOUT_MS, - '+inf', - 'BYSCORE'); - - if (invitations.length > 0) { - const invitorId = invitations[Math.floor(Math.random() * invitations.length)]; - await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, invitorId); - - const game = await this.matched(invitorId, me.id, { - noIrregularRules: false, - }); - - return game; - } - //#endregion - - const matchings = await this.redisClient.zrange( - 'reversi:matchAny', - 0, - 2, // 自分自身のIDが入っている場合もあるので2つ取得 - 'REV'); - - const items = matchings.filter(id => !id.startsWith(me.id)); - - if (items.length > 0) { - const [matchedUserId, option] = items[0].split(':'); - - await this.redisClient.zrem('reversi:matchAny', - me.id, - matchedUserId, - me.id + ':noIrregularRules', - matchedUserId + ':noIrregularRules'); - - const game = await this.matched(matchedUserId, me.id, { - noIrregularRules: options.noIrregularRules || option === 'noIrregularRules', - }); - - return game; - } else { - const redisPipeline = this.redisClient.pipeline(); - if (options.noIrregularRules) { - redisPipeline.zadd('reversi:matchAny', Date.now(), me.id + ':noIrregularRules'); - } else { - redisPipeline.zadd('reversi:matchAny', Date.now(), me.id); - } - redisPipeline.expire('reversi:matchAny', 15, 'NX'); - await redisPipeline.exec(); - return null; - } - } - - @bindThis - public async matchSpecificUserCancel(user: MiUser, targetUserId: MiUser['id']) { - await this.redisClient.zrem(`reversi:matchSpecific:${targetUserId}`, user.id); - } - - @bindThis - public async matchAnyUserCancel(user: MiUser) { - await this.redisClient.zrem('reversi:matchAny', user.id, user.id + ':noIrregularRules'); - } - - @bindThis - public async cleanOutdatedGames() { - await this.reversiGamesRepository.delete({ - id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 10)), - isStarted: false, - }); - } - - @bindThis - public async gameReady(gameId: MiReversiGame['id'], user: MiUser, ready: boolean) { - const game = await this.get(gameId); - if (game == null) throw new Error('game not found'); - if (game.isStarted) return; - - let isBothReady = false; - - if (game.user1Id === user.id) { - const updatedGame = { - ...game, - user1Ready: ready, - }; - this.cacheGame(updatedGame); - - this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', { - user1: ready, - user2: updatedGame.user2Ready, - }); - - if (ready && updatedGame.user2Ready) isBothReady = true; - } else if (game.user2Id === user.id) { - const updatedGame = { - ...game, - user2Ready: ready, - }; - this.cacheGame(updatedGame); - - this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', { - user1: updatedGame.user1Ready, - user2: ready, - }); - - if (ready && updatedGame.user1Ready) isBothReady = true; - } else { - return; - } - - if (isBothReady) { - // 3秒後、両者readyならゲーム開始 - setTimeout(async () => { - const freshGame = await this.get(game.id); - if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; - if (!freshGame.user1Ready || !freshGame.user2Ready) return; - - this.startGame(freshGame); - }, 3000); - } - } - - @bindThis - private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise { - const game = await this.reversiGamesRepository.insertOne({ - id: this.idService.gen(), - user1Id: parentId, - user2Id: childId, - user1Ready: false, - user2Ready: false, - isStarted: false, - isEnded: false, - logs: [], - map: Reversi.maps.eighteight.data, - bw: 'random', - isLlotheo: false, - noIrregularRules: options.noIrregularRules, - }, { relations: ['user1', 'user2'] }); - this.cacheGame(game); - - const packed = await this.reversiGameEntityService.packDetail(game); - this.globalEventService.publishReversiStream(parentId, 'matched', { game: packed }); - - return game; - } - - @bindThis - private async startGame(game: MiReversiGame) { - let bw: number; - if (game.bw === 'random') { - bw = Math.random() > 0.5 ? 1 : 2; - } else { - bw = parseInt(game.bw, 10); - } - - const engine = new Reversi.Game(game.map, { - isLlotheo: game.isLlotheo, - canPutEverywhere: game.canPutEverywhere, - loopedBoard: game.loopedBoard, - }); - - const crc32 = engine.calcCrc32().toString(); - - const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() - .set({ - ...this.getBakeProps(game), - startedAt: new Date(), - isStarted: true, - black: bw, - map: game.map, - crc32, - }) - .where('id = :id', { id: game.id }) - .returning('*') - .execute() - .then((response) => response.raw[0]); - // キャッシュ効率化のためにユーザー情報は再利用 - updatedGame.user1 = game.user1; - updatedGame.user2 = game.user2; - this.cacheGame(updatedGame); - - //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 - if (engine.isEnded) { - let winnerId; - if (engine.winner === true) { - winnerId = bw === 1 ? updatedGame.user1Id : updatedGame.user2Id; - } else if (engine.winner === false) { - winnerId = bw === 1 ? updatedGame.user2Id : updatedGame.user1Id; - } else { - winnerId = null; - } - - await this.endGame(updatedGame, winnerId, null); - - return; - } - //#endregion - - this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, ''); - - this.globalEventService.publishReversiGameStream(game.id, 'started', { - game: await this.reversiGameEntityService.packDetail(updatedGame), - }); - } - - @bindThis - private async endGame(game: MiReversiGame, winnerId: MiUser['id'] | null, reason: 'surrender' | 'timeout' | null) { - const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() - .set({ - ...this.getBakeProps(game), - isEnded: true, - endedAt: new Date(), - winnerId: winnerId, - surrenderedUserId: reason === 'surrender' ? (winnerId === game.user1Id ? game.user2Id : game.user1Id) : null, - timeoutUserId: reason === 'timeout' ? (winnerId === game.user1Id ? game.user2Id : game.user1Id) : null, - }) - .where('id = :id', { id: game.id }) - .returning('*') - .execute() - .then((response) => response.raw[0]); - // キャッシュ効率化のためにユーザー情報は再利用 - updatedGame.user1 = game.user1; - updatedGame.user2 = game.user2; - this.cacheGame(updatedGame); - - this.globalEventService.publishReversiGameStream(game.id, 'ended', { - winnerId: winnerId, - game: await this.reversiGameEntityService.packDetail(updatedGame), - }); - } - - @bindThis - public async getInvitations(user: MiUser): Promise { - const invitations = await this.redisClient.zrange( - `reversi:matchSpecific:${user.id}`, - Date.now() - INVITATION_TIMEOUT_MS, - '+inf', - 'BYSCORE'); - return invitations; - } - - @bindThis - public isValidReversiUpdateKey(key: unknown): key is typeof reversiUpdateKeys[number] { - if (typeof key !== 'string') return false; - return (reversiUpdateKeys as string[]).includes(key); - } - - @bindThis - public isValidReversiUpdateValue(key: K, value: unknown): value is MiReversiGame[K] { - switch (key) { - case 'map': - return Array.isArray(value) && value.every(row => typeof row === 'string'); - case 'bw': - return typeof value === 'string' && ['random', '1', '2'].includes(value); - case 'isLlotheo': - return typeof value === 'boolean'; - case 'canPutEverywhere': - return typeof value === 'boolean'; - case 'loopedBoard': - return typeof value === 'boolean'; - case 'timeLimitForEachTurn': - return typeof value === 'number' && value >= 0; - default: - return false; - } - } - - @bindThis - public async updateSettings(gameId: MiReversiGame['id'], user: MiUser, key: K, value: MiReversiGame[K]) { - const game = await this.get(gameId); - if (game == null) throw new Error('game not found'); - if (game.isStarted) return; - if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return; - if ((game.user1Id === user.id) && game.user1Ready) return; - if ((game.user2Id === user.id) && game.user2Ready) return; - - const updatedGame = { - ...game, - [key]: value, - }; - this.cacheGame(updatedGame); - - this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', { - userId: user.id, - key: key, - value: value, - }); - } - - @bindThis - public async putStoneToGame(gameId: MiReversiGame['id'], user: MiUser, pos: number, id?: string | null) { - const game = await this.get(gameId); - if (game == null) throw new Error('game not found'); - if (!game.isStarted) return; - if (game.isEnded) return; - if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return; - - const myColor = - ((game.user1Id === user.id) && game.black === 1) || ((game.user2Id === user.id) && game.black === 2) - ? true - : false; - - const engine = Reversi.Serializer.restoreGame({ - map: game.map, - isLlotheo: game.isLlotheo, - canPutEverywhere: game.canPutEverywhere, - loopedBoard: game.loopedBoard, - logs: game.logs, - }); - - if (engine.turn !== myColor) return; - if (!engine.canPut(myColor, pos)) return; - - engine.putStone(pos); - - const logs = Reversi.Serializer.deserializeLogs(game.logs); - - const log = { - time: Date.now(), - player: myColor, - operation: 'put', - pos, - } as const; - - logs.push(log); - - const serializeLogs = Reversi.Serializer.serializeLogs(logs); - - const crc32 = engine.calcCrc32().toString(); - - const updatedGame = { - ...game, - crc32, - logs: serializeLogs, - }; - this.cacheGame(updatedGame); - - this.globalEventService.publishReversiGameStream(game.id, 'log', { - ...log, - id: id ?? null, - }); - - if (engine.isEnded) { - let winnerId; - if (engine.winner === true) { - winnerId = game.black === 1 ? game.user1Id : game.user2Id; - } else if (engine.winner === false) { - winnerId = game.black === 1 ? game.user2Id : game.user1Id; - } else { - winnerId = null; - } - - await this.endGame(updatedGame, winnerId, null); - } else { - this.redisClient.setex(`reversi:game:turnTimer:${game.id}:${engine.turn ? '1' : '0'}`, updatedGame.timeLimitForEachTurn, ''); - } - } - - @bindThis - public async surrender(gameId: MiReversiGame['id'], user: MiUser) { - const game = await this.get(gameId); - if (game == null) throw new Error('game not found'); - if (game.isEnded) return; - if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return; - - const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id; - - await this.endGame(game, winnerId, 'surrender'); - } - - @bindThis - public async checkTimeout(gameId: MiReversiGame['id']) { - const game = await this.get(gameId); - if (game == null) throw new Error('game not found'); - if (game.isEnded) return; - - const engine = Reversi.Serializer.restoreGame({ - map: game.map, - isLlotheo: game.isLlotheo, - canPutEverywhere: game.canPutEverywhere, - loopedBoard: game.loopedBoard, - logs: game.logs, - }); - - if (engine.turn == null) return; - - const timer = await this.redisClient.exists(`reversi:game:turnTimer:${game.id}:${engine.turn ? '1' : '0'}`); - - if (timer === 0) { - const winnerId = engine.turn ? (game.black === 1 ? game.user2Id : game.user1Id) : (game.black === 1 ? game.user1Id : game.user2Id); - - await this.endGame(game, winnerId, 'timeout'); - } - } - - @bindThis - public async cancelGame(gameId: MiReversiGame['id'], user: MiUser) { - const game = await this.get(gameId); - if (game == null) throw new Error('game not found'); - if (game.isStarted) return; - if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return; - - await this.reversiGamesRepository.delete(game.id); - this.deleteGameCache(game.id); - - this.globalEventService.publishReversiGameStream(game.id, 'canceled', { - userId: user.id, - }); - } - - @bindThis - public async get(id: MiReversiGame['id']): Promise { - const cached = await this.redisClient.get(`reversi:game:cache:${id}`); - if (cached != null) { - // TODO: この辺りのデシリアライズ処理をどこか別のサービスに切り出したい - const parsed = JSON.parse(cached) as Serialized; - return { - ...parsed, - startedAt: parsed.startedAt != null ? new Date(parsed.startedAt) : null, - endedAt: parsed.endedAt != null ? new Date(parsed.endedAt) : null, - user1: parsed.user1 != null ? { - ...parsed.user1, - avatar: null, - banner: null, - updatedAt: parsed.user1.updatedAt != null ? new Date(parsed.user1.updatedAt) : null, - lastActiveDate: parsed.user1.lastActiveDate != null ? new Date(parsed.user1.lastActiveDate) : null, - lastFetchedAt: parsed.user1.lastFetchedAt != null ? new Date(parsed.user1.lastFetchedAt) : null, - movedAt: parsed.user1.movedAt != null ? new Date(parsed.user1.movedAt) : null, - } : null, - user2: parsed.user2 != null ? { - ...parsed.user2, - avatar: null, - banner: null, - updatedAt: parsed.user2.updatedAt != null ? new Date(parsed.user2.updatedAt) : null, - lastActiveDate: parsed.user2.lastActiveDate != null ? new Date(parsed.user2.lastActiveDate) : null, - lastFetchedAt: parsed.user2.lastFetchedAt != null ? new Date(parsed.user2.lastFetchedAt) : null, - movedAt: parsed.user2.movedAt != null ? new Date(parsed.user2.movedAt) : null, - } : null, - }; - } else { - const game = await this.reversiGamesRepository.findOne({ - where: { id }, - relations: ['user1', 'user2'], - }); - if (game == null) return null; - - this.cacheGame(game); - - return game; - } - } - - @bindThis - public async checkCrc(gameId: MiReversiGame['id'], crc32: string | number) { - const game = await this.get(gameId); - if (game == null) throw new Error('game not found'); - - if (crc32.toString() !== game.crc32) { - return game; - } else { - return null; - } - } - - @bindThis - public dispose(): void { - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); - } -} diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 76dafeb255..23ecf0157d 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -1,54 +1,31 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { In } from 'typeorm'; -import { ModuleRef } from '@nestjs/core'; -import type { - MiMeta, - MiRole, - MiRoleAssignment, - RoleAssignmentsRepository, - RolesRepository, - UsersRepository, -} from '@/models/_.js'; +import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js'; -import type { MiUser } from '@/models/User.js'; +import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; -import type { RoleCondFormulaValue } from '@/models/Role.js'; +import type { RoleCondFormulaValue } from '@/models/entities/Role.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; import { IdService } from '@/core/IdService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { Packed } from '@/misc/json-schema.js'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; -import { NotificationService } from '@/core/NotificationService.js'; -import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; +import type { OnApplicationShutdown } from '@nestjs/common'; export type RolePolicies = { gtlAvailable: boolean; ltlAvailable: boolean; canPublicNote: boolean; - mentionLimit: number; canInvite: boolean; - inviteLimit: number; - inviteLimitCycle: number; - inviteExpirationTime: number; canManageCustomEmojis: boolean; - canManageAvatarDecorations: boolean; canSearchNotes: boolean; - canUseTranslator: boolean; canHideAds: boolean; driveCapacityMb: number; - maxFileSizeMb: number; alwaysMarkNsfw: boolean; - canUpdateBioMedia: boolean; pinLimit: number; antennaLimit: number; wordMuteLimit: number; @@ -58,34 +35,18 @@ export type RolePolicies = { userListLimit: number; userEachUserListsLimit: number; rateLimitFactor: number; - avatarDecorationLimit: number; - canImportAntennas: boolean; - canImportBlocking: boolean; - canImportFollowing: boolean; - canImportMuting: boolean; - canImportUserLists: boolean; - chatAvailability: 'available' | 'readonly' | 'unavailable'; - uploadableFileTypes: string[]; }; export const DEFAULT_POLICIES: RolePolicies = { gtlAvailable: true, ltlAvailable: true, canPublicNote: true, - mentionLimit: 20, canInvite: false, - inviteLimit: 0, - inviteLimitCycle: 60 * 24 * 7, - inviteExpirationTime: 0, canManageCustomEmojis: false, - canManageAvatarDecorations: false, canSearchNotes: false, - canUseTranslator: true, canHideAds: false, driveCapacityMb: 100, - maxFileSizeMb: 30, alwaysMarkNsfw: false, - canUpdateBioMedia: true, pinLimit: 5, antennaLimit: 5, wordMuteLimit: 200, @@ -95,39 +56,19 @@ export const DEFAULT_POLICIES: RolePolicies = { userListLimit: 10, userEachUserListsLimit: 50, rateLimitFactor: 1, - avatarDecorationLimit: 1, - canImportAntennas: true, - canImportBlocking: true, - canImportFollowing: true, - canImportMuting: true, - canImportUserLists: true, - chatAvailability: 'available', - uploadableFileTypes: [ - 'text/plain', - 'application/json', - 'image/*', - 'video/*', - 'audio/*', - ], }; @Injectable() -export class RoleService implements OnApplicationShutdown, OnModuleInit { - private rolesCache: MemorySingleCache; - private roleAssignmentByUserIdCache: MemoryKVCache; - private notificationService: NotificationService; +export class RoleService implements OnApplicationShutdown { + private rolesCache: MemorySingleCache; + private roleAssignmentByUserIdCache: MemoryKVCache; public static AlreadyAssignedError = class extends Error {}; public static NotAssignedError = class extends Error {}; constructor( - private moduleRef: ModuleRef, - - @Inject(DI.meta) - private meta: MiMeta, - - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, + @Inject(DI.redis) + private redisClient: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis, @@ -141,35 +82,33 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { @Inject(DI.roleAssignmentsRepository) private roleAssignmentsRepository: RoleAssignmentsRepository, + private metaService: MetaService, private cacheService: CacheService, private userEntityService: UserEntityService, private globalEventService: GlobalEventService, private idService: IdService, - private moderationLogService: ModerationLogService, - private fanoutTimelineService: FanoutTimelineService, ) { - this.rolesCache = new MemorySingleCache(1000 * 60 * 60); // 1h - this.roleAssignmentByUserIdCache = new MemoryKVCache(1000 * 60 * 5); // 5m + //this.onMessage = this.onMessage.bind(this); + + this.rolesCache = new MemorySingleCache(1000 * 60 * 60 * 1); + this.roleAssignmentByUserIdCache = new MemoryKVCache(1000 * 60 * 60 * 1); this.redisForSub.on('message', this.onMessage); } - async onModuleInit() { - this.notificationService = this.moduleRef.get(NotificationService.name); - } - @bindThis private async onMessage(_: string, data: string): Promise { const obj = JSON.parse(data); if (obj.channel === 'internal') { - const { type, body } = obj.message as GlobalEvents['internal']['payload']; + const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'roleCreated': { const cached = this.rolesCache.get(); if (cached) { cached.push({ ...body, + createdAt: new Date(body.createdAt), updatedAt: new Date(body.updatedAt), lastUsedAt: new Date(body.lastUsedAt), }); @@ -183,6 +122,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { if (i > -1) { cached[i] = { ...body, + createdAt: new Date(body.createdAt), updatedAt: new Date(body.updatedAt), lastUsedAt: new Date(body.lastUsedAt), }; @@ -200,11 +140,10 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { case 'userRoleAssigned': { const cached = this.roleAssignmentByUserIdCache.get(body.userId); if (cached) { - cached.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい + cached.push({ ...body, + createdAt: new Date(body.createdAt), expiresAt: body.expiresAt ? new Date(body.expiresAt) : null, - user: null, // joinなカラムは通常取ってこないので - role: null, // joinなカラムは通常取ってこないので }); } break; @@ -223,82 +162,45 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } @bindThis - private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue): boolean { + private evalCond(user: User, value: RoleCondFormulaValue): boolean { try { switch (value.type) { - // ~かつ~ case 'and': { - return value.values.every(v => this.evalCond(user, roles, v)); + return value.values.every(v => this.evalCond(user, v)); } - // ~または~ case 'or': { - return value.values.some(v => this.evalCond(user, roles, v)); + return value.values.some(v => this.evalCond(user, v)); } - // ~ではない case 'not': { - return !this.evalCond(user, roles, value.value); + return !this.evalCond(user, value.value); } - // マニュアルロールがアサインされている - case 'roleAssignedTo': { - return roles.some(r => r.id === value.roleId); - } - // ローカルユーザのみ case 'isLocal': { return this.userEntityService.isLocalUser(user); } - // リモートユーザのみ case 'isRemote': { return this.userEntityService.isRemoteUser(user); } - // サスペンド済みユーザである - case 'isSuspended': { - return user.isSuspended; - } - // 鍵アカウントユーザである - case 'isLocked': { - return user.isLocked; - } - // botユーザである - case 'isBot': { - return user.isBot; - } - // 猫である - case 'isCat': { - return user.isCat; - } - // 「ユーザを見つけやすくする」が有効なアカウント - case 'isExplorable': { - return user.isExplorable; - } - // ユーザが作成されてから指定期間経過した case 'createdLessThan': { - return this.idService.parse(user.id).date.getTime() > (Date.now() - (value.sec * 1000)); + return user.createdAt.getTime() > (Date.now() - (value.sec * 1000)); } - // ユーザが作成されてから指定期間経っていない case 'createdMoreThan': { - return this.idService.parse(user.id).date.getTime() < (Date.now() - (value.sec * 1000)); + return user.createdAt.getTime() < (Date.now() - (value.sec * 1000)); } - // フォロワー数が指定値以下 case 'followersLessThanOrEq': { return user.followersCount <= value.value; } - // フォロワー数が指定値以上 case 'followersMoreThanOrEq': { return user.followersCount >= value.value; } - // フォロー数が指定値以下 case 'followingLessThanOrEq': { return user.followingCount <= value.value; } - // フォロー数が指定値以上 case 'followingMoreThanOrEq': { return user.followingCount >= value.value; } - // ノート数が指定値以下 case 'notesLessThanOrEq': { return user.notesCount <= value.value; } - // ノート数が指定値以上 case 'notesMoreThanOrEq': { return user.notesCount >= value.value; } @@ -312,27 +214,16 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async getRoles() { - const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); - return roles; - } - - @bindThis - public async getUserAssigns(userId: MiUser['id']) { + public async getUserRoles(userId: User['id']) { const now = Date.now(); let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); // 期限切れのロールを除外 assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); - return assigns; - } - - @bindThis - public async getUserRoles(userId: MiUser['id']) { + const assignedRoleIds = assigns.map(x => x.roleId); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); - const assigns = await this.getUserAssigns(userId); - const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id)); + const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id)); const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; - const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, assignedRoles, r.condFormula)); + const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula)); return [...assignedRoles, ...matchedCondRoles]; } @@ -340,18 +231,18 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { * 指定ユーザーのバッジロール一覧取得 */ @bindThis - public async getUserBadgeRoles(userId: MiUser['id']) { + public async getUserBadgeRoles(userId: User['id']) { const now = Date.now(); let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); // 期限切れのロールを除外 assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); + const assignedRoleIds = assigns.map(x => x.roleId); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); - const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id)); - const assignedBadgeRoles = assignedRoles.filter(r => r.asBadge); + const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id)); const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional')); if (badgeCondRoles.length > 0) { const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; - const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, assignedRoles, r.condFormula)); + const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula)); return [...assignedBadgeRoles, ...matchedBadgeCondRoles]; } else { return assignedBadgeRoles; @@ -359,8 +250,9 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async getUserPolicies(userId: MiUser['id'] | null): Promise { - const basePolicies = { ...DEFAULT_POLICIES, ...this.meta.policies }; + public async getUserPolicies(userId: User['id'] | null): Promise { + const meta = await this.metaService.fetch(); + const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies }; if (userId == null) return basePolicies; @@ -380,30 +272,16 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { return aggregate(policies.map(policy => policy.useDefault ? basePolicies[name] : policy.value)); } - function aggregateChatAvailability(vs: RolePolicies['chatAvailability'][]) { - if (vs.some(v => v === 'available')) return 'available'; - if (vs.some(v => v === 'readonly')) return 'readonly'; - return 'unavailable'; - } - return { gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)), ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), - mentionLimit: calc('mentionLimit', vs => Math.max(...vs)), canInvite: calc('canInvite', vs => vs.some(v => v === true)), - inviteLimit: calc('inviteLimit', vs => Math.max(...vs)), - inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)), - inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)), canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), - canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)), canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)), - canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)), - maxFileSizeMb: calc('maxFileSizeMb', vs => Math.max(...vs)), alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)), - canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)), pinLimit: calc('pinLimit', vs => Math.max(...vs)), antennaLimit: calc('antennaLimit', vs => Math.max(...vs)), wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)), @@ -413,107 +291,51 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { userListLimit: calc('userListLimit', vs => Math.max(...vs)), userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)), rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)), - avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)), - canImportAntennas: calc('canImportAntennas', vs => vs.some(v => v === true)), - canImportBlocking: calc('canImportBlocking', vs => vs.some(v => v === true)), - canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)), - canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)), - canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)), - chatAvailability: calc('chatAvailability', aggregateChatAvailability), - uploadableFileTypes: calc('uploadableFileTypes', vs => { - const set = new Set(); - for (const v of vs) { - for (const type of v) { - if (type.trim() === '') continue; - set.add(type.trim()); - } - } - return [...set]; - }), }; } @bindThis - public async isModerator(user: { id: MiUser['id'] } | null): Promise { + public async isModerator(user: { id: User['id']; isRoot: User['isRoot'] } | null): Promise { if (user == null) return false; - return (this.meta.rootUserId === user.id) || (await this.getUserRoles(user.id)).some(r => r.isModerator || r.isAdministrator); + return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isModerator || r.isAdministrator); } @bindThis - public async isAdministrator(user: { id: MiUser['id'] } | null): Promise { + public async isAdministrator(user: { id: User['id']; isRoot: User['isRoot'] } | null): Promise { if (user == null) return false; - return (this.meta.rootUserId === user.id) || (await this.getUserRoles(user.id)).some(r => r.isAdministrator); + return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator); } @bindThis - public async isExplorable(role: { id: MiRole['id'] } | null): Promise { + public async isExplorable(role: { id: Role['id']} | null): Promise { if (role == null) return false; const check = await this.rolesRepository.findOneBy({ id: role.id }); if (check == null) return false; return check.isExplorable; } - /** - * モデレーター権限のロールが割り当てられているユーザID一覧を取得する. - * - * @param opts.includeAdmins 管理者権限も含めるか(デフォルト: true) - * @param opts.includeRoot rootユーザも含めるか(デフォルト: false) - * @param opts.excludeExpire 期限切れのロールを除外するか(デフォルト: false) - */ @bindThis - public async getModeratorIds(opts?: { - includeAdmins?: boolean, - includeRoot?: boolean, - excludeExpire?: boolean, - }): Promise { - const includeAdmins = opts?.includeAdmins ?? true; - const includeRoot = opts?.includeRoot ?? false; - const excludeExpire = opts?.excludeExpire ?? false; - + public async getModeratorIds(includeAdmins = true): Promise { const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); - const moderatorRoles = includeAdmins - ? roles.filter(r => r.isModerator || r.isAdministrator) - : roles.filter(r => r.isModerator); - - const assigns = moderatorRoles.length > 0 - ? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) }) - : []; - - // Setを経由して重複を除去(ユーザIDは重複する可能性があるので) - const now = Date.now(); - const resultSet = new Set( - assigns - .filter(it => - (excludeExpire) - ? (it.expiresAt == null || it.expiresAt.getTime() > now) - : true, - ) - .map(a => a.userId), - ); - - if (includeRoot && this.meta.rootUserId) { - resultSet.add(this.meta.rootUserId); - } - - return [...resultSet].sort((x, y) => x.localeCompare(y)); + const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator); + const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ + roleId: In(moderatorRoles.map(r => r.id)), + }) : []; + // TODO: isRootなアカウントも含める + return assigns.map(a => a.userId); } @bindThis - public async getModerators(opts?: { - includeAdmins?: boolean, - includeRoot?: boolean, - excludeExpire?: boolean, - }): Promise { - const ids = await this.getModeratorIds(opts); - return ids.length > 0 - ? await this.usersRepository.findBy({ - id: In(ids), - }) - : []; + public async getModerators(includeAdmins = true): Promise { + const ids = await this.getModeratorIds(includeAdmins); + const users = ids.length > 0 ? await this.usersRepository.findBy({ + id: In(ids), + }) : []; + return users; } @bindThis - public async getAdministratorIds(): Promise { + public async getAdministratorIds(): Promise { const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); const administratorRoles = roles.filter(r => r.isAdministrator); const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ @@ -524,7 +346,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async getAdministrators(): Promise { + public async getAdministrators(): Promise { const ids = await this.getAdministratorIds(); const users = ids.length > 0 ? await this.usersRepository.findBy({ id: In(ids), @@ -533,10 +355,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async assign(userId: MiUser['id'], roleId: MiRole['id'], expiresAt: Date | null = null, moderator?: MiUser): Promise { - const now = Date.now(); - - const role = await this.rolesRepository.findOneByOrFail({ id: roleId }); + public async assign(userId: User['id'], roleId: Role['id'], expiresAt: Date | null = null): Promise { + const now = new Date(); const existing = await this.roleAssignmentsRepository.findOneBy({ roleId: roleId, @@ -544,7 +364,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { }); if (existing) { - if (existing.expiresAt && (existing.expiresAt.getTime() < now)) { + if (existing.expiresAt && (existing.expiresAt.getTime() < now.getTime())) { await this.roleAssignmentsRepository.delete({ roleId: roleId, userId: userId, @@ -554,43 +374,25 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } } - const created = await this.roleAssignmentsRepository.insertOne({ - id: this.idService.gen(now), + const created = await this.roleAssignmentsRepository.insert({ + id: this.idService.genId(), + createdAt: now, expiresAt: expiresAt, roleId: roleId, userId: userId, - }); + }).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0])); this.rolesRepository.update(roleId, { lastUsedAt: new Date(), }); this.globalEventService.publishInternalEvent('userRoleAssigned', created); - - const user = await this.usersRepository.findOneByOrFail({ id: userId }); - - if (role.isPublic && user.host === null) { - this.notificationService.createNotification(userId, 'roleAssigned', { - roleId: roleId, - }); - } - - if (moderator) { - this.moderationLogService.log(moderator, 'assignRole', { - roleId: roleId, - roleName: role.name, - userId: userId, - userUsername: user.username, - userHost: user.host, - expiresAt: expiresAt ? expiresAt.toISOString() : null, - }); - } } @bindThis - public async unassign(userId: MiUser['id'], roleId: MiRole['id'], moderator?: MiUser): Promise { + public async unassign(userId: User['id'], roleId: Role['id']): Promise { const now = new Date(); - + const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId }); if (existing == null) { throw new RoleService.NotAssignedError(); @@ -609,105 +411,27 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { }); this.globalEventService.publishInternalEvent('userRoleUnassigned', existing); - - if (moderator) { - const [user, role] = await Promise.all([ - this.usersRepository.findOneByOrFail({ id: userId }), - this.rolesRepository.findOneByOrFail({ id: roleId }), - ]); - this.moderationLogService.log(moderator, 'unassignRole', { - roleId: roleId, - roleName: role.name, - userId: userId, - userUsername: user.username, - userHost: user.host, - }); - } } @bindThis public async addNoteToRoleTimeline(note: Packed<'Note'>): Promise { const roles = await this.getUserRoles(note.userId); - const redisPipeline = this.redisForTimelines.pipeline(); + const redisPipeline = this.redisClient.pipeline(); for (const role of roles) { - this.fanoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline); + redisPipeline.xadd( + `roleTimeline:${role.id}`, + 'MAXLEN', '~', '1000', + '*', + 'note', note.id); + this.globalEventService.publishRoleTimelineStream(role.id, 'note', note); } redisPipeline.exec(); } - @bindThis - public async create(values: Partial, moderator?: MiUser): Promise { - const date = new Date(); - const created = await this.rolesRepository.insertOne({ - id: this.idService.gen(date.getTime()), - updatedAt: date, - lastUsedAt: date, - name: values.name, - description: values.description, - color: values.color, - iconUrl: values.iconUrl, - target: values.target, - condFormula: values.condFormula, - isPublic: values.isPublic, - isAdministrator: values.isAdministrator, - isModerator: values.isModerator, - isExplorable: values.isExplorable, - asBadge: values.asBadge, - preserveAssignmentOnMoveAccount: values.preserveAssignmentOnMoveAccount, - canEditMembersByModerator: values.canEditMembersByModerator, - displayOrder: values.displayOrder, - policies: values.policies, - }); - - this.globalEventService.publishInternalEvent('roleCreated', created); - - if (moderator) { - this.moderationLogService.log(moderator, 'createRole', { - roleId: created.id, - role: created, - }); - } - - return created; - } - - @bindThis - public async update(role: MiRole, params: Partial, moderator?: MiUser): Promise { - const date = new Date(); - await this.rolesRepository.update(role.id, { - updatedAt: date, - ...params, - }); - - const updated = await this.rolesRepository.findOneByOrFail({ id: role.id }); - this.globalEventService.publishInternalEvent('roleUpdated', updated); - - if (moderator) { - this.moderationLogService.log(moderator, 'updateRole', { - roleId: role.id, - before: role, - after: updated, - }); - } - } - - @bindThis - public async delete(role: MiRole, moderator?: MiUser): Promise { - await this.rolesRepository.delete({ id: role.id }); - this.globalEventService.publishInternalEvent('roleDeleted', role); - - if (moderator) { - this.moderationLogService.log(moderator, 'deleteRole', { - roleId: role.id, - role: role, - }); - } - } - @bindThis public dispose(): void { this.redisForSub.off('message', this.onMessage); diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts index 968a5dcc0b..01ce12ffdd 100644 --- a/packages/backend/src/core/S3Service.ts +++ b/packages/backend/src/core/S3Service.ts @@ -1,16 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { URL } from 'node:url'; import * as http from 'node:http'; import * as https from 'node:https'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { Upload } from '@aws-sdk/lib-storage'; -import { NodeHttpHandler, NodeHttpHandlerOptions } from '@smithy/node-http-handler'; -import type { MiMeta } from '@/models/Meta.js'; +import { NodeHttpHandler, NodeHttpHandlerOptions } from '@aws-sdk/node-http-handler'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; +import type { Meta } from '@/models/entities/Meta.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/client-s3'; @@ -18,17 +15,20 @@ import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/c @Injectable() export class S3Service { constructor( + @Inject(DI.config) + private config: Config, + private httpRequestService: HttpRequestService, ) { } @bindThis - public getS3Client(meta: MiMeta): S3Client { + public getS3Client(meta: Meta): S3Client { const u = meta.objectStorageEndpoint ? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}` : `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent - const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy, true); + const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy); const handlerOption: NodeHttpHandlerOptions = {}; if (meta.objectStorageUseSSL) { handlerOption.httpsAgent = agent as https.Agent; @@ -46,13 +46,11 @@ export class S3Service { tls: meta.objectStorageUseSSL, forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted requestHandler: new NodeHttpHandler(handlerOption), - requestChecksumCalculation: 'WHEN_REQUIRED', - responseChecksumValidation: 'WHEN_REQUIRED', }); } @bindThis - public async upload(meta: MiMeta, input: PutObjectCommandInput) { + public async upload(meta: Meta, input: PutObjectCommandInput) { const client = this.getS3Client(meta); return new Upload({ client, @@ -64,7 +62,7 @@ export class S3Service { } @bindThis - public delete(meta: MiMeta, input: DeleteObjectCommandInput) { + public delete(meta: Meta, input: DeleteObjectCommandInput) { const client = this.getS3Client(meta); return client.send(new DeleteObjectCommand(input)); } diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index 643a5f525d..9502afcc9b 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -1,22 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { type Config, FulltextSearchProvider } from '@/config.js'; +import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; -import { MiNote } from '@/models/Note.js'; -import type { NotesRepository } from '@/models/_.js'; -import { MiUser } from '@/models/_.js'; +import { Note } from '@/models/entities/Note.js'; +import { User } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { CacheService } from '@/core/CacheService.js'; import { QueryService } from '@/core/QueryService.js'; import { IdService } from '@/core/IdService.js'; -import { LoggerService } from '@/core/LoggerService.js'; import type { Index, MeiliSearch } from 'meilisearch'; type K = string; @@ -28,24 +20,10 @@ type Q = { op: '<', k: K, v: number } | { op: '>=', k: K, v: number } | { op: '<=', k: K, v: number } | - { op: 'is null', k: K } | - { op: 'is not null', k: K } | { op: 'and', qs: Q[] } | { op: 'or', qs: Q[] } | { op: 'not', q: Q }; -export type SearchOpts = { - userId?: MiNote['userId'] | null; - channelId?: MiNote['channelId'] | null; - host?: string | null; -}; - -export type SearchPagination = { - untilId?: MiNote['id']; - sinceId?: MiNote['id']; - limit: number; -}; - function compileValue(value: V): string { if (typeof value === 'string') { return `'${value}'`; // TODO: escape @@ -67,8 +45,6 @@ function compileQuery(q: Q): string { case '<=': return `(${q.k} <= ${compileValue(q.v)})`; case 'and': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' AND ') })`; case 'or': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' OR ') })`; - case 'is null': return `(${q.k} IS NULL)`; - case 'is not null': return `(${q.k} IS NOT NULL)`; case 'not': return `(NOT ${compileQuery(q.q)})`; default: throw new Error('unrecognized query operator'); } @@ -76,9 +52,7 @@ function compileQuery(q: Q): string { @Injectable() export class SearchService { - private readonly meilisearchIndexScope: 'local' | 'global' | string[] = 'local'; - private readonly meilisearchNoteIndex: Index | null = null; - private readonly provider: FulltextSearchProvider; + private meilisearchNoteIndex: Index | null = null; constructor( @Inject(DI.config) @@ -90,10 +64,8 @@ export class SearchService { @Inject(DI.notesRepository) private notesRepository: NotesRepository, - private cacheService: CacheService, private queryService: QueryService, private idService: IdService, - private loggerService: LoggerService, ) { if (meilisearch) { this.meilisearchNoteIndex = meilisearch.index(`${config.meilisearch!.index}---notes`); @@ -120,199 +92,89 @@ export class SearchService { }, }); } - - if (config.meilisearch?.scope) { - this.meilisearchIndexScope = config.meilisearch.scope; - } - - this.provider = config.fulltextSearch?.provider ?? 'sqlLike'; - this.loggerService.getLogger('SearchService').info(`-- Provider: ${this.provider}`); } @bindThis - public async indexNote(note: MiNote): Promise { - if (!this.meilisearch) return; + public async indexNote(note: Note): Promise { if (note.text == null && note.cw == null) return; if (!['home', 'public'].includes(note.visibility)) return; - switch (this.meilisearchIndexScope) { - case 'global': - break; - - case 'local': - if (note.userHost == null) break; - return; - - default: { - if (note.userHost == null) break; - if (this.meilisearchIndexScope.includes(note.userHost)) break; - return; - } - } - - await this.meilisearchNoteIndex?.addDocuments([{ - id: note.id, - createdAt: this.idService.parse(note.id).date.getTime(), - userId: note.userId, - userHost: note.userHost, - channelId: note.channelId, - cw: note.cw, - text: note.text, - tags: note.tags, - }], { - primaryKey: 'id', - }); - } - - @bindThis - public async unindexNote(note: MiNote): Promise { - if (!this.meilisearch) return; - if (!['home', 'public'].includes(note.visibility)) return; - - await this.meilisearchNoteIndex?.deleteDocument(note.id); - } - - @bindThis - public async searchNote( - q: string, - me: MiUser | null, - opts: SearchOpts, - pagination: SearchPagination, - ): Promise { - switch (this.provider) { - case 'sqlLike': - case 'sqlPgroonga': { - // ほとんど内容に差がないのでsqlLikeとsqlPgroongaを同じ処理にしている. - // 今後の拡張で差が出る用であれば関数を分ける. - return this.searchNoteByLike(q, me, opts, pagination); - } - case 'meilisearch': { - return this.searchNoteByMeiliSearch(q, me, opts, pagination); - } - default: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const typeCheck: never = this.provider; - return []; - } + if (this.meilisearch) { + this.meilisearchNoteIndex!.addDocuments([{ + id: note.id, + createdAt: note.createdAt.getTime(), + userId: note.userId, + userHost: note.userHost, + channelId: note.channelId, + cw: note.cw, + text: note.text, + tags: note.tags, + }], { + primaryKey: 'id', + }); } } @bindThis - private async searchNoteByLike( - q: string, - me: MiUser | null, - opts: SearchOpts, - pagination: SearchPagination, - ): Promise { - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), pagination.sinceId, pagination.untilId); - - if (opts.userId) { - query.andWhere('note.userId = :userId', { userId: opts.userId }); - } else if (opts.channelId) { - query.andWhere('note.channelId = :channelId', { channelId: opts.channelId }); - } - - query - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - if (this.config.fulltextSearch?.provider === 'sqlPgroonga') { - query.andWhere('note.text &@~ :q', { q }); + public async searchNote(q: string, me: User | null, opts: { + userId?: Note['userId'] | null; + channelId?: Note['channelId'] | null; + host?: string | null; + }, pagination: { + untilId?: Note['id']; + sinceId?: Note['id']; + limit?: number; + }): Promise { + if (this.meilisearch) { + const filter: Q = { + op: 'and', + qs: [], + }; + if (pagination.untilId) filter.qs.push({ op: '<', k: 'createdAt', v: this.idService.parse(pagination.untilId).date.getTime() }); + if (pagination.sinceId) filter.qs.push({ op: '>', k: 'createdAt', v: this.idService.parse(pagination.sinceId).date.getTime() }); + if (opts.userId) filter.qs.push({ op: '=', k: 'userId', v: opts.userId }); + if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId }); + if (opts.host) { + if (opts.host === '.') { + // TODO: Meilisearchが2023/05/07現在値がNULLかどうかのクエリが書けない + } else { + filter.qs.push({ op: '=', k: 'userHost', v: opts.host }); + } + } + const res = await this.meilisearchNoteIndex!.search(q, { + sort: ['createdAt:desc'], + matchingStrategy: 'all', + attributesToRetrieve: ['id', 'createdAt'], + filter: compileQuery(filter), + limit: pagination.limit, + }); + if (res.hits.length === 0) return []; + const notes = await this.notesRepository.findBy({ + id: In(res.hits.map(x => x.id)), + }); + return notes.sort((a, b) => a.id > b.id ? -1 : 1); } else { - query.andWhere('LOWER(note.text) LIKE :q', { q: `%${ sqlLikeEscape(q.toLowerCase()) }%` }); - } + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), pagination.sinceId, pagination.untilId); - if (opts.host) { - if (opts.host === '.') { - query.andWhere('user.host IS NULL'); - } else { - query.andWhere('user.host = :host', { host: opts.host }); + if (opts.userId) { + query.andWhere('note.userId = :userId', { userId: opts.userId }); + } else if (opts.channelId) { + query.andWhere('note.channelId = :channelId', { channelId: opts.channelId }); } + + query + .andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(q) }%` }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); + + this.queryService.generateVisibilityQuery(query, me); + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); + + return await query.take(pagination.limit).getMany(); } - - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBaseNoteFilteringQuery(query, me); - - return query.limit(pagination.limit).getMany(); - } - - @bindThis - private async searchNoteByMeiliSearch( - q: string, - me: MiUser | null, - opts: SearchOpts, - pagination: SearchPagination, - ): Promise { - if (!this.meilisearch || !this.meilisearchNoteIndex) { - throw new Error('MeiliSearch is not available'); - } - - const filter: Q = { - op: 'and', - qs: [], - }; - if (pagination.untilId) filter.qs.push({ - op: '<', - k: 'createdAt', - v: this.idService.parse(pagination.untilId).date.getTime(), - }); - if (pagination.sinceId) filter.qs.push({ - op: '>', - k: 'createdAt', - v: this.idService.parse(pagination.sinceId).date.getTime(), - }); - if (opts.userId) filter.qs.push({ op: '=', k: 'userId', v: opts.userId }); - if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId }); - if (opts.host) { - if (opts.host === '.') { - filter.qs.push({ op: 'is null', k: 'userHost' }); - } else { - filter.qs.push({ op: '=', k: 'userHost', v: opts.host }); - } - } - - const res = await this.meilisearchNoteIndex.search(q, { - sort: ['createdAt:desc'], - matchingStrategy: 'all', - attributesToRetrieve: ['id', 'createdAt'], - filter: compileQuery(filter), - limit: pagination.limit, - }); - if (res.hits.length === 0) { - return []; - } - - const [ - userIdsWhoMeMuting, - userIdsWhoBlockingMe, - ] = me - ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]) - : [new Set(), new Set()]; - - const query = this.notesRepository.createQueryBuilder('note') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - query.where('note.id IN (:...noteIds)', { noteIds: res.hits.map(x => x.id) }); - - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - - const notes = (await query.getMany()).filter(note => { - if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; - return true; - }); - - return notes.sort((a, b) => a.id > b.id ? -1 : 1); } } diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 318ac5ccd9..29eb65fda4 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -1,26 +1,20 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { generateKeyPair } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { DataSource, IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; -import { MiUser } from '@/models/User.js'; -import { MiUserProfile } from '@/models/UserProfile.js'; +import type { UsedUsernamesRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; +import { User } from '@/models/entities/User.js'; +import { UserProfile } from '@/models/entities/UserProfile.js'; import { IdService } from '@/core/IdService.js'; -import { MiUserKeypair } from '@/models/UserKeypair.js'; -import { MiUsedUsername } from '@/models/UsedUsername.js'; -import { generateNativeUserToken } from '@/misc/token.js'; +import { UserKeypair } from '@/models/entities/UserKeypair.js'; +import { UsedUsername } from '@/models/entities/UsedUsername.js'; +import generateUserToken from '@/misc/generate-native-user-token.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import UsersChart from '@/core/chart/charts/users.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { UserService } from '@/core/UserService.js'; -import { SystemAccountService } from '@/core/SystemAccountService.js'; import { MetaService } from '@/core/MetaService.js'; @Injectable() @@ -29,8 +23,8 @@ export class SignupService { @Inject(DI.db) private db: DataSource, - @Inject(DI.meta) - private meta: MiMeta, + @Inject(DI.config) + private config: Config, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -39,10 +33,8 @@ export class SignupService { private usedUsernamesRepository: UsedUsernamesRepository, private utilityService: UtilityService, - private userService: UserService, private userEntityService: UserEntityService, private idService: IdService, - private systemAccountService: SystemAccountService, private metaService: MetaService, private usersChart: UsersChart, ) { @@ -50,46 +42,49 @@ export class SignupService { @bindThis public async signup(opts: { - username: MiUser['username']; + username: User['username']; password?: string | null; - passwordHash?: MiUserProfile['password'] | null; + passwordHash?: UserProfile['password'] | null; host?: string | null; ignorePreservedUsernames?: boolean; }) { const { username, password, passwordHash, host } = opts; let hash = passwordHash; - + // Validate username if (!this.userEntityService.validateLocalUsername(username)) { throw new Error('INVALID_USERNAME'); } - + if (password != null && passwordHash == null) { // Validate password if (!this.userEntityService.validatePassword(password)) { throw new Error('INVALID_PASSWORD'); } - + // Generate hash of password const salt = await bcrypt.genSalt(8); hash = await bcrypt.hash(password, salt); } - + // Generate secret - const secret = generateNativeUserToken(); - + const secret = generateUserToken(); + // Check username duplication - if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { + if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { throw new Error('DUPLICATED_USERNAME'); } - + // Check deleted username duplication - if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) { + if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) { throw new Error('USED_USERNAME'); } - if (!opts.ignorePreservedUsernames && this.meta.rootUserId != null) { - const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); + const isTheFirstUser = (await this.usersRepository.countBy({ host: IsNull() })) === 0; + + if (!opts.ignorePreservedUsernames && !isTheFirstUser) { + const instance = await this.metaService.fetch(true); + const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); if (isPreserved) { throw new Error('USED_USERNAME'); } @@ -111,51 +106,48 @@ export class SignupService { }, (err, publicKey, privateKey) => err ? rej(err) : res([publicKey, privateKey]), )); - - let account!: MiUser; - + + let account!: User; + // Start transaction await this.db.transaction(async transactionalEntityManager => { - const exist = await transactionalEntityManager.findOneBy(MiUser, { + const exist = await transactionalEntityManager.findOneBy(User, { usernameLower: username.toLowerCase(), host: IsNull(), }); - + if (exist) throw new Error(' the username is already used'); - - account = await transactionalEntityManager.save(new MiUser({ - id: this.idService.gen(), + + account = await transactionalEntityManager.save(new User({ + id: this.idService.genId(), + createdAt: new Date(), username: username, usernameLower: username.toLowerCase(), host: this.utilityService.toPunyNullable(host), token: secret, + isRoot: isTheFirstUser, })); - - await transactionalEntityManager.save(new MiUserKeypair({ + + await transactionalEntityManager.save(new UserKeypair({ publicKey: keyPair[0], privateKey: keyPair[1], userId: account.id, })); - - await transactionalEntityManager.save(new MiUserProfile({ + + await transactionalEntityManager.save(new UserProfile({ userId: account.id, autoAcceptFollowed: true, password: hash, })); - - await transactionalEntityManager.save(new MiUsedUsername({ + + await transactionalEntityManager.save(new UsedUsername({ createdAt: new Date(), username: username.toLowerCase(), })); }); - + this.usersChart.update(account, true); - this.userService.notifySystemWebhook(account, 'userCreated'); - - if (this.meta.rootUserId == null) { - await this.metaService.update({ rootUserId: account.id }); - } - + return { account, secret }; } } diff --git a/packages/backend/src/core/SystemAccountService.ts b/packages/backend/src/core/SystemAccountService.ts deleted file mode 100644 index 78deffc1db..0000000000 --- a/packages/backend/src/core/SystemAccountService.ts +++ /dev/null @@ -1,214 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { randomUUID } from 'node:crypto'; -import { Inject, Injectable } from '@nestjs/common'; -import type { OnApplicationShutdown } from '@nestjs/common'; -import { DataSource, IsNull } from 'typeorm'; -import * as Redis from 'ioredis'; -import bcrypt from 'bcryptjs'; -import { MiLocalUser, MiUser } from '@/models/User.js'; -import { MiSystemAccount, MiUsedUsername, MiUserKeypair, MiUserProfile, type UsersRepository, type SystemAccountsRepository } from '@/models/_.js'; -import type { MiMeta, UserProfilesRepository } from '@/models/_.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import { MemoryKVCache } from '@/misc/cache.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { generateNativeUserToken } from '@/misc/token.js'; -import { IdService } from '@/core/IdService.js'; -import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; - -export const SYSTEM_ACCOUNT_TYPES = ['actor', 'relay', 'proxy'] as const; - -@Injectable() -export class SystemAccountService implements OnApplicationShutdown { - private cache: MemoryKVCache; - - constructor( - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - - @Inject(DI.db) - private db: DataSource, - - @Inject(DI.meta) - private meta: MiMeta, - - @Inject(DI.systemAccountsRepository) - private systemAccountsRepository: SystemAccountsRepository, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - - private idService: IdService, - ) { - this.cache = new MemoryKVCache(1000 * 60 * 10); // 10m - - this.redisForSub.on('message', this.onMessage); - } - - @bindThis - private async onMessage(_: string, data: string): Promise { - const obj = JSON.parse(data); - - if (obj.channel === 'internal') { - const { type, body } = obj.message as GlobalEvents['internal']['payload']; - switch (type) { - case 'metaUpdated': { - if (body.before != null && body.before.name !== body.after.name) { - for (const account of SYSTEM_ACCOUNT_TYPES) { - await this.updateCorrespondingUserProfile(account, { - name: body.after.name, - }); - } - } - break; - } - default: - break; - } - } - } - - @bindThis - public async list(): Promise { - const accounts = await this.systemAccountsRepository.findBy({}); - - return accounts; - } - - @bindThis - public async fetch(type: typeof SYSTEM_ACCOUNT_TYPES[number]): Promise { - const cached = this.cache.get(type); - if (cached) return cached; - - const systemAccount = await this.systemAccountsRepository.findOne({ - where: { type: type }, - relations: ['user'], - }); - - if (systemAccount) { - this.cache.set(type, systemAccount.user as MiLocalUser); - return systemAccount.user as MiLocalUser; - } else { - const created = await this.createCorrespondingUser(type, { - username: `system.${type}`, // NOTE: (できれば避けたいが) . が含まれるかどうかでシステムアカウントかどうかを判定している処理もあるので変えないように - name: this.meta.name, - }); - this.cache.set(type, created); - return created; - } - } - - @bindThis - private async createCorrespondingUser(type: typeof SYSTEM_ACCOUNT_TYPES[number], extra: { - username: MiUser['username']; - name?: MiUser['name']; - }): Promise { - const password = randomUUID(); - - // Generate hash of password - const salt = await bcrypt.genSalt(8); - const hash = await bcrypt.hash(password, salt); - - // Generate secret - const secret = generateNativeUserToken(); - - const keyPair = await genRsaKeyPair(4096); - - let account!: MiUser; - - // Start transaction - await this.db.transaction(async transactionalEntityManager => { - const exist = await transactionalEntityManager.findOneBy(MiUser, { - usernameLower: extra.username.toLowerCase(), - host: IsNull(), - }); - - if (exist) { - account = exist; - return; - } - - account = await transactionalEntityManager.insert(MiUser, { - id: this.idService.gen(), - username: extra.username, - usernameLower: extra.username.toLowerCase(), - host: null, - token: secret, - isLocked: true, - isExplorable: false, - isBot: true, - name: extra.name, - }).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0])); - - await transactionalEntityManager.insert(MiUserKeypair, { - publicKey: keyPair.publicKey, - privateKey: keyPair.privateKey, - userId: account.id, - }); - - await transactionalEntityManager.insert(MiUserProfile, { - userId: account.id, - autoAcceptFollowed: false, - password: hash, - }); - - await transactionalEntityManager.insert(MiUsedUsername, { - createdAt: new Date(), - username: extra.username.toLowerCase(), - }); - - await transactionalEntityManager.insert(MiSystemAccount, { - id: this.idService.gen(), - userId: account.id, - type: type, - }); - }); - - return account as MiLocalUser; - } - - @bindThis - public async updateCorrespondingUserProfile(type: typeof SYSTEM_ACCOUNT_TYPES[number], extra: { - name?: string | null; - description?: MiUserProfile['description']; - }): Promise { - const user = await this.fetch(type); - - const updates = {} as Partial; - if (extra.name !== undefined) updates.name = extra.name; - - if (Object.keys(updates).length > 0) { - await this.usersRepository.update(user.id, updates); - } - - const profileUpdates = {} as Partial; - if (extra.description !== undefined) profileUpdates.description = extra.description; - - if (Object.keys(profileUpdates).length > 0) { - await this.userProfilesRepository.update(user.id, profileUpdates); - } - - const updated = await this.usersRepository.findOneByOrFail({ id: user.id }) as MiLocalUser; - this.cache.set(type, updated); - - return updated; - } - - @bindThis - public dispose(): void { - this.redisForSub.off('message', this.onMessage); - this.cache.dispose(); - } - - @bindThis - public onApplicationShutdown(signal?: string): void { - this.dispose(); - } -} diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts deleted file mode 100644 index 8239490adc..0000000000 --- a/packages/backend/src/core/SystemWebhookService.ts +++ /dev/null @@ -1,259 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import type { MiUser, SystemWebhooksRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js'; -import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js'; -import { IdService } from '@/core/IdService.js'; -import { QueueService } from '@/core/QueueService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { LoggerService } from '@/core/LoggerService.js'; -import Logger from '@/logger.js'; -import { Packed } from '@/misc/json-schema.js'; -import { AbuseReportResolveType } from '@/models/AbuseUserReport.js'; -import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; - -export type AbuseReportPayload = { - id: string; - targetUserId: string; - targetUser: Packed<'UserLite'> | null; - targetUserHost: string | null; - reporterId: string; - reporter: Packed<'UserLite'> | null; - reporterHost: string | null; - assigneeId: string | null; - assignee: Packed<'UserLite'> | null; - resolved: boolean; - forwarded: boolean; - comment: string; - moderationNote: string; - resolvedAs: AbuseReportResolveType | null; -}; - -export type InactiveModeratorsWarningPayload = { - remainingTime: ModeratorInactivityRemainingTime; -}; - -export type SystemWebhookPayload = - T extends 'abuseReport' | 'abuseReportResolved' ? AbuseReportPayload : - T extends 'userCreated' ? Packed<'UserLite'> : - T extends 'inactiveModeratorsWarning' ? InactiveModeratorsWarningPayload : - T extends 'inactiveModeratorsInvitationOnlyChanged' ? Record : - never; - -@Injectable() -export class SystemWebhookService implements OnApplicationShutdown { - private activeSystemWebhooksFetched = false; - private activeSystemWebhooks: MiSystemWebhook[] = []; - - constructor( - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - @Inject(DI.systemWebhooksRepository) - private systemWebhooksRepository: SystemWebhooksRepository, - private idService: IdService, - private queueService: QueueService, - private moderationLogService: ModerationLogService, - private globalEventService: GlobalEventService, - ) { - this.redisForSub.on('message', this.onMessage); - } - - @bindThis - public async fetchActiveSystemWebhooks() { - if (!this.activeSystemWebhooksFetched) { - this.activeSystemWebhooks = await this.systemWebhooksRepository.findBy({ - isActive: true, - }); - this.activeSystemWebhooksFetched = true; - } - - return this.activeSystemWebhooks; - } - - /** - * SystemWebhook の一覧を取得する. - */ - @bindThis - public fetchSystemWebhooks(params?: { - ids?: MiSystemWebhook['id'][]; - isActive?: MiSystemWebhook['isActive']; - on?: MiSystemWebhook['on']; - }): Promise { - const query = this.systemWebhooksRepository.createQueryBuilder('systemWebhook'); - if (params) { - if (params.ids && params.ids.length > 0) { - query.andWhere('systemWebhook.id IN (:...ids)', { ids: params.ids }); - } - if (params.isActive !== undefined) { - query.andWhere('systemWebhook.isActive = :isActive', { isActive: params.isActive }); - } - if (params.on && params.on.length > 0) { - query.andWhere(':on <@ systemWebhook.on', { on: params.on }); - } - } - - return query.getMany(); - } - - /** - * SystemWebhook を作成する. - */ - @bindThis - public async createSystemWebhook( - params: { - isActive: MiSystemWebhook['isActive']; - name: MiSystemWebhook['name']; - on: MiSystemWebhook['on']; - url: MiSystemWebhook['url']; - secret: MiSystemWebhook['secret']; - }, - updater: MiUser, - ): Promise { - const id = this.idService.gen(); - await this.systemWebhooksRepository.insert({ - ...params, - id, - }); - - const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id }); - this.globalEventService.publishInternalEvent('systemWebhookCreated', webhook); - this.moderationLogService - .log(updater, 'createSystemWebhook', { - systemWebhookId: webhook.id, - webhook: webhook, - }); - - return webhook; - } - - /** - * SystemWebhook を更新する. - */ - @bindThis - public async updateSystemWebhook( - params: { - id: MiSystemWebhook['id']; - isActive: MiSystemWebhook['isActive']; - name: MiSystemWebhook['name']; - on: MiSystemWebhook['on']; - url: MiSystemWebhook['url']; - secret: MiSystemWebhook['secret']; - }, - updater: MiUser, - ): Promise { - const beforeEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: params.id }); - await this.systemWebhooksRepository.update(beforeEntity.id, { - updatedAt: new Date(), - isActive: params.isActive, - name: params.name, - on: params.on, - url: params.url, - secret: params.secret, - }); - - const afterEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: beforeEntity.id }); - this.globalEventService.publishInternalEvent('systemWebhookUpdated', afterEntity); - this.moderationLogService - .log(updater, 'updateSystemWebhook', { - systemWebhookId: beforeEntity.id, - before: beforeEntity, - after: afterEntity, - }); - - return afterEntity; - } - - /** - * SystemWebhook を削除する. - */ - @bindThis - public async deleteSystemWebhook(id: MiSystemWebhook['id'], updater: MiUser) { - const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id }); - await this.systemWebhooksRepository.delete(id); - - this.globalEventService.publishInternalEvent('systemWebhookDeleted', webhook); - this.moderationLogService - .log(updater, 'deleteSystemWebhook', { - systemWebhookId: webhook.id, - webhook, - }); - } - - /** - * SystemWebhook をWebhook配送キューに追加する - * @see QueueService.systemWebhookDeliver - */ - @bindThis - public async enqueueSystemWebhook( - type: T, - content: SystemWebhookPayload, - opts?: { - excludes?: MiSystemWebhook['id'][]; - }, - ) { - const webhooks = await this.fetchActiveSystemWebhooks() - .then(webhooks => { - return webhooks.filter(webhook => !opts?.excludes?.includes(webhook.id) && webhook.on.includes(type)); - }); - return Promise.all( - webhooks.map(webhook => { - return this.queueService.systemWebhookDeliver(webhook, type, content); - }), - ); - } - - @bindThis - private async onMessage(_: string, data: string): Promise { - const obj = JSON.parse(data); - if (obj.channel !== 'internal') { - return; - } - - const { type, body } = obj.message as GlobalEvents['internal']['payload']; - switch (type) { - case 'systemWebhookCreated': { - if (body.isActive) { - this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body)); - } - break; - } - case 'systemWebhookUpdated': { - if (body.isActive) { - const i = this.activeSystemWebhooks.findIndex(a => a.id === body.id); - if (i > -1) { - this.activeSystemWebhooks[i] = MiSystemWebhook.deserialize(body); - } else { - this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body)); - } - } else { - this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id); - } - break; - } - case 'systemWebhookDeleted': { - this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id); - break; - } - default: - break; - } - } - - @bindThis - public dispose(): void { - this.redisForSub.off('message', this.onMessage); - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); - } -} diff --git a/packages/backend/src/core/TwoFactorAuthenticationService.ts b/packages/backend/src/core/TwoFactorAuthenticationService.ts new file mode 100644 index 0000000000..dda78236e9 --- /dev/null +++ b/packages/backend/src/core/TwoFactorAuthenticationService.ts @@ -0,0 +1,445 @@ +import * as crypto from 'node:crypto'; +import { Inject, Injectable } from '@nestjs/common'; +import * as jsrsasign from 'jsrsasign'; +import { DI } from '@/di-symbols.js'; +import type { UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; +import { bindThis } from '@/decorators.js'; + +const ECC_PRELUDE = Buffer.from([0x04]); +const NULL_BYTE = Buffer.from([0]); +const PEM_PRELUDE = Buffer.from( + '3059301306072a8648ce3d020106082a8648ce3d030107034200', + 'hex', +); + +// Android Safetynet attestations are signed with this cert: +const GSR2 = `-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 +MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL +v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 +eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq +tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd +C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa +zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB +mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH +V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG +3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs +J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO +291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS +ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd +AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE-----\n`; + +function base64URLDecode(source: string) { + return Buffer.from(source.replace(/\-/g, '+').replace(/_/g, '/'), 'base64'); +} + +function getCertSubject(certificate: string) { + const subjectCert = new jsrsasign.X509(); + subjectCert.readCertPEM(certificate); + + const subjectString = subjectCert.getSubjectString(); + const subjectFields = subjectString.slice(1).split('/'); + + const fields = {} as Record; + for (const field of subjectFields) { + const eqIndex = field.indexOf('='); + fields[field.substring(0, eqIndex)] = field.substring(eqIndex + 1); + } + + return fields; +} + +function verifyCertificateChain(certificates: string[]) { + let valid = true; + + for (let i = 0; i < certificates.length; i++) { + const Cert = certificates[i]; + const certificate = new jsrsasign.X509(); + certificate.readCertPEM(Cert); + + const CACert = i + 1 >= certificates.length ? Cert : certificates[i + 1]; + + const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]); + if (certStruct == null) throw new Error('certStruct is null'); + + const algorithm = certificate.getSignatureAlgorithmField(); + const signatureHex = certificate.getSignatureValueHex(); + + // Verify against CA + const Signature = new jsrsasign.KJUR.crypto.Signature({ alg: algorithm }); + Signature.init(CACert); + Signature.updateHex(certStruct); + valid = valid && !!Signature.verify(signatureHex); // true if CA signed the certificate + } + + return valid; +} + +function PEMString(pemBuffer: Buffer, type = 'CERTIFICATE') { + if (pemBuffer.length === 65 && pemBuffer[0] === 0x04) { + pemBuffer = Buffer.concat([PEM_PRELUDE, pemBuffer], 91); + type = 'PUBLIC KEY'; + } + const cert = pemBuffer.toString('base64'); + + const keyParts = []; + const max = Math.ceil(cert.length / 64); + let start = 0; + for (let i = 0; i < max; i++) { + keyParts.push(cert.substring(start, start + 64)); + start += 64; + } + + return ( + `-----BEGIN ${type}-----\n` + + keyParts.join('\n') + + `\n-----END ${type}-----\n` + ); +} + +@Injectable() +export class TwoFactorAuthenticationService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + ) { + } + + @bindThis + public hash(data: Buffer) { + return crypto + .createHash('sha256') + .update(data) + .digest(); + } + + @bindThis + public verifySignin({ + publicKey, + authenticatorData, + clientDataJSON, + clientData, + signature, + challenge, + }: { + publicKey: Buffer, + authenticatorData: Buffer, + clientDataJSON: Buffer, + clientData: any, + signature: Buffer, + challenge: string + }) { + if (clientData.type !== 'webauthn.get') { + throw new Error('type is not webauthn.get'); + } + + if (this.hash(clientData.challenge).toString('hex') !== challenge) { + throw new Error('challenge mismatch'); + } + if (clientData.origin !== this.config.scheme + '://' + this.config.host) { + throw new Error('origin mismatch'); + } + + const verificationData = Buffer.concat( + [authenticatorData, this.hash(clientDataJSON)], + 32 + authenticatorData.length, + ); + + return crypto + .createVerify('SHA256') + .update(verificationData) + .verify(PEMString(publicKey), signature); + } + + @bindThis + public getProcedures() { + return { + none: { + verify({ publicKey }: { publicKey: Map }) { + const negTwo = publicKey.get(-2); + + if (!negTwo || negTwo.length !== 32) { + throw new Error('invalid or no -2 key given'); + } + const negThree = publicKey.get(-3); + if (!negThree || negThree.length !== 32) { + throw new Error('invalid or no -3 key given'); + } + + const publicKeyU2F = Buffer.concat( + [ECC_PRELUDE, negTwo, negThree], + 1 + 32 + 32, + ); + + return { + publicKey: publicKeyU2F, + valid: true, + }; + }, + }, + 'android-key': { + verify({ + attStmt, + authenticatorData, + clientDataHash, + publicKey, + rpIdHash, + credentialId, + }: { + attStmt: any, + authenticatorData: Buffer, + clientDataHash: Buffer, + publicKey: Map; + rpIdHash: Buffer, + credentialId: Buffer, + }) { + if (attStmt.alg !== -7) { + throw new Error('alg mismatch'); + } + + const verificationData = Buffer.concat([ + authenticatorData, + clientDataHash, + ]); + + const attCert: Buffer = attStmt.x5c[0]; + + const negTwo = publicKey.get(-2); + + if (!negTwo || negTwo.length !== 32) { + throw new Error('invalid or no -2 key given'); + } + const negThree = publicKey.get(-3); + if (!negThree || negThree.length !== 32) { + throw new Error('invalid or no -3 key given'); + } + + const publicKeyData = Buffer.concat( + [ECC_PRELUDE, negTwo, negThree], + 1 + 32 + 32, + ); + + if (!attCert.equals(publicKeyData)) { + throw new Error('public key mismatch'); + } + + const isValid = crypto + .createVerify('SHA256') + .update(verificationData) + .verify(PEMString(attCert), attStmt.sig); + + // TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON) + + return { + valid: isValid, + publicKey: publicKeyData, + }; + }, + }, + // what a stupid attestation + 'android-safetynet': { + verify: ({ + attStmt, + authenticatorData, + clientDataHash, + publicKey, + rpIdHash, + credentialId, + }: { + attStmt: any, + authenticatorData: Buffer, + clientDataHash: Buffer, + publicKey: Map; + rpIdHash: Buffer, + credentialId: Buffer, + }) => { + const verificationData = this.hash( + Buffer.concat([authenticatorData, clientDataHash]), + ); + + const jwsParts = attStmt.response.toString('utf-8').split('.'); + + const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8')); + const response = JSON.parse( + base64URLDecode(jwsParts[1]).toString('utf-8'), + ); + const signature = jwsParts[2]; + + if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) { + throw new Error('invalid nonce'); + } + + const certificateChain = header.x5c + .map((key: any) => PEMString(key)) + .concat([GSR2]); + + if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') { + throw new Error('invalid common name'); + } + + if (!verifyCertificateChain(certificateChain)) { + throw new Error('Invalid certificate chain!'); + } + + const signatureBase = Buffer.from( + jwsParts[0] + '.' + jwsParts[1], + 'utf-8', + ); + + const valid = crypto + .createVerify('sha256') + .update(signatureBase) + .verify(certificateChain[0], base64URLDecode(signature)); + + const negTwo = publicKey.get(-2); + + if (!negTwo || negTwo.length !== 32) { + throw new Error('invalid or no -2 key given'); + } + const negThree = publicKey.get(-3); + if (!negThree || negThree.length !== 32) { + throw new Error('invalid or no -3 key given'); + } + + const publicKeyData = Buffer.concat( + [ECC_PRELUDE, negTwo, negThree], + 1 + 32 + 32, + ); + return { + valid, + publicKey: publicKeyData, + }; + }, + }, + packed: { + verify({ + attStmt, + authenticatorData, + clientDataHash, + publicKey, + rpIdHash, + credentialId, + }: { + attStmt: any, + authenticatorData: Buffer, + clientDataHash: Buffer, + publicKey: Map; + rpIdHash: Buffer, + credentialId: Buffer, + }) { + const verificationData = Buffer.concat([ + authenticatorData, + clientDataHash, + ]); + + if (attStmt.x5c) { + const attCert = attStmt.x5c[0]; + + const validSignature = crypto + .createVerify('SHA256') + .update(verificationData) + .verify(PEMString(attCert), attStmt.sig); + + const negTwo = publicKey.get(-2); + + if (!negTwo || negTwo.length !== 32) { + throw new Error('invalid or no -2 key given'); + } + const negThree = publicKey.get(-3); + if (!negThree || negThree.length !== 32) { + throw new Error('invalid or no -3 key given'); + } + + const publicKeyData = Buffer.concat( + [ECC_PRELUDE, negTwo, negThree], + 1 + 32 + 32, + ); + + return { + valid: validSignature, + publicKey: publicKeyData, + }; + } else if (attStmt.ecdaaKeyId) { + // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation + throw new Error('ECDAA-Verify is not supported'); + } else { + if (attStmt.alg !== -7) throw new Error('alg mismatch'); + + throw new Error('self attestation is not supported'); + } + }, + }, + + 'fido-u2f': { + verify({ + attStmt, + authenticatorData, + clientDataHash, + publicKey, + rpIdHash, + credentialId, + }: { + attStmt: any, + authenticatorData: Buffer, + clientDataHash: Buffer, + publicKey: Map, + rpIdHash: Buffer, + credentialId: Buffer + }) { + const x5c: Buffer[] = attStmt.x5c; + if (x5c.length !== 1) { + throw new Error('x5c length does not match expectation'); + } + + const attCert = x5c[0]; + + // TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve + + const negTwo: Buffer = publicKey.get(-2); + + if (!negTwo || negTwo.length !== 32) { + throw new Error('invalid or no -2 key given'); + } + const negThree: Buffer = publicKey.get(-3); + if (!negThree || negThree.length !== 32) { + throw new Error('invalid or no -3 key given'); + } + + const publicKeyU2F = Buffer.concat( + [ECC_PRELUDE, negTwo, negThree], + 1 + 32 + 32, + ); + + const verificationData = Buffer.concat([ + NULL_BYTE, + rpIdHash, + clientDataHash, + credentialId, + publicKeyU2F, + ]); + + const validSignature = crypto + .createVerify('SHA256') + .update(verificationData) + .verify(PEMString(attCert), attStmt.sig); + + return { + valid: validSignature, + publicKey: publicKeyU2F, + }; + }, + }, + }; + } +} diff --git a/packages/backend/src/core/UserAuthService.ts b/packages/backend/src/core/UserAuthService.ts deleted file mode 100644 index bdc27cbe8e..0000000000 --- a/packages/backend/src/core/UserAuthService.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { QueryFailedError } from 'typeorm'; -import * as OTPAuth from 'otpauth'; -import { DI } from '@/di-symbols.js'; -import type { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js'; -import { bindThis } from '@/decorators.js'; -import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import type { MiLocalUser } from '@/models/User.js'; - -@Injectable() -export class UserAuthService { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - ) { - } - - @bindThis - public async twoFactorAuthenticate(profile: MiUserProfile, token: string): Promise { - if (profile.twoFactorBackupSecret?.includes(token)) { - await this.userProfilesRepository.update({ userId: profile.userId }, { - twoFactorBackupSecret: profile.twoFactorBackupSecret.filter((secret) => secret !== token), - }); - } else { - const delta = OTPAuth.TOTP.validate({ - secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!), - digits: 6, - token, - window: 5, - }); - - if (delta === null) { - throw new Error('authentication failed'); - } - } - } -} diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 8da1bb2092..3ca22f8bbc 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -1,22 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; import { IdService } from '@/core/IdService.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiBlocking } from '@/models/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Blocking } from '@/models/entities/Blocking.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListMembershipsRepository } from '@/models/_.js'; +import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; import Logger from '@/logger.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { LoggerService } from '@/core/LoggerService.js'; -import { UserWebhookService } from '@/core/UserWebhookService.js'; +import { WebhookService } from '@/core/WebhookService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; @@ -38,15 +34,15 @@ export class UserBlockingService implements OnModuleInit { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, private cacheService: CacheService, private userEntityService: UserEntityService, private idService: IdService, private queueService: QueueService, private globalEventService: GlobalEventService, - private webhookService: UserWebhookService, + private webhookService: WebhookService, private apRendererService: ApRendererService, private loggerService: LoggerService, ) { @@ -58,7 +54,7 @@ export class UserBlockingService implements OnModuleInit { } @bindThis - public async block(blocker: MiUser, blockee: MiUser, silent = false) { + public async block(blocker: User, blockee: User, silent = false) { await Promise.all([ this.cancelRequest(blocker, blockee, silent), this.cancelRequest(blockee, blocker, silent), @@ -68,12 +64,13 @@ export class UserBlockingService implements OnModuleInit { ]); const blocking = { - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), blocker, blockerId: blocker.id, blockee, blockeeId: blockee.id, - } as MiBlocking; + } as Blocking; await this.blockingsRepository.insert(blocking); @@ -92,7 +89,7 @@ export class UserBlockingService implements OnModuleInit { } @bindThis - private async cancelRequest(follower: MiUser, followee: MiUser, silent = false) { + private async cancelRequest(follower: User, followee: User, silent = false) { const request = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, followerId: follower.id, @@ -109,16 +106,22 @@ export class UserBlockingService implements OnModuleInit { if (this.userEntityService.isLocalUser(followee)) { this.userEntityService.pack(followee, followee, { - schema: 'MeDetailed', + detail: true, }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); } if (this.userEntityService.isLocalUser(follower) && !silent) { this.userEntityService.pack(followee, follower, { - schema: 'UserDetailedNotMe', + detail: true, }).then(async packed => { this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); - this.webhookService.enqueueUserWebhook(follower.id, 'unfollow', { user: packed }); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'unfollow', { + user: packed, + }); + } }); } @@ -136,13 +139,13 @@ export class UserBlockingService implements OnModuleInit { } @bindThis - private async removeFromList(listOwner: MiUser, user: MiUser) { + private async removeFromList(listOwner: User, user: User) { const userLists = await this.userListsRepository.findBy({ userId: listOwner.id, }); for (const userList of userLists) { - await this.userListMembershipsRepository.delete({ + await this.userListJoiningsRepository.delete({ userListId: userList.id, userId: user.id, }); @@ -150,7 +153,7 @@ export class UserBlockingService implements OnModuleInit { } @bindThis - public async unblock(blocker: MiUser, blockee: MiUser) { + public async unblock(blocker: User, blockee: User) { const blocking = await this.blockingsRepository.findOneBy({ blockerId: blocker.id, blockeeId: blockee.id, @@ -184,7 +187,7 @@ export class UserBlockingService implements OnModuleInit { } @bindThis - public async checkBlocked(blockerId: MiUser['id'], blockeeId: MiUser['id']): Promise { + public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise { return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId); } } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index e7a6be99fb..7d90bc2c08 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -1,47 +1,42 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; +import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; -import { Brackets, IsNull } from 'typeorm'; -import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; +import type { LocalUser, PartialLocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { QueueService } from '@/core/QueueService.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { IdService } from '@/core/IdService.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; +import type { Packed } from '@/misc/json-schema.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; -import { UserWebhookService } from '@/core/UserWebhookService.js'; +import { WebhookService } from '@/core/WebhookService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { bindThis } from '@/decorators.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; -import { AccountMoveService } from '@/core/AccountMoveService.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import type { ThinUser } from '@/queue/types.js'; import Logger from '../logger.js'; +import { IsNull } from 'typeorm'; +import { AccountMoveService } from '@/core/AccountMoveService.js'; const logger = new Logger('following/create'); -type Local = MiLocalUser | { - id: MiLocalUser['id']; - host: MiLocalUser['host']; - uri: MiLocalUser['uri'] +type Local = LocalUser | { + id: LocalUser['id']; + host: LocalUser['host']; + uri: LocalUser['uri'] }; -type Remote = MiRemoteUser | { - id: MiRemoteUser['id']; - host: MiRemoteUser['host']; - uri: MiRemoteUser['uri']; - inbox: MiRemoteUser['inbox']; +type Remote = RemoteUser | { + id: RemoteUser['id']; + host: RemoteUser['host']; + uri: RemoteUser['uri']; + inbox: RemoteUser['inbox']; }; type Both = Local | Remote; @@ -55,9 +50,6 @@ export class UserFollowingService implements OnModuleInit { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -74,14 +66,14 @@ export class UserFollowingService implements OnModuleInit { private instancesRepository: InstancesRepository, private cacheService: CacheService, - private utilityService: UtilityService, private userEntityService: UserEntityService, private idService: IdService, private queueService: QueueService, private globalEventService: GlobalEventService, + private metaService: MetaService, private notificationService: NotificationService, private federatedInstanceService: FederatedInstanceService, - private webhookService: UserWebhookService, + private webhookService: WebhookService, private apRendererService: ApRendererService, private accountMoveService: AccountMoveService, private perUserFollowingChart: PerUserFollowingChart, @@ -94,33 +86,11 @@ export class UserFollowingService implements OnModuleInit { } @bindThis - public async deliverAccept(follower: MiRemoteUser, followee: MiPartialLocalUser, requestId?: string) { - const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); - this.queueService.deliver(followee, content, follower.inbox, false); - } - - @bindThis - public async follow( - _follower: ThinUser, - _followee: ThinUser, - { requestId, silent = false, withReplies }: { - requestId?: string, - silent?: boolean, - withReplies?: boolean, - } = {}, - ): Promise { - /** - * 必ず最新のユーザー情報を取得する - */ + public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string, silent = false): Promise { const [follower, followee] = await Promise.all([ this.usersRepository.findOneByOrFail({ id: _follower.id }), this.usersRepository.findOneByOrFail({ id: _followee.id }), - ]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser]; - - if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isRemoteUser(followee)) { - // What? - throw new Error('Remote user cannot follow remote user.'); - } + ]) as [LocalUser | RemoteUser, LocalUser | RemoteUser]; // check blocking const [blocking, blocked] = await Promise.all([ @@ -142,66 +112,39 @@ export class UserFollowingService implements OnModuleInit { if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked'); } - if (await this.followingsRepository.exists({ - where: { - followerId: follower.id, - followeeId: followee.id, - }, - })) { - // すでにフォロー関係が存在している場合 - if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - // リモート → ローカル: acceptを送り返しておしまい - this.deliverAccept(follower, followee, requestId); - return; - } - if (this.userEntityService.isLocalUser(follower)) { - // ローカル → リモート/ローカル: 例外 - throw new IdentifiableError('ec3f65c0-a9d1-47d9-8791-b2e7b9dcdced', 'already following'); - } - } - const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id }); + // フォロー対象が鍵アカウントである or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or - // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである or - // フォロワーがローカルユーザーであり、フォロー対象がサイレンスされているサーバーである + // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである // 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく - if ( - followee.isLocked || - (followeeProfile.carefulBot && follower.isBot) || - (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') || - (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost(this.meta.silencedHosts, follower.host)) - ) { + if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee))) { let autoAccept = false; // 鍵アカウントであっても、既にフォローされていた場合はスルー - const isFollowing = await this.followingsRepository.exists({ - where: { - followerId: follower.id, - followeeId: followee.id, - }, + const following = await this.followingsRepository.findOneBy({ + followerId: follower.id, + followeeId: followee.id, }); - if (isFollowing) { + if (following) { autoAccept = true; } // フォローしているユーザーは自動承認オプション if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) { - const isFollowed = await this.followingsRepository.exists({ - where: { - followerId: followee.id, - followeeId: follower.id, - }, + const followed = await this.followingsRepository.findOneBy({ + followerId: followee.id, + followeeId: follower.id, }); - if (isFollowed) autoAccept = true; + if (followed) autoAccept = true; } // Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account. if (followee.isLocked && !autoAccept) { autoAccept = !!(await this.accountMoveService.validateAlsoKnownAs( follower, - (oldSrc, newSrc) => this.followingsRepository.exists({ + (oldSrc, newSrc) => this.followingsRepository.exist({ where: { followeeId: followee.id, followerId: newSrc.id, @@ -212,38 +155,38 @@ export class UserFollowingService implements OnModuleInit { } if (!autoAccept) { - await this.createFollowRequest(follower, followee, requestId, withReplies); + await this.createFollowRequest(follower, followee, requestId); return; } } - await this.insertFollowingDoc(followee, follower, silent, withReplies); + await this.insertFollowingDoc(followee, follower, silent); if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - this.deliverAccept(follower, followee, requestId); + const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); + this.queueService.deliver(followee, content, follower.inbox, false); } } @bindThis private async insertFollowingDoc( followee: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'] + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'] }, follower: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'] + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'] }, silent = false, - withReplies?: boolean, ): Promise { if (follower.id === followee.id) return; let alreadyFollowed = false as boolean; await this.followingsRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), followerId: follower.id, followeeId: followee.id, - withReplies: withReplies, // 非正規化 followerHost: follower.host, @@ -263,31 +206,25 @@ export class UserFollowingService implements OnModuleInit { this.cacheService.userFollowingsCache.refresh(follower.id); - const requestExist = await this.followRequestsRepository.exists({ - where: { - followeeId: followee.id, - followerId: follower.id, - }, + const req = await this.followRequestsRepository.findOneBy({ + followeeId: followee.id, + followerId: follower.id, }); - if (requestExist) { + if (req) { await this.followRequestsRepository.delete({ followeeId: followee.id, followerId: follower.id, }); + + // 通知を作成 + this.notificationService.createNotification(follower.id, 'followRequestAccepted', { + notifierId: followee.id, + }); } if (alreadyFollowed) return; - // 通知を作成 - if (follower.host === null) { - const profile = await this.cacheService.userProfileCache.fetch(followee.id); - - this.notificationService.createNotification(follower.id, 'followRequestAccepted', { - message: profile.followedMessage, - }, followee.id); - } - this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id }); const [followeeUser, followerUser] = await Promise.all([ @@ -305,35 +242,39 @@ export class UserFollowingService implements OnModuleInit { //#endregion //#region Update instance stats - if (this.meta.enableStatsForFederatedInstances) { - if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - this.federatedInstanceService.fetchOrRegister(follower.host).then(async i => { - this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); - if (this.meta.enableChartsForFederatedInstances) { - this.instanceChart.updateFollowing(i.host, true); - } - }); - } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - this.federatedInstanceService.fetchOrRegister(followee.host).then(async i => { - this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); - if (this.meta.enableChartsForFederatedInstances) { - this.instanceChart.updateFollowers(i.host, true); - } - }); - } + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { + this.federatedInstanceService.fetch(follower.host).then(async i => { + this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowing(i.host, true); + } + }); + } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { + this.federatedInstanceService.fetch(followee.host).then(async i => { + this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowers(i.host, true); + } + }); } //#endregion this.perUserFollowingChart.update(follower, followee, true); } + // Publish follow event if (this.userEntityService.isLocalUser(follower) && !silent) { - // Publish follow event this.userEntityService.pack(followee.id, follower, { - schema: 'UserDetailedNotMe', + detail: true, }).then(async packed => { - this.globalEventService.publishMainStream(follower.id, 'follow', packed); - this.webhookService.enqueueUserWebhook(follower.id, 'follow', { user: packed }); + this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'follow', { + user: packed, + }); + } }); } @@ -341,22 +282,29 @@ export class UserFollowingService implements OnModuleInit { if (this.userEntityService.isLocalUser(followee)) { this.userEntityService.pack(follower.id, followee).then(async packed => { this.globalEventService.publishMainStream(followee.id, 'followed', packed); - this.webhookService.enqueueUserWebhook(followee.id, 'followed', { user: packed }); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'followed', { + user: packed, + }); + } }); // 通知を作成 this.notificationService.createNotification(followee.id, 'follow', { - }, follower.id); + notifierId: follower.id, + }); } } @bindThis public async unfollow( follower: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, followee: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, silent = false, ): Promise { @@ -368,7 +316,7 @@ export class UserFollowingService implements OnModuleInit { where: { followerId: follower.id, followeeId: followee.id, - }, + } }); if (following === null || !following.follower || !following.followee) { @@ -382,32 +330,38 @@ export class UserFollowingService implements OnModuleInit { this.decrementFollowing(following.follower, following.followee); + // Publish unfollow event if (!silent && this.userEntityService.isLocalUser(follower)) { - // Publish unfollow event this.userEntityService.pack(followee.id, follower, { - schema: 'UserDetailedNotMe', + detail: true, }).then(async packed => { this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); - this.webhookService.enqueueUserWebhook(follower.id, 'unfollow', { user: packed }); + + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'unfollow', { + user: packed, + }); + } }); } if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower as MiPartialLocalUser, followee as MiPartialRemoteUser), follower)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower as PartialLocalUser, followee as PartialRemoteUser), follower)); this.queueService.deliver(follower, content, followee.inbox, false); } if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { // local user has null host - const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower as MiPartialRemoteUser, followee as MiPartialLocalUser), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower as PartialRemoteUser, followee as PartialLocalUser), followee)); this.queueService.deliver(followee, content, follower.inbox, false); } } @bindThis private async decrementFollowing( - follower: MiUser, - followee: MiUser, + follower: User, + followee: User, ): Promise { this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id }); @@ -421,22 +375,20 @@ export class UserFollowingService implements OnModuleInit { //#endregion //#region Update instance stats - if (this.meta.enableStatsForFederatedInstances) { - if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - this.federatedInstanceService.fetchOrRegister(follower.host).then(async i => { - this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1); - if (this.meta.enableChartsForFederatedInstances) { - this.instanceChart.updateFollowing(i.host, false); - } - }); - } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - this.federatedInstanceService.fetchOrRegister(followee.host).then(async i => { - this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1); - if (this.meta.enableChartsForFederatedInstances) { - this.instanceChart.updateFollowers(i.host, false); - } - }); - } + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { + this.federatedInstanceService.fetch(follower.host).then(async i => { + this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowing(i.host, false); + } + }); + } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { + this.federatedInstanceService.fetch(followee.host).then(async i => { + this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowers(i.host, false); + } + }); } //#endregion @@ -454,8 +406,8 @@ export class UserFollowingService implements OnModuleInit { followerId: user.id, followee: { movedToUri: IsNull(), - }, - }, + } + } }); const nonMovedFollowers = await this.followingsRepository.count({ relations: { @@ -465,8 +417,8 @@ export class UserFollowingService implements OnModuleInit { followeeId: user.id, follower: { movedToUri: IsNull(), - }, - }, + } + } }); await this.usersRepository.update( { id: user.id }, @@ -481,13 +433,12 @@ export class UserFollowingService implements OnModuleInit { @bindThis public async createFollowRequest( follower: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, followee: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, requestId?: string, - withReplies?: boolean, ): Promise { if (follower.id === followee.id) return; @@ -500,18 +451,12 @@ export class UserFollowingService implements OnModuleInit { if (blocking) throw new Error('blocking'); if (blocked) throw new Error('blocked'); - // Remove old follow requests before creating a new one. - await this.followRequestsRepository.delete({ - followeeId: followee.id, - followerId: follower.id, - }); - - const followRequest = await this.followRequestsRepository.insertOne({ - id: this.idService.gen(), + const followRequest = await this.followRequestsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), followerId: follower.id, followeeId: followee.id, requestId, - withReplies, // 非正規化 followerHost: follower.host, @@ -520,23 +465,25 @@ export class UserFollowingService implements OnModuleInit { followeeHost: followee.host, followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined, followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined, - }); + }).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0])); // Publish receiveRequest event if (this.userEntityService.isLocalUser(followee)) { this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed)); this.userEntityService.pack(followee.id, followee, { - schema: 'MeDetailed', + detail: true, }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); // 通知を作成 this.notificationService.createNotification(followee.id, 'receiveFollowRequest', { - }, follower.id); + notifierId: follower.id, + followRequestId: followRequest.id, + }); } if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiPartialLocalUser, followee as MiPartialRemoteUser, requestId ?? `${this.config.url}/follows/${followRequest.id}`)); + const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower as PartialLocalUser, followee as PartialRemoteUser, requestId ?? `${this.config.url}/follows/${followRequest.id}`)); this.queueService.deliver(follower, content, followee.inbox, false); } } @@ -544,28 +491,26 @@ export class UserFollowingService implements OnModuleInit { @bindThis public async cancelFollowRequest( followee: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox'] + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox'] }, follower: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host'] + id: User['id']; host: User['host']; uri: User['host'] }, ): Promise { if (this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower as MiPartialLocalUser | MiPartialRemoteUser, followee as MiPartialRemoteUser), follower)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower as PartialLocalUser | PartialRemoteUser, followee as PartialRemoteUser), follower)); if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので this.queueService.deliver(follower, content, followee.inbox, false); } } - const requestExist = await this.followRequestsRepository.exists({ - where: { - followeeId: followee.id, - followerId: follower.id, - }, + const request = await this.followRequestsRepository.findOneBy({ + followeeId: followee.id, + followerId: follower.id, }); - if (!requestExist) { + if (request == null) { throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); } @@ -575,16 +520,16 @@ export class UserFollowingService implements OnModuleInit { }); this.userEntityService.pack(followee.id, followee, { - schema: 'MeDetailed', + detail: true, }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); } @bindThis public async acceptFollowRequest( followee: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, - follower: MiUser, + follower: User, ): Promise { const request = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, @@ -595,21 +540,22 @@ export class UserFollowingService implements OnModuleInit { throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.'); } - await this.insertFollowingDoc(followee, follower, false, request.withReplies); + await this.insertFollowingDoc(followee, follower); if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - this.deliverAccept(follower, followee as MiPartialLocalUser, request.requestId ?? undefined); + const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as PartialLocalUser, request.requestId!), followee)); + this.queueService.deliver(followee, content, follower.inbox, false); } this.userEntityService.pack(followee.id, followee, { - schema: 'MeDetailed', + detail: true, }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); } @bindThis public async acceptAllFollowRequests( user: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; + id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, ): Promise { const requests = await this.followRequestsRepository.findBy({ @@ -692,7 +638,7 @@ export class UserFollowingService implements OnModuleInit { where: { followeeId: followee.id, followerId: follower.id, - }, + } }); if (!following || !following.followee || !following.follower) return; @@ -722,44 +668,16 @@ export class UserFollowingService implements OnModuleInit { @bindThis private async publishUnfollow(followee: Both, follower: Local): Promise { const packedFollowee = await this.userEntityService.pack(followee.id, follower, { - schema: 'UserDetailedNotMe', + detail: true, }); this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee); - this.webhookService.enqueueUserWebhook(follower.id, 'unfollow', { user: packedFollowee }); - } - @bindThis - public getFollowees(userId: MiUser['id']) { - return this.followingsRepository.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: userId }) - .getMany(); - } - - @bindThis - public isFollowing(followerId: MiUser['id'], followeeId: MiUser['id']) { - return this.followingsRepository.exists({ - where: { - followerId, - followeeId, - }, - }); - } - - @bindThis - public async isMutual(aUserId: MiUser['id'], bUserId: MiUser['id']) { - const count = await this.followingsRepository.createQueryBuilder('following') - .where(new Brackets(qb => { - qb.where('following.followerId = :aUserId', { aUserId }) - .andWhere('following.followeeId = :bUserId', { bUserId }); - })) - .orWhere(new Brackets(qb => { - qb.where('following.followerId = :bUserId', { bUserId }) - .andWhere('following.followeeId = :aUserId', { aUserId }); - })) - .getCount(); - - return count === 2; + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); + for (const webhook of webhooks) { + this.queueService.webhookDeliver(webhook, 'unfollow', { + user: packedFollowee, + }); + } } } diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 92d61cd103..d768f08650 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -1,20 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { MiUser } from '@/models/User.js'; -import type { UserKeypairsRepository } from '@/models/_.js'; +import type { User } from '@/models/entities/User.js'; +import type { UserKeypairsRepository } from '@/models/index.js'; import { RedisKVCache } from '@/misc/cache.js'; -import type { MiUserKeypair } from '@/models/UserKeypair.js'; +import type { UserKeypair } from '@/models/entities/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @Injectable() export class UserKeypairService implements OnApplicationShutdown { - private cache: RedisKVCache; + private cache: RedisKVCache; constructor( @Inject(DI.redis) @@ -23,9 +18,9 @@ export class UserKeypairService implements OnApplicationShutdown { @Inject(DI.userKeypairsRepository) private userKeypairsRepository: UserKeypairsRepository, ) { - this.cache = new RedisKVCache(this.redisClient, 'userKeypair', { + this.cache = new RedisKVCache(this.redisClient, 'userKeypair', { lifetime: 1000 * 60 * 60 * 24, // 24h - memoryCacheLifetime: 1000 * 60 * 60, // 1h + memoryCacheLifetime: Infinity, fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }), toRedisConverter: (value) => JSON.stringify(value), fromRedisConverter: (value) => JSON.parse(value), @@ -33,7 +28,7 @@ export class UserKeypairService implements OnApplicationShutdown { } @bindThis - public async getUserKeypair(userId: MiUser['id']): Promise { + public async getUserKeypair(userId: User['id']): Promise { return await this.cache.fetch(userId); } diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index 61435500b7..08cc907ebf 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -1,159 +1,63 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable, OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import { ModuleRef } from '@nestjs/core'; -import type { UserListMembershipsRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiUserList } from '@/models/UserList.js'; -import type { MiUserListMembership } from '@/models/UserListMembership.js'; +import { Inject, Injectable } from '@nestjs/common'; +import type { UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import type { UserList } from '@/models/entities/UserList.js'; +import type { UserListJoining } from '@/models/entities/UserListJoining.js'; import { IdService } from '@/core/IdService.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { bindThis } from '@/decorators.js'; -import { QueueService } from '@/core/QueueService.js'; -import { RedisKVCache } from '@/misc/cache.js'; import { RoleService } from '@/core/RoleService.js'; -import { SystemAccountService } from '@/core/SystemAccountService.js'; +import { QueueService } from '@/core/QueueService.js'; @Injectable() -export class UserListService implements OnApplicationShutdown, OnModuleInit { +export class UserListService { public static TooManyUsersError = class extends Error {}; - public membersCache: RedisKVCache>; - private roleService: RoleService; - constructor( - private moduleRef: ModuleRef, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, - @Inject(DI.redis) - private redisClient: Redis.Redis, - - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, private userEntityService: UserEntityService, private idService: IdService, + private userFollowingService: UserFollowingService, + private roleService: RoleService, private globalEventService: GlobalEventService, + private proxyAccountService: ProxyAccountService, private queueService: QueueService, - private systemAccountService: SystemAccountService, ) { - this.membersCache = new RedisKVCache>(this.redisClient, 'userListMembers', { - lifetime: 1000 * 60 * 30, // 30m - memoryCacheLifetime: 1000 * 60, // 1m - fetcher: (key) => this.userListMembershipsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))), - toRedisConverter: (value) => JSON.stringify(Array.from(value)), - fromRedisConverter: (value) => new Set(JSON.parse(value)), - }); - - this.redisForSub.on('message', this.onMessage); - } - - async onModuleInit() { - this.roleService = this.moduleRef.get(RoleService.name); } @bindThis - private async onMessage(_: string, data: string): Promise { - const obj = JSON.parse(data); - - if (obj.channel === 'internal') { - const { type, body } = obj.message as GlobalEvents['internal']['payload']; - switch (type) { - case 'userListMemberAdded': { - const { userListId, memberId } = body; - const members = await this.membersCache.get(userListId); - if (members) { - members.add(memberId); - } - break; - } - case 'userListMemberRemoved': { - const { userListId, memberId } = body; - const members = await this.membersCache.get(userListId); - if (members) { - members.delete(memberId); - } - break; - } - default: - break; - } - } - } - - @bindThis - public async addMember(target: MiUser, list: MiUserList, me: MiUser, options: { withReplies?: boolean } = {}) { - const currentCount = await this.userListMembershipsRepository.countBy({ + public async push(target: User, list: UserList, me: User) { + const currentCount = await this.userListJoiningsRepository.countBy({ userListId: list.id, }); - if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { + if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { throw new UserListService.TooManyUsersError(); } - await this.userListMembershipsRepository.insert({ - id: this.idService.gen(), + await this.userListJoiningsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), userId: target.id, userListId: list.id, - userListUserId: list.userId, - withReplies: options.withReplies ?? false, - } as MiUserListMembership); + } as UserListJoining); - this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id }); this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target)); // このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする if (this.userEntityService.isRemoteUser(target)) { - const proxy = await this.systemAccountService.fetch('proxy'); - this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: target.id } }]); + const proxy = await this.proxyAccountService.fetch(); + if (proxy) { + this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: target.id } }]); + } } } - - @bindThis - public async removeMember(target: MiUser, list: MiUserList) { - await this.userListMembershipsRepository.delete({ - userId: target.id, - userListId: list.id, - }); - - this.globalEventService.publishInternalEvent('userListMemberRemoved', { userListId: list.id, memberId: target.id }); - this.globalEventService.publishUserListStream(list.id, 'userRemoved', await this.userEntityService.pack(target)); - } - - @bindThis - public async updateMembership(target: MiUser, list: MiUserList, options: { withReplies?: boolean }) { - const membership = await this.userListMembershipsRepository.findOneBy({ - userId: target.id, - userListId: list.id, - }); - - if (membership == null) { - throw new Error('User is not a member of the list'); - } - - await this.userListMembershipsRepository.update({ - id: membership.id, - }, { - withReplies: options.withReplies, - }); - } - - @bindThis - public dispose(): void { - this.redisForSub.off('message', this.onMessage); - this.membersCache.dispose(); - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); - } } diff --git a/packages/backend/src/core/UserMutingService.ts b/packages/backend/src/core/UserMutingService.ts index 06643be5fb..d201ec6c04 100644 --- a/packages/backend/src/core/UserMutingService.ts +++ b/packages/backend/src/core/UserMutingService.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; -import type { MutingsRepository, MiMuting } from '@/models/_.js'; +import type { MutingsRepository, Muting } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; -import type { MiUser } from '@/models/User.js'; +import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; @@ -24,9 +19,10 @@ export class UserMutingService { } @bindThis - public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise { + public async mute(user: User, target: User, expiresAt: Date | null = null): Promise { await this.mutingsRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), expiresAt: expiresAt ?? null, muterId: user.id, muteeId: target.id, @@ -36,7 +32,7 @@ export class UserMutingService { } @bindThis - public async unmute(mutings: MiMuting[]): Promise { + public async unmute(mutings: Muting[]): Promise { if (mutings.length === 0) return; await this.mutingsRepository.delete({ diff --git a/packages/backend/src/core/UserRenoteMutingService.ts b/packages/backend/src/core/UserRenoteMutingService.ts deleted file mode 100644 index bdc5e23f4b..0000000000 --- a/packages/backend/src/core/UserRenoteMutingService.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; -import type { RenoteMutingsRepository } from '@/models/_.js'; -import type { MiRenoteMuting } from '@/models/RenoteMuting.js'; - -import { IdService } from '@/core/IdService.js'; -import type { MiUser } from '@/models/User.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { CacheService } from '@/core/CacheService.js'; - -@Injectable() -export class UserRenoteMutingService { - constructor( - @Inject(DI.renoteMutingsRepository) - private renoteMutingsRepository: RenoteMutingsRepository, - - private idService: IdService, - private cacheService: CacheService, - ) { - } - - @bindThis - public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise { - await this.renoteMutingsRepository.insert({ - id: this.idService.gen(), - muterId: user.id, - muteeId: target.id, - }); - - await this.cacheService.renoteMutingsCache.refresh(user.id); - } - - @bindThis - public async unmute(mutings: MiRenoteMuting[]): Promise { - if (mutings.length === 0) return; - - await this.renoteMutingsRepository.delete({ - id: In(mutings.map(m => m.id)), - }); - - const muterIds = [...new Set(mutings.map(m => m.muterId))]; - for (const muterId of muterIds) { - await this.cacheService.renoteMutingsCache.refresh(muterId); - } - } -} diff --git a/packages/backend/src/core/UserSearchService.ts b/packages/backend/src/core/UserSearchService.ts deleted file mode 100644 index 4be7bd9bdb..0000000000 --- a/packages/backend/src/core/UserSearchService.ts +++ /dev/null @@ -1,301 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Brackets, SelectQueryBuilder } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import { type FollowingsRepository, MiUser, type MutingsRepository, type UserProfilesRepository, type UsersRepository } from '@/models/_.js'; -import { bindThis } from '@/decorators.js'; -import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; -import type { Config } from '@/config.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { Packed } from '@/misc/json-schema.js'; - -function defaultActiveThreshold() { - return new Date(Date.now() - 1000 * 60 * 60 * 24 * 30); -} - -@Injectable() -export class UserSearchService { - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - - @Inject(DI.mutingsRepository) - private mutingsRepository: MutingsRepository, - - private userEntityService: UserEntityService, - ) { - } - - /** - * ユーザ名とホスト名によるユーザ検索を行う. - * - * - 検索結果には優先順位がつけられており、以下の順序で検索が行われる. - * 1. フォローしているユーザのうち、一定期間以内(※)に更新されたユーザ - * 2. フォローしているユーザのうち、一定期間以内に更新されていないユーザ - * 3. フォローしていないユーザのうち、一定期間以内に更新されたユーザ - * 4. フォローしていないユーザのうち、一定期間以内に更新されていないユーザ - * - ログインしていない場合は、以下の順序で検索が行われる. - * 1. 一定期間以内に更新されたユーザ - * 2. 一定期間以内に更新されていないユーザ - * - それぞれの検索結果はユーザ名の昇順でソートされる. - * - 動作的には先に登場した検索結果の登場位置が優先される(条件的にユーザIDが重複することはないが). - * (1で既にヒットしていた場合、2, 3, 4でヒットしても無視される) - * - ユーザ名とホスト名の検索条件はそれぞれ前方一致で検索される. - * - ユーザ名の検索は大文字小文字を区別しない. - * - ホスト名の検索は大文字小文字を区別しない. - * - 検索結果は最大で {@link opts.limit} 件までとなる. - * - * ※一定期間とは {@link params.activeThreshold} で指定された日時から現在までの期間を指す. - * - * @param params 検索条件. - * @param opts 関数の動作を制御するオプション. - * @param me 検索を実行するユーザの情報. 未ログインの場合は指定しない. - * @see {@link UserSearchService#buildSearchUserQueries} - * @see {@link UserSearchService#buildSearchUserNoLoginQueries} - */ - @bindThis - public async searchByUsernameAndHost( - params: { - username?: string | null, - host?: string | null, - activeThreshold?: Date, - }, - opts?: { - limit?: number, - detail?: boolean, - }, - me?: MiUser | null, - ): Promise[]> { - const queries = me ? this.buildSearchUserQueries(me, params) : this.buildSearchUserNoLoginQueries(params); - - let resultSet = new Set(); - const limit = opts?.limit ?? 10; - for (const query of queries) { - const ids = await query - .select('user.id') - .limit(limit - resultSet.size) - .orderBy('user.usernameLower', 'ASC') - .getRawMany<{ user_id: MiUser['id'] }>() - .then(res => res.map(x => x.user_id)); - - resultSet = new Set([...resultSet, ...ids]); - if (resultSet.size >= limit) { - break; - } - } - - return this.userEntityService.packMany<'UserLite' | 'UserDetailed'>( - [...resultSet].slice(0, limit), - me, - { schema: opts?.detail ? 'UserDetailed' : 'UserLite' }, - ); - } - - /** - * ログイン済みユーザによる検索実行時のクエリ一覧を構築する. - * @param me - * @param params - * @private - */ - @bindThis - private buildSearchUserQueries( - me: MiUser, - params: { - username?: string | null, - host?: string | null, - activeThreshold?: Date, - }, - ) { - // デフォルト30日以内に更新されたユーザーをアクティブユーザーとする - const activeThreshold = params.activeThreshold ?? defaultActiveThreshold(); - - const followingUserQuery = this.followingsRepository.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); - - const activeFollowingUsersQuery = this.generateUserQueryBuilder(params) - .andWhere(`user.id IN (${followingUserQuery.getQuery()})`) - .andWhere('user.updatedAt > :activeThreshold', { activeThreshold }); - activeFollowingUsersQuery.setParameters(followingUserQuery.getParameters()); - - const inactiveFollowingUsersQuery = this.generateUserQueryBuilder(params) - .andWhere(`user.id IN (${followingUserQuery.getQuery()})`) - .andWhere(new Brackets(qb => { - qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt <= :activeThreshold', { activeThreshold }); - })); - inactiveFollowingUsersQuery.setParameters(followingUserQuery.getParameters()); - - // 自分自身がヒットするとしたらここ - const activeUserQuery = this.generateUserQueryBuilder(params) - .andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`) - .andWhere('user.updatedAt > :activeThreshold', { activeThreshold }); - activeUserQuery.setParameters(followingUserQuery.getParameters()); - - const inactiveUserQuery = this.generateUserQueryBuilder(params) - .andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`) - .andWhere('user.updatedAt <= :activeThreshold', { activeThreshold }); - inactiveUserQuery.setParameters(followingUserQuery.getParameters()); - - return [activeFollowingUsersQuery, inactiveFollowingUsersQuery, activeUserQuery, inactiveUserQuery]; - } - - /** - * ログインしていないユーザによる検索実行時のクエリ一覧を構築する. - * @param params - * @private - */ - @bindThis - private buildSearchUserNoLoginQueries(params: { - username?: string | null, - host?: string | null, - activeThreshold?: Date, - }) { - // デフォルト30日以内に更新されたユーザーをアクティブユーザーとする - const activeThreshold = params.activeThreshold ?? defaultActiveThreshold(); - - const activeUserQuery = this.generateUserQueryBuilder(params) - .andWhere(new Brackets(qb => { - qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold }); - })); - - const inactiveUserQuery = this.generateUserQueryBuilder(params) - .andWhere('user.updatedAt <= :activeThreshold', { activeThreshold }); - - return [activeUserQuery, inactiveUserQuery]; - } - - /** - * ユーザ検索クエリで共通する抽出条件をあらかじめ設定したクエリビルダを生成する. - * @param params - * @private - */ - @bindThis - private generateUserQueryBuilder(params: { - username?: string | null, - host?: string | null, - }): SelectQueryBuilder { - const userQuery = this.usersRepository.createQueryBuilder('user'); - - if (params.username) { - userQuery.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(params.username.toLowerCase()) + '%' }); - } - - if (params.host) { - if (params.host === this.config.hostname || params.host === '.') { - userQuery.andWhere('user.host IS NULL'); - } else { - userQuery.andWhere('user.host LIKE :host', { - host: sqlLikeEscape(params.host.toLowerCase()) + '%', - }); - } - } - - userQuery.andWhere('user.isSuspended = FALSE'); - - return userQuery; - } - - @bindThis - public async search(query: string, meId: MiUser['id'] | null, options: Partial<{ - limit: number; - offset: number; - origin: 'local' | 'remote' | 'combined'; - }> = {}) { - const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 - - const isUsername = query.startsWith('@') && !query.includes(' ') && query.indexOf('@', 1) === -1; - - let users: MiUser[] = []; - - const mutingQuery = meId == null ? null : this.mutingsRepository.createQueryBuilder('muting') - .select('muting.muteeId') - .where('muting.muterId = :muterId', { muterId: meId }); - - const nameQuery = this.usersRepository.createQueryBuilder('user') - .where(new Brackets(qb => { - qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(query) + '%' }); - - if (isUsername) { - qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(query.replace('@', '').toLowerCase()) + '%' }); - } else if (this.userEntityService.validateLocalUsername(query)) { // Also search username if it qualifies as username - qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(query.toLowerCase()) + '%' }); - } - })) - .andWhere(new Brackets(qb => { - qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })) - .andWhere('user.isSuspended = FALSE'); - - if (mutingQuery) { - nameQuery.andWhere(`user.id NOT IN (${mutingQuery.getQuery()})`); - nameQuery.setParameters(mutingQuery.getParameters()); - } - - if (options.origin === 'local') { - nameQuery.andWhere('user.host IS NULL'); - } else if (options.origin === 'remote') { - nameQuery.andWhere('user.host IS NOT NULL'); - } - - users = await nameQuery - .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .limit(options.limit) - .offset(options.offset) - .getMany(); - - if (users.length < (options.limit ?? 30)) { - const profQuery = this.userProfilesRepository.createQueryBuilder('prof') - .select('prof.userId') - .where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(query) + '%' }); - - if (mutingQuery) { - profQuery.andWhere(`prof.userId NOT IN (${mutingQuery.getQuery()})`); - profQuery.setParameters(mutingQuery.getParameters()); - } - - if (options.origin === 'local') { - profQuery.andWhere('prof.userHost IS NULL'); - } else if (options.origin === 'remote') { - profQuery.andWhere('prof.userHost IS NOT NULL'); - } - - const userQuery = this.usersRepository.createQueryBuilder('user') - .where(`user.id IN (${ profQuery.getQuery() })`) - .andWhere(new Brackets(qb => { - qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })) - .andWhere('user.isSuspended = FALSE') - .setParameters(profQuery.getParameters()); - - users = users.concat(await userQuery - .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .limit(options.limit) - .offset(options.offset) - .getMany(), - ); - } - - return users; - } -} diff --git a/packages/backend/src/core/UserService.ts b/packages/backend/src/core/UserService.ts deleted file mode 100644 index 1f471513f3..0000000000 --- a/packages/backend/src/core/UserService.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import type { FollowingsRepository, UsersRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; - -@Injectable() -export class UserService { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - private systemWebhookService: SystemWebhookService, - private userEntityService: UserEntityService, - ) { - } - - @bindThis - public async updateLastActiveDate(user: MiUser): Promise { - if (user.isHibernated) { - const result = await this.usersRepository.createQueryBuilder().update() - .set({ - lastActiveDate: new Date(), - }) - .where('id = :id', { id: user.id }) - .returning('*') - .execute() - .then((response) => { - return response.raw[0]; - }); - const wokeUp = result.isHibernated; - if (wokeUp) { - this.usersRepository.update(user.id, { - isHibernated: false, - }); - this.followingsRepository.update({ - followerId: user.id, - }, { - isFollowerHibernated: false, - }); - } - } else { - this.usersRepository.update(user.id, { - lastActiveDate: new Date(), - }); - } - } - - /** - * SystemWebhookを用いてユーザに関する操作内容を管理者各位に通知する. - * ここではJobQueueへのエンキューのみを行うため、即時実行されない. - * - * @see SystemWebhookService.enqueueSystemWebhook - */ - @bindThis - public async notifySystemWebhook(user: MiUser, type: 'userCreated') { - const packedUser = await this.userEntityService.pack(user, null, { schema: 'UserLite' }); - return this.systemWebhookService.enqueueSystemWebhook(type, packedUser); - } -} diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index 7920e58e36..b197d335d8 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -1,93 +1,44 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Not, IsNull } from 'typeorm'; -import type { FollowingsRepository, FollowRequestsRepository, UsersRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; +import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import { RelationshipJobData } from '@/queue/types.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; @Injectable() export class UserSuspendService { constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - @Inject(DI.followRequestsRepository) - private followRequestsRepository: FollowRequestsRepository, - private userEntityService: UserEntityService, private queueService: QueueService, private globalEventService: GlobalEventService, private apRendererService: ApRendererService, - private moderationLogService: ModerationLogService, ) { } @bindThis - public async suspend(user: MiUser, moderator: MiUser): Promise { - await this.usersRepository.update(user.id, { - isSuspended: true, - }); - - this.moderationLogService.log(moderator, 'suspend', { - userId: user.id, - userUsername: user.username, - userHost: user.host, - }); - - (async () => { - await this.postSuspend(user).catch(e => {}); - await this.unFollowAll(user).catch(e => {}); - })(); - } - - @bindThis - public async unsuspend(user: MiUser, moderator: MiUser): Promise { - await this.usersRepository.update(user.id, { - isSuspended: false, - }); - - this.moderationLogService.log(moderator, 'unsuspend', { - userId: user.id, - userUsername: user.username, - userHost: user.host, - }); - - (async () => { - await this.postUnsuspend(user).catch(e => {}); - })(); - } - - @bindThis - private async postSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise { + public async doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); - - this.followRequestsRepository.delete({ - followeeId: user.id, - }); - this.followRequestsRepository.delete({ - followerId: user.id, - }); - + if (this.userEntityService.isLocalUser(user)) { // 知り得る全SharedInboxにDelete配信 const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user)); - + const queue: string[] = []; - + const followings = await this.followingsRepository.find({ where: [ { followerSharedInbox: Not(IsNull()) }, @@ -95,13 +46,13 @@ export class UserSuspendService { ], select: ['followerSharedInbox', 'followeeSharedInbox'], }); - + const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); - + for (const inbox of inboxes) { if (inbox != null && !queue.includes(inbox)) queue.push(inbox); } - + for (const inbox of queue) { this.queueService.deliver(user, content, inbox, true); } @@ -109,15 +60,15 @@ export class UserSuspendService { } @bindThis - private async postUnsuspend(user: MiUser): Promise { + public async doPostUnsuspend(user: User): Promise { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false }); - + if (this.userEntityService.isLocalUser(user)) { // 知り得る全SharedInboxにUndo Delete配信 const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user)); - + const queue: string[] = []; - + const followings = await this.followingsRepository.find({ where: [ { followerSharedInbox: Not(IsNull()) }, @@ -125,38 +76,16 @@ export class UserSuspendService { ], select: ['followerSharedInbox', 'followeeSharedInbox'], }); - + const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); - + for (const inbox of inboxes) { if (inbox != null && !queue.includes(inbox)) queue.push(inbox); } - + for (const inbox of queue) { this.queueService.deliver(user as any, content, inbox, true); } } } - - @bindThis - private async unFollowAll(follower: MiUser) { - const followings = await this.followingsRepository.find({ - where: { - followerId: follower.id, - followeeId: Not(IsNull()), - }, - }); - - const jobs: RelationshipJobData[] = []; - for (const following of followings) { - if (following.followeeId && following.followerId) { - jobs.push({ - from: { id: following.followerId }, - to: { id: following.followeeId }, - silent: true, - }); - } - } - this.queueService.createUnfollowJob(jobs); - } } diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts deleted file mode 100644 index 9b0a598a1b..0000000000 --- a/packages/backend/src/core/UserWebhookService.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import { MiUser, type WebhooksRepository } from '@/models/_.js'; -import { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { GlobalEvents } from '@/core/GlobalEventService.js'; -import type { Packed } from '@/misc/json-schema.js'; -import { QueueService } from '@/core/QueueService.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; - -export type UserWebhookPayload = - T extends 'note' | 'reply' | 'renote' | 'mention' ? { - note: Packed<'Note'>, - } : - T extends 'follow' | 'unfollow' ? { - user: Packed<'UserDetailedNotMe'>, - } : - T extends 'followed' ? { - user: Packed<'UserLite'>, - } : never; - -@Injectable() -export class UserWebhookService implements OnApplicationShutdown { - private activeWebhooksFetched = false; - private activeWebhooks: MiWebhook[] = []; - - constructor( - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - @Inject(DI.webhooksRepository) - private webhooksRepository: WebhooksRepository, - private queueService: QueueService, - ) { - this.redisForSub.on('message', this.onMessage); - } - - @bindThis - public async getActiveWebhooks() { - if (!this.activeWebhooksFetched) { - this.activeWebhooks = await this.webhooksRepository.findBy({ - active: true, - }); - this.activeWebhooksFetched = true; - } - - return this.activeWebhooks; - } - - /** - * UserWebhook の一覧を取得する. - */ - @bindThis - public fetchWebhooks(params?: { - ids?: MiWebhook['id'][]; - isActive?: MiWebhook['active']; - on?: MiWebhook['on']; - }): Promise { - const query = this.webhooksRepository.createQueryBuilder('webhook'); - if (params) { - if (params.ids && params.ids.length > 0) { - query.andWhere('webhook.id IN (:...ids)', { ids: params.ids }); - } - if (params.isActive !== undefined) { - query.andWhere('webhook.active = :isActive', { isActive: params.isActive }); - } - if (params.on && params.on.length > 0) { - query.andWhere(':on <@ webhook.on', { on: params.on }); - } - } - - return query.getMany(); - } - - /** - * UserWebhook をWebhook配送キューに追加する - * @see QueueService.userWebhookDeliver - */ - @bindThis - public async enqueueUserWebhook( - userId: MiUser['id'], - type: T, - content: UserWebhookPayload, - ) { - const webhooks = await this.getActiveWebhooks() - .then(webhooks => webhooks.filter(webhook => webhook.userId === userId && webhook.on.includes(type))); - return Promise.all( - webhooks.map(webhook => { - return this.queueService.userWebhookDeliver(webhook, type, content); - }), - ); - } - - @bindThis - private async onMessage(_: string, data: string): Promise { - const obj = JSON.parse(data); - if (obj.channel !== 'internal') { - return; - } - - const { type, body } = obj.message as GlobalEvents['internal']['payload']; - switch (type) { - case 'webhookCreated': { - if (body.active) { - this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい - ...body, - latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, - user: null, // joinなカラムは通常取ってこないので - }); - } - break; - } - case 'webhookUpdated': { - if (body.active) { - const i = this.activeWebhooks.findIndex(a => a.id === body.id); - if (i > -1) { - this.activeWebhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい - ...body, - latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, - user: null, // joinなカラムは通常取ってこないので - }; - } else { - this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい - ...body, - latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, - user: null, // joinなカラムは通常取ってこないので - }); - } - } else { - this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id); - } - break; - } - case 'webhookDeleted': { - this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id); - break; - } - default: - break; - } - } - - @bindThis - public dispose(): void { - this.redisForSub.off('message', this.onMessage); - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); - } -} diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 67ec6cc7b0..d00708a442 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -1,26 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { URL, domainToASCII } from 'node:url'; +import { URL } from 'node:url'; +import { toASCII } from 'punycode'; import { Inject, Injectable } from '@nestjs/common'; -import RE2 from 're2'; -import semver from 'semver'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; -import { MiMeta, SoftwareSuspension } from '@/models/Meta.js'; -import { MiInstance } from '@/models/Instance.js'; @Injectable() export class UtilityService { constructor( @Inject(DI.config) private config: Config, - - @Inject(DI.meta) - private meta: MiMeta, ) { } @@ -35,130 +24,26 @@ export class UtilityService { return this.toPuny(this.config.host) === this.toPuny(host); } - @bindThis - public isUriLocal(uri: string): boolean { - return this.punyHost(uri) === this.toPuny(this.config.host); - } - - // メールアドレスのバリデーションを行う - // https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address - @bindThis - public validateEmailFormat(email: string): boolean { - const regexp = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; - return regexp.test(email); - } - @bindThis public isBlockedHost(blockedHosts: string[], host: string | null): boolean { if (host == null) return false; return blockedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); } - @bindThis - public isSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean { - if (!silencedHosts || host == null) return false; - return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); - } - - @bindThis - public isMediaSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean { - if (!silencedHosts || host == null) return false; - return silencedHosts.some(x => host.toLowerCase() === x); - } - - @bindThis - public concatNoteContentsForKeyWordCheck(content: { - cw?: string | null; - text?: string | null; - pollChoices?: string[] | null; - others?: string[] | null; - }): string { - /** - * ノートの内容を結合してキーワードチェック用の文字列を生成する - * cwとtextは内容が繋がっているかもしれないので間に何も入れずにチェックする - */ - return `${content.cw ?? ''}${content.text ?? ''}\n${(content.pollChoices ?? []).join('\n')}\n${(content.others ?? []).join('\n')}`; - } - - @bindThis - public isKeyWordIncluded(text: string, keyWords: string[]): boolean { - if (keyWords.length === 0) return false; - if (text === '') return false; - - const regexpregexp = /^\/(.+)\/(.*)$/; - - const matched = keyWords.some(filter => { - // represents RegExp - const regexp = filter.match(regexpregexp); - // This should never happen due to input sanitisation. - if (!regexp) { - const words = filter.split(' '); - return words.every(keyword => text.includes(keyword)); - } - try { - // TODO: RE2インスタンスをキャッシュ - return new RE2(regexp[1], regexp[2]).test(text); - } catch (err) { - // This should never happen due to input sanitisation. - return false; - } - }); - - return matched; - } - @bindThis public extractDbHost(uri: string): string { const url = new URL(uri); - return this.toPuny(url.host); + return this.toPuny(url.hostname); } @bindThis public toPuny(host: string): string { - return domainToASCII(host.toLowerCase()); + return toASCII(host.toLowerCase()); } @bindThis public toPunyNullable(host: string | null | undefined): string | null { if (host == null) return null; - return domainToASCII(host.toLowerCase()); - } - - @bindThis - public punyHost(url: string): string { - const urlObj = new URL(url); - const host = `${this.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`; - return host; - } - - @bindThis - public isFederationAllowedHost(host: string): boolean { - if (this.meta.federation === 'none') return false; - if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false; - if (this.isBlockedHost(this.meta.blockedHosts, host)) return false; - - return true; - } - - @bindThis - public isFederationAllowedUri(uri: string): boolean { - const host = this.extractDbHost(uri); - return this.isFederationAllowedHost(host); - } - - @bindThis - public isDeliverSuspendedSoftware(software: Pick): SoftwareSuspension | undefined { - if (software.softwareName == null) return undefined; - if (software.softwareVersion == null) { - // software version is null; suspend iff versionRange is * - return this.meta.deliverSuspendedSoftware.find(x => - x.software === software.softwareName - && x.versionRange.trim() === '*'); - } else { - const softwareVersion = software.softwareVersion; - return this.meta.deliverSuspendedSoftware.find(x => - x.software === software.softwareName - && semver.satisfies(softwareVersion, x.versionRange, { includePrerelease: true })); - } + return toASCII(host.toLowerCase()); } } diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index 747fe4fc7e..5869905db0 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import FFmpeg from 'fluent-ffmpeg'; import { DI } from '@/di-symbols.js'; @@ -26,7 +21,7 @@ export class VideoProcessingService { @bindThis public async generateVideoThumbnail(source: string): Promise { const [dir, cleanup] = await createTempDir(); - + try { await new Promise((res, rej) => { FFmpeg({ @@ -57,7 +52,7 @@ export class VideoProcessingService { query({ thumbnail: '1', url, - }), + }) ); } } diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts deleted file mode 100644 index 372e1e2ab7..0000000000 --- a/packages/backend/src/core/WebAuthnService.ts +++ /dev/null @@ -1,326 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import { - generateAuthenticationOptions, - generateRegistrationOptions, verifyAuthenticationResponse, - verifyRegistrationResponse, -} from '@simplewebauthn/server'; -import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers'; -import { DI } from '@/di-symbols.js'; -import type { MiMeta, UserSecurityKeysRepository } from '@/models/_.js'; -import type { Config } from '@/config.js'; -import { bindThis } from '@/decorators.js'; -import { MiUser } from '@/models/_.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import type { - AuthenticationResponseJSON, - AuthenticatorTransportFuture, - CredentialDeviceType, - PublicKeyCredentialCreationOptionsJSON, - PublicKeyCredentialRequestOptionsJSON, - RegistrationResponseJSON, -} from '@simplewebauthn/types'; - -@Injectable() -export class WebAuthnService { - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.meta) - private meta: MiMeta, - - @Inject(DI.redis) - private redisClient: Redis.Redis, - - @Inject(DI.userSecurityKeysRepository) - private userSecurityKeysRepository: UserSecurityKeysRepository, - ) { - } - - @bindThis - public getRelyingParty(): { origin: string; rpId: string; rpName: string; rpIcon?: string; } { - return { - origin: this.config.url, - rpId: this.config.hostname, - rpName: this.meta.name ?? this.config.host, - rpIcon: this.meta.iconUrl ?? undefined, - }; - } - - @bindThis - public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise { - const relyingParty = this.getRelyingParty(); - const keys = await this.userSecurityKeysRepository.findBy({ - userId: userId, - }); - - const registrationOptions = await generateRegistrationOptions({ - rpName: relyingParty.rpName, - rpID: relyingParty.rpId, - userID: isoUint8Array.fromUTF8String(userId), - userName: userName, - userDisplayName: userDisplayName, - attestationType: 'indirect', - excludeCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{ - id: key.id, - transports: key.transports ?? undefined, - })), - authenticatorSelection: { - residentKey: 'required', - userVerification: 'preferred', - }, - }); - - await this.redisClient.setex(`webauthn:challenge:${userId}`, 90, registrationOptions.challenge); - - return registrationOptions; - } - - @bindThis - public async verifyRegistration(userId: MiUser['id'], response: RegistrationResponseJSON): Promise<{ - credentialID: string; - credentialPublicKey: Uint8Array; - attestationObject: Uint8Array; - fmt: AttestationFormat; - counter: number; - userVerified: boolean; - credentialDeviceType: CredentialDeviceType; - credentialBackedUp: boolean; - transports?: AuthenticatorTransportFuture[]; - }> { - const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`); - - if (!challenge) { - throw new IdentifiableError('7dbfb66c-9216-4e2b-9c27-cef2ac8efb84', 'challenge not found'); - } - - await this.redisClient.del(`webauthn:challenge:${userId}`); - - const relyingParty = this.getRelyingParty(); - - let verification; - try { - verification = await verifyRegistrationResponse({ - response: response, - expectedChallenge: challenge, - expectedOrigin: relyingParty.origin, - expectedRPID: relyingParty.rpId, - requireUserVerification: true, - }); - } catch (error) { - console.error(error); - throw new IdentifiableError('5c1446f8-8ca7-4d31-9f39-656afe9c5d87', 'verification failed'); - } - - const { verified } = verification; - - if (!verified || !verification.registrationInfo) { - throw new IdentifiableError('bb333667-3832-4a80-8bb5-c505be7d710d', 'verification failed'); - } - - const { registrationInfo } = verification; - - return { - credentialID: registrationInfo.credential.id, - credentialPublicKey: registrationInfo.credential.publicKey, - attestationObject: registrationInfo.attestationObject, - fmt: registrationInfo.fmt, - counter: registrationInfo.credential.counter, - userVerified: registrationInfo.userVerified, - credentialDeviceType: registrationInfo.credentialDeviceType, - credentialBackedUp: registrationInfo.credentialBackedUp, - transports: response.response.transports, - }; - } - - @bindThis - public async initiateAuthentication(userId: MiUser['id']): Promise { - const relyingParty = this.getRelyingParty(); - const keys = await this.userSecurityKeysRepository.findBy({ - userId: userId, - }); - - if (keys.length === 0) { - throw new IdentifiableError('f27fd449-9af4-4841-9249-1f989b9fa4a4', 'no keys found'); - } - - const authenticationOptions = await generateAuthenticationOptions({ - rpID: relyingParty.rpId, - allowCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{ - id: key.id, - transports: key.transports ?? undefined, - })), - userVerification: 'preferred', - }); - - await this.redisClient.setex(`webauthn:challenge:${userId}`, 90, authenticationOptions.challenge); - - return authenticationOptions; - } - - /** - * Initiate Passkey Auth (Without specifying user) - * @returns authenticationOptions - */ - @bindThis - public async initiateSignInWithPasskeyAuthentication(context: string): Promise { - const relyingParty = await this.getRelyingParty(); - - const authenticationOptions = await generateAuthenticationOptions({ - rpID: relyingParty.rpId, - userVerification: 'preferred', - }); - - await this.redisClient.setex(`webauthn:challenge:${context}`, 90, authenticationOptions.challenge); - - return authenticationOptions; - } - - /** - * Verify Webauthn AuthenticationCredential - * @throws IdentifiableError - * @returns If the challenge is successful, return the user ID. Otherwise, return null. - */ - @bindThis - public async verifySignInWithPasskeyAuthentication(context: string, response: AuthenticationResponseJSON): Promise { - const challenge = await this.redisClient.getdel(`webauthn:challenge:${context}`); - - if (!challenge) { - throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', `challenge '${context}' not found`); - } - - const key = await this.userSecurityKeysRepository.findOneBy({ - id: response.id, - }); - - if (!key) { - throw new IdentifiableError('36b96a7d-b547-412d-aeed-2d611cdc8cdc', 'Unknown Webauthn key'); - } - - const relyingParty = await this.getRelyingParty(); - - let verification; - try { - verification = await verifyAuthenticationResponse({ - response: response, - expectedChallenge: challenge, - expectedOrigin: relyingParty.origin, - expectedRPID: relyingParty.rpId, - credential: { - id: key.id, - publicKey: Buffer.from(key.publicKey, 'base64url'), - counter: key.counter, - transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined, - }, - requireUserVerification: true, - }); - } catch (error) { - throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', `verification failed: ${error}`); - } - - const { verified, authenticationInfo } = verification; - - if (!verified) { - return null; - } - - await this.userSecurityKeysRepository.update({ - id: response.id, - }, { - lastUsed: new Date(), - counter: authenticationInfo.newCounter, - credentialDeviceType: authenticationInfo.credentialDeviceType, - credentialBackedUp: authenticationInfo.credentialBackedUp, - }); - - return key.userId; - } - - @bindThis - public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise { - const challenge = await this.redisClient.getdel(`webauthn:challenge:${userId}`); - - if (!challenge) { - throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', 'challenge not found'); - } - - const key = await this.userSecurityKeysRepository.findOneBy({ - id: response.id, - userId: userId, - }); - - if (!key) { - throw new IdentifiableError('36b96a7d-b547-412d-aeed-2d611cdc8cdc', 'unknown key'); - } - - // マイグレーション - if (key.counter === 0 && key.publicKey.length === 87) { - const cert = new Uint8Array(Buffer.from(key.publicKey, 'base64url')); - if (cert[0] === 0x04) { // 前の実装ではいつも 0x04 で始まっていた - const halfLength = (cert.length - 1) / 2; - - const cborMap = new Map(); - cborMap.set(1, 2); // kty, EC2 - cborMap.set(3, -7); // alg, ES256 - cborMap.set(-1, 1); // crv, P256 - cborMap.set(-2, cert.slice(1, halfLength + 1)); // x - cborMap.set(-3, cert.slice(halfLength + 1)); // y - - const cborPubKey = Buffer.from(isoCBOR.encode(cborMap)).toString('base64url'); - await this.userSecurityKeysRepository.update({ - id: response.id, - userId: userId, - }, { - publicKey: cborPubKey, - }); - key.publicKey = cborPubKey; - } - } - - const relyingParty = this.getRelyingParty(); - - let verification; - try { - verification = await verifyAuthenticationResponse({ - response: response, - expectedChallenge: challenge, - expectedOrigin: relyingParty.origin, - expectedRPID: relyingParty.rpId, - credential: { - id: key.id, - publicKey: Buffer.from(key.publicKey, 'base64url'), - counter: key.counter, - transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined, - }, - requireUserVerification: true, - }); - } catch (error) { - console.error(error); - throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', 'verification failed'); - } - - const { verified, authenticationInfo } = verification; - - if (!verified) { - return false; - } - - await this.userSecurityKeysRepository.update({ - id: response.id, - userId: userId, - }, { - lastUsed: new Date(), - counter: authenticationInfo.newCounter, - credentialDeviceType: authenticationInfo.credentialDeviceType, - credentialBackedUp: authenticationInfo.credentialBackedUp, - }); - - return verified; - } -} diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts index 374536a741..f58a6a10fc 100644 --- a/packages/backend/src/core/WebfingerService.ts +++ b/packages/backend/src/core/WebfingerService.ts @@ -1,20 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { URL } from 'node:url'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; import { query as urlQuery } from '@/misc/prelude/url.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; -export type ILink = { +type ILink = { href: string; rel?: string; }; -export type IWebFinger = { +type IWebFinger = { links: ILink[]; subject: string; }; @@ -25,6 +22,9 @@ const mRegex = /^([^@]+)@(.*)/; @Injectable() export class WebfingerService { constructor( + @Inject(DI.config) + private config: Config, + private httpRequestService: HttpRequestService, ) { } diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts new file mode 100644 index 0000000000..467755a072 --- /dev/null +++ b/packages/backend/src/core/WebhookService.ts @@ -0,0 +1,92 @@ +import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; +import type { WebhooksRepository } from '@/models/index.js'; +import type { Webhook } from '@/models/entities/Webhook.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +@Injectable() +export class WebhookService implements OnApplicationShutdown { + private webhooksFetched = false; + private webhooks: Webhook[] = []; + + constructor( + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, + + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + ) { + //this.onMessage = this.onMessage.bind(this); + this.redisForSub.on('message', this.onMessage); + } + + @bindThis + public async getActiveWebhooks() { + if (!this.webhooksFetched) { + this.webhooks = await this.webhooksRepository.findBy({ + active: true, + }); + this.webhooksFetched = true; + } + + return this.webhooks; + } + + @bindThis + private async onMessage(_: string, data: string): Promise { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message as StreamMessages['internal']['payload']; + switch (type) { + case 'webhookCreated': + if (body.active) { + this.webhooks.push({ + ...body, + createdAt: new Date(body.createdAt), + latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, + }); + } + break; + case 'webhookUpdated': + if (body.active) { + const i = this.webhooks.findIndex(a => a.id === body.id); + if (i > -1) { + this.webhooks[i] = { + ...body, + createdAt: new Date(body.createdAt), + latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, + }; + } else { + this.webhooks.push({ + ...body, + createdAt: new Date(body.createdAt), + latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, + }); + } + } else { + this.webhooks = this.webhooks.filter(a => a.id !== body.id); + } + break; + case 'webhookDeleted': + this.webhooks = this.webhooks.filter(a => a.id !== body.id); + break; + default: + break; + } + } + } + + @bindThis + public dispose(): void { + this.redisForSub.off('message', this.onMessage); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } +} diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts deleted file mode 100644 index 9cf985b688..0000000000 --- a/packages/backend/src/core/WebhookTestService.ts +++ /dev/null @@ -1,486 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js'; -import { bindThis } from '@/decorators.js'; -import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js'; -import { type AbuseReportPayload, SystemWebhookPayload, SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { type Packed } from '@/misc/json-schema.js'; -import { type WebhookEventTypes } from '@/models/Webhook.js'; -import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js'; -import { QueueService } from '@/core/QueueService.js'; -import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; - -const oneDayMillis = 24 * 60 * 60 * 1000; - -function generateDummyUser(override?: Partial): MiUser { - return { - id: 'dummy-user-1', - updatedAt: new Date(Date.now() - oneDayMillis * 7), - lastFetchedAt: new Date(Date.now() - oneDayMillis * 5), - lastActiveDate: new Date(Date.now() - oneDayMillis * 3), - hideOnlineStatus: false, - username: 'dummy1', - usernameLower: 'dummy1', - name: 'DummyUser1', - followersCount: 10, - followingCount: 5, - movedToUri: null, - movedAt: null, - alsoKnownAs: null, - notesCount: 30, - avatarId: null, - avatar: null, - bannerId: null, - banner: null, - avatarUrl: null, - bannerUrl: null, - avatarBlurhash: null, - bannerBlurhash: null, - avatarDecorations: [], - tags: [], - isSuspended: false, - isLocked: false, - isBot: false, - isCat: true, - isExplorable: true, - isHibernated: false, - isDeleted: false, - requireSigninToViewContents: false, - makeNotesFollowersOnlyBefore: null, - makeNotesHiddenBefore: null, - chatScope: 'mutual', - emojis: [], - score: 0, - host: null, - inbox: null, - sharedInbox: null, - featured: null, - uri: null, - followersUri: null, - token: null, - ...override, - }; -} - -function generateDummyNote(override?: Partial): MiNote { - return { - id: 'dummy-note-1', - replyId: null, - reply: null, - renoteId: null, - renote: null, - threadId: null, - text: 'This is a dummy note for testing purposes.', - name: null, - cw: null, - userId: 'dummy-user-1', - user: null, - localOnly: true, - reactionAcceptance: 'likeOnly', - renoteCount: 10, - repliesCount: 5, - clippedCount: 0, - reactions: {}, - visibility: 'public', - uri: null, - url: null, - fileIds: [], - attachedFileTypes: [], - visibleUserIds: [], - mentions: [], - mentionedRemoteUsers: '[]', - reactionAndUserPairCache: [], - emojis: [], - tags: [], - hasPoll: false, - channelId: null, - channel: null, - userHost: null, - replyUserId: null, - replyUserHost: null, - renoteUserId: null, - renoteUserHost: null, - ...override, - }; -} - -const dummyUser1 = generateDummyUser(); -const dummyUser2 = generateDummyUser({ - id: 'dummy-user-2', - updatedAt: new Date(Date.now() - oneDayMillis * 30), - lastFetchedAt: new Date(Date.now() - oneDayMillis), - lastActiveDate: new Date(Date.now() - oneDayMillis), - username: 'dummy2', - usernameLower: 'dummy2', - name: 'DummyUser2', - followersCount: 40, - followingCount: 50, - notesCount: 900, -}); -const dummyUser3 = generateDummyUser({ - id: 'dummy-user-3', - updatedAt: new Date(Date.now() - oneDayMillis * 15), - lastFetchedAt: new Date(Date.now() - oneDayMillis * 2), - lastActiveDate: new Date(Date.now() - oneDayMillis * 2), - username: 'dummy3', - usernameLower: 'dummy3', - name: 'DummyUser3', - followersCount: 60, - followingCount: 70, - notesCount: 15900, -}); - -@Injectable() -export class WebhookTestService { - public static NoSuchWebhookError = class extends Error { - }; - - constructor( - private customEmojiService: CustomEmojiService, - private userWebhookService: UserWebhookService, - private systemWebhookService: SystemWebhookService, - private queueService: QueueService, - ) { - } - - /** - * UserWebhookのテスト送信を行う. - * 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない. - * - * また、この関数経由で送信されるWebhookは以下の設定を無視する. - * - Webhookそのものの有効・無効設定(active) - * - 送信対象イベント(on)に関する設定 - */ - @bindThis - public async testUserWebhook( - params: { - webhookId: MiWebhook['id'], - type: T, - override?: Partial>, - }, - sender: MiUser | null, - ) { - const webhooks = await this.userWebhookService.fetchWebhooks({ ids: [params.webhookId] }) - .then(it => it.filter(it => it.userId === sender?.id)); - if (webhooks.length === 0) { - throw new WebhookTestService.NoSuchWebhookError(); - } - - const webhook = webhooks[0]; - const send = (type: U, contents: UserWebhookPayload) => { - const merged = { - ...webhook, - ...params.override, - }; - - // テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図). - // また、Jobの試行回数も1回だけ. - this.queueService.userWebhookDeliver(merged, type, contents, { attempts: 1 }); - }; - - const dummyNote1 = generateDummyNote({ - userId: dummyUser1.id, - user: dummyUser1, - }); - const dummyReply1 = generateDummyNote({ - id: 'dummy-reply-1', - replyId: dummyNote1.id, - reply: dummyNote1, - userId: dummyUser1.id, - user: dummyUser1, - }); - const dummyRenote1 = generateDummyNote({ - id: 'dummy-renote-1', - renoteId: dummyNote1.id, - renote: dummyNote1, - userId: dummyUser2.id, - user: dummyUser2, - text: null, - }); - const dummyMention1 = generateDummyNote({ - id: 'dummy-mention-1', - userId: dummyUser1.id, - user: dummyUser1, - text: `@${dummyUser2.username} This is a mention to you.`, - mentions: [dummyUser2.id], - }); - - switch (params.type) { - case 'note': { - send('note', { note: await this.toPackedNote(dummyNote1) }); - break; - } - case 'reply': { - send('reply', { note: await this.toPackedNote(dummyReply1) }); - break; - } - case 'renote': { - send('renote', { note: await this.toPackedNote(dummyRenote1) }); - break; - } - case 'mention': { - send('mention', { note: await this.toPackedNote(dummyMention1) }); - break; - } - case 'follow': { - send('follow', { user: await this.toPackedUserDetailedNotMe(dummyUser1) }); - break; - } - case 'followed': { - send('followed', { user: await this.toPackedUserLite(dummyUser2) }); - break; - } - case 'unfollow': { - send('unfollow', { user: await this.toPackedUserDetailedNotMe(dummyUser3) }); - break; - } - // まだ実装されていない (#9485) - case 'reaction': - return; - default: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _exhaustiveAssertion: never = params.type; - return; - } - } - } - - /** - * SystemWebhookのテスト送信を行う. - * 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない. - * - * また、この関数経由で送信されるWebhookは以下の設定を無視する. - * - Webhookそのものの有効・無効設定(isActive) - * - 送信対象イベント(on)に関する設定 - */ - @bindThis - public async testSystemWebhook( - params: { - webhookId: MiSystemWebhook['id'], - type: T, - override?: Partial>, - }, - ) { - const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [params.webhookId] }); - if (webhooks.length === 0) { - throw new WebhookTestService.NoSuchWebhookError(); - } - - const webhook = webhooks[0]; - const send = (type: U, contents: SystemWebhookPayload) => { - const merged = { - ...webhook, - ...params.override, - }; - - // テスト目的なのでSystemWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図). - // また、Jobの試行回数も1回だけ. - this.queueService.systemWebhookDeliver(merged, type, contents, { attempts: 1 }); - }; - - switch (params.type) { - case 'abuseReport': { - send('abuseReport', await this.generateAbuseReport({ - targetUserId: dummyUser1.id, - targetUser: dummyUser1, - reporterId: dummyUser2.id, - reporter: dummyUser2, - })); - break; - } - case 'abuseReportResolved': { - send('abuseReportResolved', await this.generateAbuseReport({ - targetUserId: dummyUser1.id, - targetUser: dummyUser1, - reporterId: dummyUser2.id, - reporter: dummyUser2, - assigneeId: dummyUser3.id, - assignee: dummyUser3, - resolved: true, - })); - break; - } - case 'userCreated': { - send('userCreated', await this.toPackedUserLite(dummyUser1)); - break; - } - case 'inactiveModeratorsWarning': { - const dummyTime: ModeratorInactivityRemainingTime = { - time: 100000, - asDays: 1, - asHours: 24, - }; - - send('inactiveModeratorsWarning', { - remainingTime: dummyTime, - }); - break; - } - case 'inactiveModeratorsInvitationOnlyChanged': { - send('inactiveModeratorsInvitationOnlyChanged', {}); - break; - } - default: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _exhaustiveAssertion: never = params.type; - return; - } - } - } - - @bindThis - private async generateAbuseReport(override?: Partial): Promise { - const result: MiAbuseUserReport = { - id: 'dummy-abuse-report1', - targetUserId: 'dummy-target-user', - targetUser: null, - reporterId: 'dummy-reporter-user', - reporter: null, - assigneeId: null, - assignee: null, - resolved: false, - forwarded: false, - comment: 'This is a dummy report for testing purposes.', - targetUserHost: null, - reporterHost: null, - resolvedAs: null, - moderationNote: 'foo', - ...override, - }; - - return { - ...result, - targetUser: result.targetUser ? await this.toPackedUserLite(result.targetUser) : null, - reporter: result.reporter ? await this.toPackedUserLite(result.reporter) : null, - assignee: result.assignee ? await this.toPackedUserLite(result.assignee) : null, - }; - } - - @bindThis - private async toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Promise> { - return { - id: note.id, - createdAt: new Date().toISOString(), - deletedAt: null, - text: note.text, - cw: note.cw, - userId: note.userId, - user: await this.toPackedUserLite(note.user ?? generateDummyUser()), - replyId: note.replyId, - renoteId: note.renoteId, - isHidden: false, - visibility: note.visibility, - mentions: note.mentions, - visibleUserIds: note.visibleUserIds, - fileIds: note.fileIds, - files: [], - tags: note.tags, - poll: null, - emojis: await this.customEmojiService.populateEmojis(note.emojis, note.userHost), - channelId: note.channelId, - channel: note.channel, - localOnly: note.localOnly, - reactionAcceptance: note.reactionAcceptance, - reactionEmojis: {}, - reactions: {}, - reactionCount: 0, - renoteCount: note.renoteCount, - repliesCount: note.repliesCount, - uri: note.uri ?? undefined, - url: note.url ?? undefined, - reactionAndUserPairCache: note.reactionAndUserPairCache, - ...(detail ? { - clippedCount: note.clippedCount, - reply: note.reply ? await this.toPackedNote(note.reply, false) : null, - renote: note.renote ? await this.toPackedNote(note.renote, true) : null, - myReaction: null, - } : {}), - ...override, - }; - } - - @bindThis - private async toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Promise> { - return { - id: user.id, - name: user.name, - username: user.username, - host: user.host, - avatarUrl: user.avatarId == null ? null : user.avatarUrl, - avatarBlurhash: user.avatarId == null ? null : user.avatarBlurhash, - avatarDecorations: user.avatarDecorations.map(it => ({ - id: it.id, - angle: it.angle, - flipH: it.flipH, - url: 'https://example.com/dummy-image001.png', - offsetX: it.offsetX, - offsetY: it.offsetY, - })), - isBot: user.isBot, - isCat: user.isCat, - emojis: await this.customEmojiService.populateEmojis(user.emojis, user.host), - onlineStatus: 'active', - badgeRoles: [], - ...override, - }; - } - - @bindThis - private async toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Promise> { - return { - ...await this.toPackedUserLite(user), - url: null, - uri: null, - movedTo: null, - alsoKnownAs: [], - createdAt: new Date().toISOString(), - updatedAt: user.updatedAt?.toISOString() ?? null, - lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null, - bannerUrl: user.bannerId == null ? null : user.bannerUrl, - bannerBlurhash: user.bannerId == null ? null : user.bannerBlurhash, - isLocked: user.isLocked, - isSilenced: false, - isSuspended: user.isSuspended, - description: null, - location: null, - birthday: null, - lang: null, - fields: [], - verifiedLinks: [], - followersCount: user.followersCount, - followingCount: user.followingCount, - notesCount: user.notesCount, - pinnedNoteIds: [], - pinnedNotes: [], - pinnedPageId: null, - pinnedPage: null, - publicReactions: true, - followersVisibility: 'public', - followingVisibility: 'public', - chatScope: 'mutual', - canChat: true, - twoFactorEnabled: false, - usePasswordLessLogin: false, - securityKeys: false, - roles: [], - memo: null, - moderationNote: undefined, - isFollowing: false, - isFollowed: false, - hasPendingFollowRequestFromYou: false, - hasPendingFollowRequestToYou: false, - isBlocking: false, - isBlocked: false, - isMuted: false, - isRenoteMuted: false, - notify: 'none', - withReplies: true, - ...override, - }; - } -} diff --git a/packages/backend/src/core/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts index 5a5a76f7d6..8282a6324c 100644 --- a/packages/backend/src/core/activitypub/ApAudienceService.ts +++ b/packages/backend/src/core/activitypub/ApAudienceService.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; -import type { MiRemoteUser, MiUser } from '@/models/User.js'; +import type { RemoteUser, User } from '@/models/entities/User.js'; import { concat, unique } from '@/misc/prelude/array.js'; import { bindThis } from '@/decorators.js'; import { getApIds } from './type.js'; @@ -17,12 +12,10 @@ type Visibility = 'public' | 'home' | 'followers' | 'specified'; type AudienceInfo = { visibility: Visibility, - mentionedUsers: MiUser[], - visibleUsers: MiUser[], + mentionedUsers: User[], + visibleUsers: User[], }; -type GroupedAudience = Record<'public' | 'followers' | 'other', string[]>; - @Injectable() export class ApAudienceService { constructor( @@ -31,17 +24,17 @@ export class ApAudienceService { } @bindThis - public async parseAudience(actor: MiRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { + public async parseAudience(actor: RemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { const toGroups = this.groupingAudience(getApIds(to), actor); const ccGroups = this.groupingAudience(getApIds(cc), actor); - + const others = unique(concat([toGroups.other, ccGroups.other])); - - const limit = promiseLimit(2); + + const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), - )).filter(x => x != null); - + )).filter((x): x is User => x != null); + if (toGroups.public.length > 0) { return { visibility: 'public', @@ -49,7 +42,7 @@ export class ApAudienceService { visibleUsers: [], }; } - + if (ccGroups.public.length > 0) { return { visibility: 'home', @@ -57,30 +50,30 @@ export class ApAudienceService { visibleUsers: [], }; } - - if (toGroups.followers.length > 0 || ccGroups.followers.length > 0) { + + if (toGroups.followers.length > 0) { return { visibility: 'followers', mentionedUsers, visibleUsers: [], }; } - + return { visibility: 'specified', mentionedUsers, visibleUsers: mentionedUsers, }; } - + @bindThis - private groupingAudience(ids: string[], actor: MiRemoteUser): GroupedAudience { - const groups: GroupedAudience = { - public: [], - followers: [], - other: [], + private groupingAudience(ids: string[], actor: RemoteUser) { + const groups = { + public: [] as string[], + followers: [] as string[], + other: [] as string[], }; - + for (const id of ids) { if (this.isPublic(id)) { groups.public.push(id); @@ -90,23 +83,25 @@ export class ApAudienceService { groups.other.push(id); } } - + groups.other = unique(groups.other); - + return groups; } - + @bindThis - private isPublic(id: string): boolean { + private isPublic(id: string) { return [ 'https://www.w3.org/ns/activitystreams#Public', - 'as:Public', + 'as#Public', 'Public', ].includes(id); } - + @bindThis - private isFollowers(id: string, actor: MiRemoteUser): boolean { - return id === (actor.followersUri ?? `${actor.uri}/followers`); + private isFollowers(id: string, actor: RemoteUser) { + return ( + id === (actor.followersUri ?? `${actor.uri}/followers`) + ); } } diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 5c16744a77..2d9e7a14ee 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -1,19 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import escapeRegexp from 'escape-regexp'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; +import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { MemoryKVCache } from '@/misc/cache.js'; -import type { MiUserPublickey } from '@/models/UserPublickey.js'; +import type { UserPublickey } from '@/models/entities/UserPublickey.js'; import { CacheService } from '@/core/CacheService.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import type { MiNote } from '@/models/Note.js'; +import type { Note } from '@/models/entities/Note.js'; import { bindThis } from '@/decorators.js'; -import { MiLocalUser, MiRemoteUser } from '@/models/User.js'; +import { LocalUser, RemoteUser } from '@/models/entities/User.js'; import { getApId } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; import type { IObject } from './type.js'; @@ -36,8 +31,8 @@ export type UriParseResult = { @Injectable() export class ApDbResolverService implements OnApplicationShutdown { - private publicKeyCache: MemoryKVCache; - private publicKeyByUserIdCache: MemoryKVCache; + private publicKeyCache: MemoryKVCache; + private publicKeyByUserIdCache: MemoryKVCache; constructor( @Inject(DI.config) @@ -54,35 +49,39 @@ export class ApDbResolverService implements OnApplicationShutdown { private cacheService: CacheService, private apPersonService: ApPersonService, - private utilityService: UtilityService, ) { - this.publicKeyCache = new MemoryKVCache(1000 * 60 * 60 * 12); // 12h - this.publicKeyByUserIdCache = new MemoryKVCache(1000 * 60 * 60 * 12); // 12h + this.publicKeyCache = new MemoryKVCache(Infinity); + this.publicKeyByUserIdCache = new MemoryKVCache(Infinity); } @bindThis public parseUri(value: string | IObject): UriParseResult { - const separator = '/'; - - const uri = new URL(getApId(value)); - if (this.utilityService.toPuny(uri.host) !== this.utilityService.toPuny(this.config.host)) { - return { local: false, uri: uri.href }; + const uri = getApId(value); + + // the host part of a URL is case insensitive, so use the 'i' flag. + const localRegex = new RegExp('^' + escapeRegexp(this.config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i'); + const matchLocal = uri.match(localRegex); + + if (matchLocal) { + return { + local: true, + type: matchLocal[1], + id: matchLocal[2], + rest: matchLocal[3], + }; + } else { + return { + local: false, + uri, + }; } - - const [, type, id, ...rest] = uri.pathname.split(separator); - return { - local: true, - type, - id, - rest: rest.length === 0 ? undefined : rest.join(separator), - }; } /** * AP Note => Misskey Note in DB */ @bindThis - public async getNoteFromApId(value: string | IObject): Promise { + public async getNoteFromApId(value: string | IObject): Promise { const parsed = this.parseUri(value); if (parsed.local) { @@ -102,21 +101,19 @@ export class ApDbResolverService implements OnApplicationShutdown { * AP Person => Misskey User in DB */ @bindThis - public async getUserFromApId(value: string | IObject): Promise { + public async getUserFromApId(value: string | IObject): Promise { const parsed = this.parseUri(value); if (parsed.local) { if (parsed.type !== 'users') return null; - return await this.cacheService.userByIdCache.fetchMaybe( - parsed.id, - () => this.usersRepository.findOneBy({ id: parsed.id, isDeleted: false }).then(x => x ?? undefined), - ) as MiLocalUser | undefined ?? null; + return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({ + id: parsed.id, + }).then(x => x ?? undefined)) as LocalUser | undefined ?? null; } else { - return await this.cacheService.uriPersonCache.fetch( - parsed.uri, - () => this.usersRepository.findOneBy({ uri: parsed.uri, isDeleted: false }), - ) as MiRemoteUser | null; + return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({ + uri: parsed.uri, + })) as RemoteUser | null; } } @@ -125,14 +122,14 @@ export class ApDbResolverService implements OnApplicationShutdown { */ @bindThis public async getAuthUserFromKeyId(keyId: string): Promise<{ - user: MiRemoteUser; - key: MiUserPublickey; + user: RemoteUser; + key: UserPublickey; } | null> { const key = await this.publicKeyCache.fetch(keyId, async () => { const key = await this.userPublickeysRepository.findOneBy({ keyId, }); - + if (key == null) return null; return key; @@ -140,12 +137,8 @@ export class ApDbResolverService implements OnApplicationShutdown { if (key == null) return null; - const user = await this.cacheService.findUserById(key.userId).catch(() => null) as MiRemoteUser | null; - if (user == null) return null; - if (user.isDeleted) return null; - return { - user, + user: await this.cacheService.findUserById(key.userId) as RemoteUser, key, }; } @@ -155,17 +148,14 @@ export class ApDbResolverService implements OnApplicationShutdown { */ @bindThis public async getAuthUserFromApId(uri: string): Promise<{ - user: MiRemoteUser; - key: MiUserPublickey | null; + user: RemoteUser; + key: UserPublickey | null; } | null> { - const user = await this.apPersonService.resolvePerson(uri) as MiRemoteUser; - if (user.isDeleted) return null; + const user = await this.apPersonService.resolvePerson(uri) as RemoteUser; - const key = await this.publicKeyByUserIdCache.fetch( - user.id, - () => this.userPublickeysRepository.findOneBy({ userId: user.id }), - v => v != null, - ); + if (user == null) return null; + + const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null); return { user, diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 0140ce9fd6..62a2a33a19 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -1,18 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository } from '@/models/_.js'; -import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; +import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; +import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import { QueueService } from '@/core/QueueService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import type { IActivity } from '@/core/activitypub/type.js'; -import { ThinUser } from '@/queue/types.js'; interface IRecipe { type: string; @@ -24,133 +18,24 @@ interface IFollowersRecipe extends IRecipe { interface IDirectRecipe extends IRecipe { type: 'Direct'; - to: MiRemoteUser; + to: RemoteUser; } -const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe => +const isFollowers = (recipe: any): recipe is IFollowersRecipe => recipe.type === 'Followers'; -const isDirect = (recipe: IRecipe): recipe is IDirectRecipe => +const isDirect = (recipe: any): recipe is IDirectRecipe => recipe.type === 'Direct'; -class DeliverManager { - private actor: ThinUser; - private activity: IActivity | null; - private recipes: IRecipe[] = []; - - /** - * Constructor - * @param userEntityService - * @param followingsRepository - * @param queueService - * @param actor Actor - * @param activity Activity to deliver - */ - constructor( - private userEntityService: UserEntityService, - private followingsRepository: FollowingsRepository, - private queueService: QueueService, - - actor: { id: MiUser['id']; host: null; }, - activity: IActivity | null, - ) { - // 型で弾いてはいるが一応ローカルユーザーかチェック - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (actor.host != null) throw new Error('actor.host must be null'); - - // パフォーマンス向上のためキューに突っ込むのはidのみに絞る - this.actor = { - id: actor.id, - }; - this.activity = activity; - } - - /** - * Add recipe for followers deliver - */ - @bindThis - public addFollowersRecipe(): void { - const deliver: IFollowersRecipe = { - type: 'Followers', - }; - - this.addRecipe(deliver); - } - - /** - * Add recipe for direct deliver - * @param to To - */ - @bindThis - public addDirectRecipe(to: MiRemoteUser): void { - const recipe: IDirectRecipe = { - type: 'Direct', - to, - }; - - this.addRecipe(recipe); - } - - /** - * Add recipe - * @param recipe Recipe - */ - @bindThis - public addRecipe(recipe: IRecipe): void { - this.recipes.push(recipe); - } - - /** - * Execute delivers - */ - @bindThis - public async execute(): Promise { - // The value flags whether it is shared or not. - // key: inbox URL, value: whether it is sharedInbox - const inboxes = new Map(); - - // build inbox list - // Process follower recipes first to avoid duplication when processing direct recipes later. - if (this.recipes.some(r => isFollowers(r))) { - // followers deliver - // TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう - // ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう? - const followers = await this.followingsRepository.find({ - where: { - followeeId: this.actor.id, - followerHost: Not(IsNull()), - }, - select: { - followerSharedInbox: true, - followerInbox: true, - }, - }); - - for (const following of followers) { - const inbox = following.followerSharedInbox ?? following.followerInbox; - if (inbox === null) throw new Error('inbox is null'); - inboxes.set(inbox, following.followerSharedInbox != null); - } - } - - for (const recipe of this.recipes.filter(isDirect)) { - // check that shared inbox has not been added yet - if (recipe.to.sharedInbox !== null && inboxes.has(recipe.to.sharedInbox)) continue; - - // check that they actually have an inbox - if (recipe.to.inbox === null) continue; - - inboxes.set(recipe.to.inbox, false); - } - - // deliver - await this.queueService.deliverMany(this.actor, this.activity, inboxes); - } -} - @Injectable() export class ApDeliverManagerService { constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @@ -161,11 +46,11 @@ export class ApDeliverManagerService { /** * Deliver activity to followers - * @param actor * @param activity Activity + * @param from Followee */ @bindThis - public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity): Promise { + public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: any) { const manager = new DeliverManager( this.userEntityService, this.followingsRepository, @@ -179,12 +64,11 @@ export class ApDeliverManagerService { /** * Deliver activity to user - * @param actor * @param activity Activity * @param to Target user */ @bindThis - public async deliverToUser(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, to: MiRemoteUser): Promise { + public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: any, to: RemoteUser) { const manager = new DeliverManager( this.userEntityService, this.followingsRepository, @@ -196,34 +80,130 @@ export class ApDeliverManagerService { await manager.execute(); } - /** - * Deliver activity to users - * @param actor - * @param activity Activity - * @param targets Target users - */ @bindThis - public async deliverToUsers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, targets: MiRemoteUser[]): Promise { - const manager = new DeliverManager( - this.userEntityService, - this.followingsRepository, - this.queueService, - actor, - activity, - ); - for (const to of targets) manager.addDirectRecipe(to); - await manager.execute(); - } - - @bindThis - public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager { + public createDeliverManager(actor: { id: User['id']; host: null; }, activity: any) { return new DeliverManager( this.userEntityService, this.followingsRepository, this.queueService, - actor, + actor, activity, ); } } + +class DeliverManager { + private actor: { id: User['id']; host: null; }; + private activity: any; + private recipes: IRecipe[] = []; + + /** + * Constructor + * @param actor Actor + * @param activity Activity to deliver + */ + constructor( + private userEntityService: UserEntityService, + private followingsRepository: FollowingsRepository, + private queueService: QueueService, + + actor: { id: User['id']; host: null; }, + activity: any, + ) { + this.actor = actor; + this.activity = activity; + } + + /** + * Add recipe for followers deliver + */ + @bindThis + public addFollowersRecipe() { + const deliver = { + type: 'Followers', + } as IFollowersRecipe; + + this.addRecipe(deliver); + } + + /** + * Add recipe for direct deliver + * @param to To + */ + @bindThis + public addDirectRecipe(to: RemoteUser) { + const recipe = { + type: 'Direct', + to, + } as IDirectRecipe; + + this.addRecipe(recipe); + } + + /** + * Add recipe + * @param recipe Recipe + */ + @bindThis + public addRecipe(recipe: IRecipe) { + this.recipes.push(recipe); + } + + /** + * Execute delivers + */ + @bindThis + public async execute() { + if (!this.userEntityService.isLocalUser(this.actor)) return; + + // The value flags whether it is shared or not. + const inboxes = new Map(); + + /* + build inbox list + + Process follower recipes first to avoid duplication when processing + direct recipes later. + */ + if (this.recipes.some(r => isFollowers(r))) { + // followers deliver + // TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう + // ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう? + const followers = await this.followingsRepository.find({ + where: { + followeeId: this.actor.id, + followerHost: Not(IsNull()), + }, + select: { + followerSharedInbox: true, + followerInbox: true, + }, + }) as { + followerSharedInbox: string | null; + followerInbox: string; + }[]; + + for (const following of followers) { + const inbox = following.followerSharedInbox ?? following.followerInbox; + inboxes.set(inbox, following.followerSharedInbox != null); + } + } + + this.recipes.filter((recipe): recipe is IDirectRecipe => + // followers recipes have already been processed + isDirect(recipe) + // check that shared inbox has not been added yet + && !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox)) + // check that they actually have an inbox + && recipe.to.inbox != null, + ) + .forEach(recipe => inboxes.set(recipe.to.inbox!, false)); + + // deliver + for (const inbox of inboxes) { + // inbox[0]: inbox, inbox[1]: whether it is sharedInbox + this.queueService.deliver(this.actor, this.activity, inbox[0], inbox[1]); + } + } +} diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index e88f60b806..efef777fb0 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; @@ -17,19 +12,19 @@ import { NoteCreateService } from '@/core/NoteCreateService.js'; import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; import { AppLockService } from '@/core/AppLockService.js'; import type Logger from '@/logger.js'; +import { MetaService } from '@/core/MetaService.js'; +import { AccountMoveService } from '@/core/AccountMoveService.js'; import { IdService } from '@/core/IdService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { CacheService } from '@/core/CacheService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { QueueService } from '@/core/QueueService.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; -import type { MiRemoteUser } from '@/models/User.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { AbuseReportService } from '@/core/AbuseReportService.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; +import type { RemoteUser } from '@/models/entities/User.js'; +import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; import { ApDbResolverService } from './ApDbResolverService.js'; @@ -38,7 +33,7 @@ import { ApAudienceService } from './ApAudienceService.js'; import { ApPersonService } from './models/ApPersonService.js'; import { ApQuestionService } from './models/ApQuestionService.js'; import type { Resolver } from './ApResolverService.js'; -import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js'; +import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js'; @Injectable() export class ApInboxService { @@ -48,9 +43,6 @@ export class ApInboxService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -60,6 +52,9 @@ export class ApInboxService { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, + @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, @@ -67,7 +62,7 @@ export class ApInboxService { private noteEntityService: NoteEntityService, private utilityService: UtilityService, private idService: IdService, - private abuseReportService: AbuseReportService, + private metaService: MetaService, private userFollowingService: UserFollowingService, private apAudienceService: ApAudienceService, private reactionService: ReactionService, @@ -83,101 +78,80 @@ export class ApInboxService { private apNoteService: ApNoteService, private apPersonService: ApPersonService, private apQuestionService: ApQuestionService, + private accountMoveService: AccountMoveService, + private cacheService: CacheService, private queueService: QueueService, - private globalEventService: GlobalEventService, ) { this.logger = this.apLoggerService.logger; } @bindThis - public async performActivity(actor: MiRemoteUser, activity: IObject, resolver?: Resolver): Promise { - let result = undefined as string | void; + public async performActivity(actor: RemoteUser, activity: IObject) { if (isCollectionOrOrderedCollection(activity)) { - const results = [] as [string, string | void][]; - // eslint-disable-next-line no-param-reassign - resolver ??= this.apResolverService.createResolver(); - - const items = toArray(isCollection(activity) ? activity.items : activity.orderedItems); - if (items.length >= resolver.getRecursionLimit()) { - throw new Error(`skipping activity: collection would surpass recursion limit: ${this.utilityService.extractDbHost(actor.uri)}`); - } - - for (const item of items) { + const resolver = this.apResolverService.createResolver(); + for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { const act = await resolver.resolve(item); - if (act.id == null || this.utilityService.extractDbHost(act.id) !== this.utilityService.extractDbHost(actor.uri)) { - this.logger.debug('skipping activity: activity id is null or mismatching'); - continue; - } try { - results.push([getApId(item), await this.performOneActivity(actor, act, resolver)]); + await this.performOneActivity(actor, act); } catch (err) { if (err instanceof Error || typeof err === 'string') { this.logger.error(err); - } else { - throw err; } } } - - const hasReason = results.some(([, reason]) => (reason != null && !reason.startsWith('ok'))); - if (hasReason) { - result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n'); - } } else { - result = await this.performOneActivity(actor, activity, resolver); + await this.performOneActivity(actor, activity); } // ついでにリモートユーザーの情報が古かったら更新しておく if (actor.uri) { if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { setImmediate(() => { - // 同一ユーザーの情報を再度処理するので、使用済みのresolverを再利用してはいけない - this.apPersonService.updatePerson(actor.uri); + this.apPersonService.updatePerson(actor.uri!); }); } } - return result; } @bindThis - public async performOneActivity(actor: MiRemoteUser, activity: IObject, resolver?: Resolver): Promise { + public async performOneActivity(actor: RemoteUser, activity: IObject): Promise { if (actor.isSuspended) return; if (isCreate(activity)) { - return await this.create(actor, activity, resolver); + await this.create(actor, activity); } else if (isDelete(activity)) { - return await this.delete(actor, activity); + await this.delete(actor, activity); } else if (isUpdate(activity)) { - return await this.update(actor, activity, resolver); + await this.update(actor, activity); } else if (isFollow(activity)) { - return await this.follow(actor, activity); + await this.follow(actor, activity); } else if (isAccept(activity)) { - return await this.accept(actor, activity, resolver); + await this.accept(actor, activity); } else if (isReject(activity)) { - return await this.reject(actor, activity, resolver); + await this.reject(actor, activity); } else if (isAdd(activity)) { - return await this.add(actor, activity, resolver); + await this.add(actor, activity).catch(err => this.logger.error(err)); } else if (isRemove(activity)) { - return await this.remove(actor, activity, resolver); + await this.remove(actor, activity).catch(err => this.logger.error(err)); } else if (isAnnounce(activity)) { - return await this.announce(actor, activity, resolver); + await this.announce(actor, activity); } else if (isLike(activity)) { - return await this.like(actor, activity); + await this.like(actor, activity); } else if (isUndo(activity)) { - return await this.undo(actor, activity, resolver); + await this.undo(actor, activity); } else if (isBlock(activity)) { - return await this.block(actor, activity); + await this.block(actor, activity); } else if (isFlag(activity)) { - return await this.flag(actor, activity); + await this.flag(actor, activity); } else if (isMove(activity)) { - return await this.move(actor, activity, resolver); + await this.move(actor, activity); } else { - return `unrecognized activity type: ${activity.type}`; + this.logger.warn(`unrecognized activity type: ${activity.type}`); } } @bindThis - private async follow(actor: MiRemoteUser, activity: IFollow): Promise { + private async follow(actor: RemoteUser, activity: IFollow): Promise { const followee = await this.apDbResolverService.getUserFromApId(activity.object); if (followee == null) { @@ -189,12 +163,12 @@ export class ApInboxService { } // don't queue because the sender may attempt again when timeout - await this.userFollowingService.follow(actor, followee, { requestId: activity.id }); + await this.userFollowingService.follow(actor, followee, activity.id); return 'ok'; } @bindThis - private async like(actor: MiRemoteUser, activity: ILike): Promise { + private async like(actor: RemoteUser, activity: ILike): Promise { const targetUri = getApId(activity.object); const note = await this.apNoteService.fetchNote(targetUri); @@ -202,26 +176,22 @@ export class ApInboxService { await this.apNoteService.extractEmojis(activity.tag ?? [], actor.host).catch(() => null); - try { - await this.reactionService.create(actor, note, activity._misskey_reaction ?? activity.content ?? activity.name); - return 'ok'; - } catch (err) { - if (err instanceof IdentifiableError && err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') { + return await this.reactionService.create(actor, note, activity._misskey_reaction ?? activity.content ?? activity.name).catch(err => { + if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') { return 'skip: already reacted'; } else { throw err; } - } + }).then(() => 'ok'); } @bindThis - private async accept(actor: MiRemoteUser, activity: IAccept, resolver?: Resolver): Promise { + private async accept(actor: RemoteUser, activity: IAccept): Promise { const uri = activity.id ?? activity; this.logger.info(`Accept: ${uri}`); - // eslint-disable-next-line no-param-reassign - resolver ??= this.apResolverService.createResolver(); + const resolver = this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(err => { this.logger.error(`Resolution failed: ${err}`); @@ -234,7 +204,7 @@ export class ApInboxService { } @bindThis - private async acceptFollow(actor: MiRemoteUser, activity: IFollow): Promise { + private async acceptFollow(actor: RemoteUser, activity: IFollow): Promise { // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある const follower = await this.apDbResolverService.getUserFromApId(activity.actor); @@ -258,63 +228,52 @@ export class ApInboxService { } @bindThis - private async add(actor: MiRemoteUser, activity: IAdd, resolver?: Resolver): Promise { - if (actor.uri !== activity.actor) { - return 'invalid actor'; + private async add(actor: RemoteUser, activity: IAdd): Promise { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); } if (activity.target == null) { - return 'target is null'; + throw new Error('target is null'); } if (activity.target === actor.featured) { - const note = await this.apNoteService.resolveNote(activity.object, { resolver }); - if (note == null) return 'note not found'; + const note = await this.apNoteService.resolveNote(activity.object); + if (note == null) throw new Error('note not found'); await this.notePiningService.addPinned(actor, note.id); return; } - return `unknown target: ${activity.target}`; + throw new Error(`unknown target: ${activity.target}`); } @bindThis - private async announce(actor: MiRemoteUser, activity: IAnnounce, resolver?: Resolver): Promise { + private async announce(actor: RemoteUser, activity: IAnnounce): Promise { const uri = getApId(activity); this.logger.info(`Announce: ${uri}`); - // eslint-disable-next-line no-param-reassign - resolver ??= this.apResolverService.createResolver(); - - if (!activity.object) return 'skip: activity has no object property'; const targetUri = getApId(activity.object); - if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.'; - const target = await resolver.resolve(activity.object).catch(e => { - this.logger.error(`Resolution failed: ${e}`); - throw e; - }); - - if (isPost(target)) return await this.announceNote(actor, activity, target); - - return `skip: unknown object type ${getApType(target)}`; + this.announceNote(actor, activity, targetUri); } @bindThis - private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost, resolver?: Resolver): Promise { + private async announceNote(actor: RemoteUser, activity: IAnnounce, targetUri: string): Promise { const uri = getApId(activity); if (actor.isSuspended) { return; } - // アナウンス先が許可されているかチェック - if (!this.utilityService.isFederationAllowedUri(uri)) return; + // アナウンス先をブロックしてたら中断 + const meta = await this.metaService.fetch(); + if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return; const unlock = await this.appLockService.getApLock(uri); try { - // 既に同じURIを持つものが登録されていないかチェック + // 既に同じURIを持つものが登録されていないかチェック const exist = await this.apNoteService.fetchNote(uri); if (exist) { return; @@ -323,34 +282,32 @@ export class ApInboxService { // Announce対象をresolve let renote; try { - renote = await this.apNoteService.resolveNote(target, { resolver }); - if (renote == null) return 'announce target is null'; + renote = await this.apNoteService.resolveNote(targetUri); + if (renote == null) throw new Error('announce target is null'); } catch (err) { // 対象が4xxならスキップ if (err instanceof StatusError) { - if (!err.isRetryable) { - return `Ignored announce target ${target.id} - ${err.statusCode}`; + if (err.isClientError) { + this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`); + return; } - return `Error in announce target ${target.id} - ${err.statusCode}`; + + this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode ?? err}`); } throw err; } if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) { - return 'skip: invalid actor for this activity'; + this.logger.warn('skip: invalid actor for this activity'); + return; } this.logger.info(`Creating the (Re)Note: ${uri}`); - const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc, resolver); - const createdAt = activity.published ? new Date(activity.published) : null; - - if (createdAt && createdAt < this.idService.parse(renote.id).date) { - return 'skip: malformed createdAt'; - } + const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc); await this.noteCreateService.create(actor, { - createdAt, + createdAt: activity.published ? new Date(activity.published) : null, renote, visibility: activityAudience.visibility, visibleUsers: activityAudience.visibleUsers, @@ -362,7 +319,7 @@ export class ApInboxService { } @bindThis - private async block(actor: MiRemoteUser, activity: IBlock): Promise { + private async block(actor: RemoteUser, activity: IBlock): Promise { // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず const blockee = await this.apDbResolverService.getUserFromApId(activity.object); @@ -380,15 +337,11 @@ export class ApInboxService { } @bindThis - private async create(actor: MiRemoteUser, activity: ICreate, resolver?: Resolver): Promise { + private async create(actor: RemoteUser, activity: ICreate): Promise { const uri = getApId(activity); this.logger.info(`Create: ${uri}`); - if (!activity.object) return 'skip: activity has no object property'; - const targetUri = getApId(activity.object); - if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.'; - // copy audiences between activity <=> object. if (typeof activity.object === 'object') { const to = unique(concat([toArray(activity.to), toArray(activity.object.to)])); @@ -405,8 +358,7 @@ export class ApInboxService { activity.object.attributedTo = activity.actor; } - // eslint-disable-next-line no-param-reassign - resolver ??= this.apResolverService.createResolver(); + const resolver = this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(e => { this.logger.error(`Resolution failed: ${e}`); @@ -414,14 +366,14 @@ export class ApInboxService { }); if (isPost(object)) { - await this.createNote(resolver, actor, object, false, activity); + this.createNote(resolver, actor, object, false, activity); } else { - return `Unknown type: ${getApType(object)}`; + this.logger.warn(`Unknown type: ${getApType(object)}`); } } @bindThis - private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { + private async createNote(resolver: Resolver, actor: RemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { const uri = getApId(note); if (typeof note === 'object') { @@ -433,8 +385,6 @@ export class ApInboxService { if (this.utilityService.extractDbHost(actor.uri) !== this.utilityService.extractDbHost(note.id)) { return 'skip: host in actor.uri !== note.id'; } - } else { - return 'skip: note.id is not a string'; } } @@ -444,10 +394,10 @@ export class ApInboxService { const exist = await this.apNoteService.fetchNote(note); if (exist) return 'skip: note exists'; - await this.apNoteService.createNote(note, actor, resolver, silent); + await this.apNoteService.createNote(note, resolver, silent); return 'ok'; } catch (err) { - if (err instanceof StatusError && !err.isRetryable) { + if (err instanceof StatusError && err.isClientError) { return `skip ${err.statusCode}`; } else { throw err; @@ -458,9 +408,9 @@ export class ApInboxService { } @bindThis - private async delete(actor: MiRemoteUser, activity: IDelete): Promise { - if (actor.uri !== activity.actor) { - return 'invalid actor'; + private async delete(actor: RemoteUser, activity: IDelete): Promise { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); } // 削除対象objectのtype @@ -470,7 +420,7 @@ export class ApInboxService { // typeが不明だけど、どうせ消えてるのでremote resolveしない formerType = undefined; } else { - const object = activity.object; + const object = activity.object as IObject; if (isTombstone(object)) { formerType = toSingle(object.formerType); } else { @@ -500,26 +450,31 @@ export class ApInboxService { } @bindThis - private async deleteActor(actor: MiRemoteUser, uri: string): Promise { + private async deleteActor(actor: RemoteUser, uri: string): Promise { this.logger.info(`Deleting the Actor: ${uri}`); if (actor.uri !== uri) { return `skip: delete actor ${actor.uri} !== ${uri}`; } - if (!(await this.usersRepository.update({ id: actor.id, isDeleted: false }, { isDeleted: true })).affected) { - return 'skip: already deleted or actor not found'; + const user = await this.usersRepository.findOneBy({ id: actor.id }); + if (user == null) { + return 'skip: actor not found'; + } else if (user.isDeleted) { + return 'skip: already deleted'; } const job = await this.queueService.createDeleteAccountJob(actor); - this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id }); + await this.usersRepository.update(actor.id, { + isDeleted: true, + }); return `ok: queued ${job.name} ${job.id}`; } @bindThis - private async deleteNote(actor: MiRemoteUser, uri: string): Promise { + private async deleteNote(actor: RemoteUser, uri: string): Promise { this.logger.info(`Deleting the Note: ${uri}`); const unlock = await this.appLockService.getApLock(uri); @@ -543,39 +498,37 @@ export class ApInboxService { } @bindThis - private async flag(actor: MiRemoteUser, activity: IFlag): Promise { + private async flag(actor: RemoteUser, activity: IFlag): Promise { // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する const uris = getApIds(activity.object); - const userIds = uris - .filter(uri => uri.startsWith(this.config.url + '/users/')) - .map(uri => uri.split('/').at(-1)) - .filter(x => x != null); + const userIds = uris.filter(uri => uri.startsWith(this.config.url + '/users/')).map(uri => uri.split('/').pop()!); const users = await this.usersRepository.findBy({ id: In(userIds), }); if (users.length < 1) return 'skip'; - await this.abuseReportService.report([{ + await this.abuseUserReportsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), targetUserId: users[0].id, targetUserHost: users[0].host, reporterId: actor.id, reporterHost: actor.host, comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`, - }]); + }); return 'ok'; } @bindThis - private async reject(actor: MiRemoteUser, activity: IReject, resolver?: Resolver): Promise { + private async reject(actor: RemoteUser, activity: IReject): Promise { const uri = activity.id ?? activity; this.logger.info(`Reject: ${uri}`); - // eslint-disable-next-line no-param-reassign - resolver ??= this.apResolverService.createResolver(); + const resolver = this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(e => { this.logger.error(`Resolution failed: ${e}`); @@ -588,7 +541,7 @@ export class ApInboxService { } @bindThis - private async rejectFollow(actor: MiRemoteUser, activity: IFollow): Promise { + private async rejectFollow(actor: RemoteUser, activity: IFollow): Promise { // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある const follower = await this.apDbResolverService.getUserFromApId(activity.actor); @@ -612,37 +565,36 @@ export class ApInboxService { } @bindThis - private async remove(actor: MiRemoteUser, activity: IRemove, resolver?: Resolver): Promise { - if (actor.uri !== activity.actor) { - return 'invalid actor'; + private async remove(actor: RemoteUser, activity: IRemove): Promise { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); } if (activity.target == null) { - return 'target is null'; + throw new Error('target is null'); } if (activity.target === actor.featured) { - const note = await this.apNoteService.resolveNote(activity.object, { resolver }); - if (note == null) return 'note not found'; + const note = await this.apNoteService.resolveNote(activity.object); + if (note == null) throw new Error('note not found'); await this.notePiningService.removePinned(actor, note.id); return; } - return `unknown target: ${activity.target}`; + throw new Error(`unknown target: ${activity.target}`); } @bindThis - private async undo(actor: MiRemoteUser, activity: IUndo, resolver?: Resolver): Promise { - if (actor.uri !== activity.actor) { - return 'invalid actor'; + private async undo(actor: RemoteUser, activity: IUndo): Promise { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); } const uri = activity.id ?? activity; this.logger.info(`Undo: ${uri}`); - // eslint-disable-next-line no-param-reassign - resolver ??= this.apResolverService.createResolver(); + const resolver = this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(e => { this.logger.error(`Resolution failed: ${e}`); @@ -660,20 +612,18 @@ export class ApInboxService { } @bindThis - private async undoAccept(actor: MiRemoteUser, activity: IAccept): Promise { + private async undoAccept(actor: RemoteUser, activity: IAccept): Promise { const follower = await this.apDbResolverService.getUserFromApId(activity.object); if (follower == null) { return 'skip: follower not found'; } - const isFollowing = await this.followingsRepository.exists({ - where: { - followerId: follower.id, - followeeId: actor.id, - }, + const following = await this.followingsRepository.findOneBy({ + followerId: follower.id, + followeeId: actor.id, }); - if (isFollowing) { + if (following) { await this.userFollowingService.unfollow(follower, actor); return 'ok: unfollowed'; } @@ -682,7 +632,7 @@ export class ApInboxService { } @bindThis - private async undoAnnounce(actor: MiRemoteUser, activity: IAnnounce): Promise { + private async undoAnnounce(actor: RemoteUser, activity: IAnnounce): Promise { const uri = getApId(activity); const note = await this.notesRepository.findOneBy({ @@ -697,7 +647,7 @@ export class ApInboxService { } @bindThis - private async undoBlock(actor: MiRemoteUser, activity: IBlock): Promise { + private async undoBlock(actor: RemoteUser, activity: IBlock): Promise { const blockee = await this.apDbResolverService.getUserFromApId(activity.object); if (blockee == null) { @@ -713,7 +663,7 @@ export class ApInboxService { } @bindThis - private async undoFollow(actor: MiRemoteUser, activity: IFollow): Promise { + private async undoFollow(actor: RemoteUser, activity: IFollow): Promise { const followee = await this.apDbResolverService.getUserFromApId(activity.object); if (followee == null) { return 'skip: followee not found'; @@ -723,26 +673,22 @@ export class ApInboxService { return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません'; } - const requestExist = await this.followRequestsRepository.exists({ - where: { - followerId: actor.id, - followeeId: followee.id, - }, + const req = await this.followRequestsRepository.findOneBy({ + followerId: actor.id, + followeeId: followee.id, }); - const isFollowing = await this.followingsRepository.exists({ - where: { - followerId: actor.id, - followeeId: followee.id, - }, + const following = await this.followingsRepository.findOneBy({ + followerId: actor.id, + followeeId: followee.id, }); - if (requestExist) { + if (req) { await this.userFollowingService.cancelFollowRequest(followee, actor); return 'ok: follow request canceled'; } - if (isFollowing) { + if (following) { await this.userFollowingService.unfollow(actor, followee); return 'ok: unfollowed'; } @@ -751,7 +697,7 @@ export class ApInboxService { } @bindThis - private async undoLike(actor: MiRemoteUser, activity: ILike): Promise { + private async undoLike(actor: RemoteUser, activity: ILike): Promise { const targetUri = getApId(activity.object); const note = await this.apNoteService.fetchNote(targetUri); @@ -766,15 +712,14 @@ export class ApInboxService { } @bindThis - private async update(actor: MiRemoteUser, activity: IUpdate, resolver?: Resolver): Promise { - if (actor.uri !== activity.actor) { + private async update(actor: RemoteUser, activity: IUpdate): Promise { + if ('actor' in activity && actor.uri !== activity.actor) { return 'skip: invalid actor'; } this.logger.debug('Update'); - // eslint-disable-next-line no-param-reassign - resolver ??= this.apResolverService.createResolver(); + const resolver = this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(e => { this.logger.error(`Resolution failed: ${e}`); @@ -782,10 +727,10 @@ export class ApInboxService { }); if (isActor(object)) { - await this.apPersonService.updatePerson(actor.uri, resolver, object); + await this.apPersonService.updatePerson(actor.uri!, resolver, object); return 'ok: Person updated'; } else if (getApType(object) === 'Question') { - await this.apQuestionService.updateQuestion(object, actor, resolver).catch(err => console.error(err)); + await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err)); return 'ok: Question updated'; } else { return `skip: Unknown type: ${getApType(object)}`; @@ -793,11 +738,11 @@ export class ApInboxService { } @bindThis - private async move(actor: MiRemoteUser, activity: IMove, resolver?: Resolver): Promise { + private async move(actor: RemoteUser, activity: IMove): Promise { // fetch the new and old accounts const targetUri = getApHrefNullable(activity.target); if (!targetUri) return 'skip: invalid activity target'; - return await this.apPersonService.updatePerson(actor.uri, resolver) ?? 'skip: nothing to do'; + return await this.apPersonService.updatePerson(actor.uri) ?? 'skip: nothing to do'; } } diff --git a/packages/backend/src/core/activitypub/ApLoggerService.ts b/packages/backend/src/core/activitypub/ApLoggerService.ts index 428d8061ce..eeffab1b6d 100644 --- a/packages/backend/src/core/activitypub/ApLoggerService.ts +++ b/packages/backend/src/core/activitypub/ApLoggerService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { RemoteLoggerService } from '@/core/RemoteLoggerService.js'; diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts index f4c07e472c..6116822f7a 100644 --- a/packages/backend/src/core/activitypub/ApMfmService.ts +++ b/packages/backend/src/core/activitypub/ApMfmService.ts @@ -1,45 +1,33 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import * as mfm from 'mfm-js'; -import { MfmService, Appender } from '@/core/MfmService.js'; -import type { MiNote } from '@/models/Note.js'; -import { bindThis } from '@/decorators.js'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; +import { MfmService } from '@/core/MfmService.js'; +import type { Note } from '@/models/entities/Note.js'; import { extractApHashtagObjects } from './models/tag.js'; import type { IObject } from './type.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ApMfmService { constructor( + @Inject(DI.config) + private config: Config, + private mfmService: MfmService, ) { } @bindThis - public htmlToMfm(html: string, tag?: IObject | IObject[]): string { - const hashtagNames = extractApHashtagObjects(tag).map(x => x.name); + public htmlToMfm(html: string, tag?: IObject | IObject[]) { + const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null); + return this.mfmService.fromHtml(html, hashtagNames); } @bindThis - public getNoteHtml(note: Pick, additionalAppender: Appender[] = []) { - let noMisskeyContent = false; - const srcMfm = (note.text ?? ''); - - const parsed = mfm.parse(srcMfm); - - if (!additionalAppender.length && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) { - noMisskeyContent = true; - } - - const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers), additionalAppender); - - return { - content, - noMisskeyContent, - }; - } + public getNoteHtml(note: Note) { + if (!note.text) return ''; + return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); + } } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 55521d6e3a..d8b95ca4d1 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -1,37 +1,32 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { createPublicKey, randomUUID } from 'node:crypto'; +import { createPublicKey } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; +import { In, IsNull } from 'typeorm'; +import { v4 as uuid } from 'uuid'; import * as mfm from 'mfm-js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import type { IMentionedRemoteUsers, MiNote } from '@/models/Note.js'; -import type { MiBlocking } from '@/models/Blocking.js'; -import type { MiRelay } from '@/models/Relay.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiNoteReaction } from '@/models/NoteReaction.js'; -import type { MiEmoji } from '@/models/Emoji.js'; -import type { MiPoll } from '@/models/Poll.js'; -import type { MiPollVote } from '@/models/PollVote.js'; +import type { PartialLocalUser, LocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js'; +import type { IMentionedRemoteUsers, Note } from '@/models/entities/Note.js'; +import type { Blocking } from '@/models/entities/Blocking.js'; +import type { Relay } from '@/models/entities/Relay.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { NoteReaction } from '@/models/entities/NoteReaction.js'; +import type { Emoji } from '@/models/entities/Emoji.js'; +import type { Poll } from '@/models/entities/Poll.js'; +import type { PollVote } from '@/models/entities/PollVote.js'; import { UserKeypairService } from '@/core/UserKeypairService.js'; -import { MfmService, type Appender } from '@/core/MfmService.js'; +import { MfmService } from '@/core/MfmService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import type { MiUserKeypair } from '@/models/UserKeypair.js'; -import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository, MiMeta } from '@/models/_.js'; +import type { UserKeypair } from '@/models/entities/UserKeypair.js'; +import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import { IdService } from '@/core/IdService.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import { JsonLdService } from './JsonLdService.js'; +import { isNotNull } from '@/misc/is-not-null.js'; +import { LdSignatureService } from './LdSignatureService.js'; import { ApMfmService } from './ApMfmService.js'; -import { CONTEXT } from './misc/contexts.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; +import type { IIdentifier } from './models/identifier.js'; @Injectable() export class ApRendererService { @@ -39,9 +34,6 @@ export class ApRendererService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -54,23 +46,24 @@ export class ApRendererService { @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, private customEmojiService: CustomEmojiService, private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, - private jsonLdService: JsonLdService, + private ldSignatureService: LdSignatureService, private userKeypairService: UserKeypairService, private apMfmService: ApMfmService, private mfmService: MfmService, - private idService: IdService, - private utilityService: UtilityService, ) { } @bindThis - public renderAccept(object: string | IObject, user: { id: MiUser['id']; host: null }): IAccept { + public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept { return { type: 'Accept', actor: this.userEntityService.genLocalUserUri(user.id), @@ -79,7 +72,7 @@ export class ApRendererService { } @bindThis - public renderAdd(user: MiLocalUser, target: string | IObject | undefined, object: string | IObject): IAdd { + public renderAdd(user: LocalUser, target: any, object: any): IAdd { return { type: 'Add', actor: this.userEntityService.genLocalUserUri(user.id), @@ -89,7 +82,7 @@ export class ApRendererService { } @bindThis - public renderAnnounce(object: string | IObject, note: MiNote): IAnnounce { + public renderAnnounce(object: any, note: Note): IAnnounce { const attributedTo = this.userEntityService.genLocalUserUri(note.userId); let to: string[] = []; @@ -112,7 +105,7 @@ export class ApRendererService { id: `${this.config.url}/notes/${note.id}/activity`, actor: this.userEntityService.genLocalUserUri(note.userId), type: 'Announce', - published: this.idService.parse(note.id).date.toISOString(), + published: note.createdAt.toISOString(), to, cc, object, @@ -125,7 +118,7 @@ export class ApRendererService { * @param block The block to be rendered. The blockee relation must be loaded. */ @bindThis - public renderBlock(block: MiBlocking): IBlock { + public renderBlock(block: Blocking): IBlock { if (block.blockee?.uri == null) { throw new Error('renderBlock: missing blockee uri'); } @@ -139,14 +132,14 @@ export class ApRendererService { } @bindThis - public renderCreate(object: IObject, note: MiNote): ICreate { - const activity: ICreate = { + public renderCreate(object: IObject, note: Note): ICreate { + const activity = { id: `${this.config.url}/notes/${note.id}/activity`, actor: this.userEntityService.genLocalUserUri(note.userId), type: 'Create', - published: this.idService.parse(note.id).date.toISOString(), + published: note.createdAt.toISOString(), object, - }; + } as ICreate; if (object.to) activity.to = object.to; if (object.cc) activity.cc = object.cc; @@ -155,7 +148,7 @@ export class ApRendererService { } @bindThis - public renderDelete(object: IObject | string, user: { id: MiUser['id']; host: null }): IDelete { + public renderDelete(object: IObject | string, user: { id: User['id']; host: null }): IDelete { return { type: 'Delete', actor: this.userEntityService.genLocalUserUri(user.id), @@ -165,18 +158,17 @@ export class ApRendererService { } @bindThis - public renderDocument(file: MiDriveFile): IApDocument { + public renderDocument(file: DriveFile): IApDocument { return { type: 'Document', mediaType: file.webpublicType ?? file.type, url: this.driveFileEntityService.getPublicUrl(file), name: file.comment, - sensitive: file.isSensitive, }; } @bindThis - public renderEmoji(emoji: MiEmoji): IApEmoji { + public renderEmoji(emoji: Emoji): IApEmoji { return { id: `${this.config.url}/emojis/${emoji.name}`, type: 'Emoji', @@ -188,15 +180,12 @@ export class ApRendererService { // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) url: emoji.publicUrl || emoji.originalUrl, }, - _misskey_license: { - freeText: emoji.license, - }, }; } // to anonymise reporters, the reporting actor must be a system user @bindThis - public renderFlag(user: MiLocalUser, object: IObject | string, content: string): IFlag { + public renderFlag(user: LocalUser, object: IObject | string, content: string): IFlag { return { type: 'Flag', actor: this.userEntityService.genLocalUserUri(user.id), @@ -206,7 +195,7 @@ export class ApRendererService { } @bindThis - public renderFollowRelay(relay: MiRelay, relayActor: MiLocalUser): IFollow { + public renderFollowRelay(relay: Relay, relayActor: LocalUser): IFollow { return { id: `${this.config.url}/activities/follow-relay/${relay.id}`, type: 'Follow', @@ -220,22 +209,22 @@ export class ApRendererService { * @param id Follower|Followee ID */ @bindThis - public async renderFollowUser(id: MiUser['id']): Promise { - const user = await this.usersRepository.findOneByOrFail({ id: id }) as MiPartialLocalUser | MiPartialRemoteUser; + public async renderFollowUser(id: User['id']) { + const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser; return this.userEntityService.getUserUri(user); } @bindThis public renderFollow( - follower: MiPartialLocalUser | MiPartialRemoteUser, - followee: MiPartialLocalUser | MiPartialRemoteUser, + follower: PartialLocalUser | PartialRemoteUser, + followee: PartialLocalUser | PartialRemoteUser, requestId?: string, ): IFollow { return { id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`, type: 'Follow', - actor: this.userEntityService.getUserUri(follower), - object: this.userEntityService.getUserUri(followee), + actor: this.userEntityService.getUserUri(follower)!, + object: this.userEntityService.getUserUri(followee)!, }; } @@ -249,7 +238,7 @@ export class ApRendererService { } @bindThis - public renderImage(file: MiDriveFile): IApImage { + public renderImage(file: DriveFile): IApImage { return { type: 'Image', url: this.driveFileEntityService.getPublicUrl(file), @@ -259,39 +248,7 @@ export class ApRendererService { } @bindThis - public renderIdenticon(user: MiLocalUser): IApImage { - return { - type: 'Image', - url: this.userEntityService.getIdenticonUrl(user), - sensitive: false, - name: null, - }; - } - - @bindThis - public renderSystemAvatar(user: MiLocalUser): IApImage { - if (this.meta.iconUrl == null) return this.renderIdenticon(user); - return { - type: 'Image', - url: this.meta.iconUrl, - sensitive: false, - name: null, - }; - } - - @bindThis - public renderSystemBanner(): IApImage | null { - if (this.meta.bannerUrl == null) return null; - return { - type: 'Image', - url: this.meta.bannerUrl, - sensitive: false, - name: null, - }; - } - - @bindThis - public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey { + public renderKey(user: LocalUser, key: UserKeypair, postfix?: string): IKey { return { id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`, type: 'Key', @@ -304,17 +261,17 @@ export class ApRendererService { } @bindThis - public async renderLike(noteReaction: MiNoteReaction, note: { uri: string | null }): Promise { + public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise { const reaction = noteReaction.reaction; - const object: ILike = { + const object = { type: 'Like', id: `${this.config.url}/likes/${noteReaction.id}`, actor: `${this.config.url}/users/${noteReaction.userId}`, object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`, content: reaction, _misskey_reaction: reaction, - }; + } as ILike; if (reaction.startsWith(':')) { const name = reaction.replaceAll(':', ''); @@ -327,21 +284,21 @@ export class ApRendererService { } @bindThis - public renderMention(mention: MiPartialLocalUser | MiPartialRemoteUser): IApMention { + public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention { return { type: 'Mention', - href: this.userEntityService.getUserUri(mention), - name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as MiLocalUser).username}`, + href: this.userEntityService.getUserUri(mention)!, + name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`, }; } @bindThis public renderMove( - src: MiPartialLocalUser | MiPartialRemoteUser, - dst: MiPartialLocalUser | MiPartialRemoteUser, + src: PartialLocalUser | PartialRemoteUser, + dst: PartialLocalUser | PartialRemoteUser, ): IMove { - const actor = this.userEntityService.getUserUri(src); - const target = this.userEntityService.getUserUri(dst); + const actor = this.userEntityService.getUserUri(src)!; + const target = this.userEntityService.getUserUri(dst)!; return { id: `${this.config.url}/moves/${src.id}/${dst.id}`, actor, @@ -352,23 +309,23 @@ export class ApRendererService { } @bindThis - public async renderNote(note: MiNote, dive = true): Promise { - const getPromisedFiles = async (ids: string[]): Promise => { - if (ids.length === 0) return []; + public async renderNote(note: Note, dive = true): Promise { + const getPromisedFiles = async (ids: string[]) => { + if (!ids || ids.length === 0) return []; const items = await this.driveFilesRepository.findBy({ id: In(ids) }); - return ids.map(id => items.find(item => item.id === id)).filter(x => x != null); + return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[]; }; let inReplyTo; - let inReplyToNote: MiNote | null; + let inReplyToNote: Note | null; if (note.replyId) { inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId }); if (inReplyToNote != null) { - const inReplyToUserExist = await this.usersRepository.exists({ where: { id: inReplyToNote.userId } }); + const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId }); - if (inReplyToUserExist) { + if (inReplyToUser != null) { if (inReplyToNote.uri) { inReplyTo = inReplyToNote.uri; } else { @@ -418,41 +375,29 @@ export class ApRendererService { id: In(note.mentions), }) : []; - const hashtagTags = note.tags.map(tag => this.renderHashtag(tag)); - const mentionTags = mentionedUsers.map(u => this.renderMention(u as MiLocalUser | MiRemoteUser)); + const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag)); + const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser)); const files = await getPromisedFiles(note.fileIds); const text = note.text ?? ''; - let poll: MiPoll | null = null; + let poll: Poll | null = null; if (note.hasPoll) { poll = await this.pollsRepository.findOneBy({ noteId: note.id }); } - const apAppend: Appender[] = []; + let apText = text; if (quote) { - // Append quote link as `

RE: ...` - // the claas name `quote-inline` is used in non-misskey clients for styling quote notes. - // For compatibility, the span part should be kept as possible. - apAppend.push((doc, body) => { - body.appendChild(doc.createElement('br')); - body.appendChild(doc.createElement('br')); - const span = doc.createElement('span'); - span.className = 'quote-inline'; - span.appendChild(doc.createTextNode('RE: ')); - const link = doc.createElement('a'); - link.setAttribute('href', quote); - link.textContent = quote; - span.appendChild(link); - body.appendChild(span); - }); + apText += `\n\nRE: ${quote}`; } const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; - const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend); + const content = this.apMfmService.getNoteHtml(Object.assign({}, note, { + text: apText, + })); const emojis = await this.getEmojis(note.emojis); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); @@ -465,6 +410,9 @@ export class ApRendererService { const asPoll = poll ? { type: 'Question', + content: this.apMfmService.getNoteHtml(Object.assign({}, note, { + text: text, + })), [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ type: 'Note', @@ -482,16 +430,14 @@ export class ApRendererService { attributedTo, summary: summary ?? undefined, content: content ?? undefined, - ...(noMisskeyContent ? {} : { - _misskey_content: text, - source: { - content: text, - mediaType: 'text/x.misskeymarkdown', - }, - }), + _misskey_content: text, + source: { + content: text, + mediaType: 'text/x.misskeymarkdown', + }, _misskey_quote: quote, quoteUrl: quote, - published: this.idService.parse(note.id).date.toISOString(), + published: note.createdAt.toISOString(), to, cc, inReplyTo, @@ -503,45 +449,39 @@ export class ApRendererService { } @bindThis - public async renderPerson(user: MiLocalUser) { + public async renderPerson(user: LocalUser) { const id = this.userEntityService.genLocalUserUri(user.id); - const isSystem = user.username.includes('.'); + const isSystem = !!user.username.match(/\./); const [avatar, banner, profile] = await Promise.all([ - user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined, - user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined, + user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : Promise.resolve(undefined), + user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : Promise.resolve(undefined), this.userProfilesRepository.findOneByOrFail({ userId: user.id }), ]); - const tryRewriteUrl = (maybeUrl: string) => { - const urlSafeRegex = /^(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/; - try { - const match = maybeUrl.match(urlSafeRegex); - if (!match) { - return maybeUrl; - } - const urlPart = match[0]; - const urlPartParsed = new URL(urlPart); - const restPart = maybeUrl.slice(match[0].length); - - return `${urlPart}${restPart}`; - } catch (e) { - return maybeUrl; - } - }; - - const attachment = profile.fields.map(field => ({ + const attachment: { type: 'PropertyValue', - name: field.name, - value: (field.value.startsWith('http://') || field.value.startsWith('https://')) - ? tryRewriteUrl(field.value) - : field.value, - })); + name: string, + value: string, + identifier?: IIdentifier, + }[] = []; + + if (profile.fields) { + for (const field of profile.fields) { + attachment.push({ + type: 'PropertyValue', + name: field.name, + value: (field.value != null && field.value.match(/^https?:/)) + ? `${new URL(field.value).href}` + : field.value, + }); + } + } const emojis = await this.getEmojis(user.emojis); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); - const hashtagTags = user.tags.map(tag => this.renderHashtag(tag)); + const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag)); const tag = [ ...apemojis, @@ -550,7 +490,7 @@ export class ApRendererService { const keypair = await this.userKeypairService.getUserKeypair(user.id); - const person: any = { + const person = { type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person', id, inbox: `${id}/inbox`, @@ -564,20 +504,15 @@ export class ApRendererService { preferredUsername: user.username, name: user.name, summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null, - _misskey_summary: profile.description, - _misskey_followedMessage: profile.followedMessage, - _misskey_requireSigninToViewContents: user.requireSigninToViewContents, - _misskey_makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore, - _misskey_makeNotesHiddenBefore: user.makeNotesHiddenBefore, - icon: avatar ? this.renderImage(avatar) : isSystem ? this.renderSystemAvatar(user) : this.renderIdenticon(user), - image: banner ? this.renderImage(banner) : isSystem ? this.renderSystemBanner() : null, + icon: avatar ? this.renderImage(avatar) : null, + image: banner ? this.renderImage(banner) : null, tag, manuallyApprovesFollowers: user.isLocked, - discoverable: user.isExplorable, + discoverable: !!user.isExplorable, publicKey: this.renderKey(user, keypair, '#main-key'), isCat: user.isCat, attachment: attachment.length ? attachment : undefined, - }; + } as any; if (user.movedToUri) { person.movedTo = user.movedToUri; @@ -599,7 +534,7 @@ export class ApRendererService { } @bindThis - public renderQuestion(user: { id: MiUser['id'] }, note: MiNote, poll: MiPoll): IQuestion { + public renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll): IQuestion { return { type: 'Question', id: `${this.config.url}/questions/${note.id}`, @@ -617,7 +552,7 @@ export class ApRendererService { } @bindThis - public renderReject(object: string | IObject, user: { id: MiUser['id'] }): IReject { + public renderReject(object: any, user: { id: User['id'] }): IReject { return { type: 'Reject', actor: this.userEntityService.genLocalUserUri(user.id), @@ -626,7 +561,7 @@ export class ApRendererService { } @bindThis - public renderRemove(user: { id: MiUser['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove { + public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove { return { type: 'Remove', actor: this.userEntityService.genLocalUserUri(user.id), @@ -644,8 +579,8 @@ export class ApRendererService { } @bindThis - public renderUndo(object: string | IObject, user: { id: MiUser['id'] }): IUndo { - const id = typeof object !== 'string' && typeof object.id === 'string' && this.utilityService.isUriLocal(object.id) ? `${object.id}/undo` : undefined; + public renderUndo(object: any, user: { id: User['id'] }): IUndo { + const id = typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined; return { type: 'Undo', @@ -657,7 +592,7 @@ export class ApRendererService { } @bindThis - public renderUpdate(object: string | IObject, user: { id: MiUser['id'] }): IUpdate { + public renderUpdate(object: any, user: { id: User['id'] }): IUpdate { return { id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`, actor: this.userEntityService.genLocalUserUri(user.id), @@ -669,7 +604,7 @@ export class ApRendererService { } @bindThis - public renderVote(user: { id: MiUser['id'] }, vote: MiPollVote, note: MiNote, poll: MiPoll, pollOwner: MiRemoteUser): ICreate { + public renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: RemoteUser): ICreate { return { id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`, actor: this.userEntityService.genLocalUserUri(user.id), @@ -690,19 +625,49 @@ export class ApRendererService { @bindThis public addContext(x: T): T & { '@context': any; id: string; } { if (typeof x === 'object' && x.id == null) { - x.id = `${this.config.url}/${randomUUID()}`; + x.id = `${this.config.url}/${uuid()}`; } - return Object.assign({ '@context': CONTEXT }, x as T & { id: string }); + return Object.assign({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + { + // as non-standards + manuallyApprovesFollowers: 'as:manuallyApprovesFollowers', + sensitive: 'as:sensitive', + Hashtag: 'as:Hashtag', + quoteUrl: 'as:quoteUrl', + // Mastodon + toot: 'http://joinmastodon.org/ns#', + Emoji: 'toot:Emoji', + featured: 'toot:featured', + discoverable: 'toot:discoverable', + // schema + schema: 'http://schema.org#', + PropertyValue: 'schema:PropertyValue', + value: 'schema:value', + // Misskey + misskey: 'https://misskey-hub.net/ns#', + '_misskey_content': 'misskey:_misskey_content', + '_misskey_quote': 'misskey:_misskey_quote', + '_misskey_reaction': 'misskey:_misskey_reaction', + '_misskey_votes': 'misskey:_misskey_votes', + 'isCat': 'misskey:isCat', + // vcard + vcard: 'http://www.w3.org/2006/vcard/ns#', + }, + ], + }, x as T & { id: string; }); } @bindThis - public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise { + public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise { const keypair = await this.userKeypairService.getUserKeypair(user.id); - const jsonLd = this.jsonLdService.use(); - jsonLd.debug = false; - activity = await jsonLd.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`); + const ldSignature = this.ldSignatureService.use(); + ldSignature.debug = false; + activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`); return activity; } @@ -718,13 +683,13 @@ export class ApRendererService { */ @bindThis public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) { - const page: any = { + const page = { id, partOf, type: 'OrderedCollectionPage', totalItems, orderedItems, - }; + } as any; if (prev) page.prev = prev; if (next) page.next = next; @@ -741,7 +706,7 @@ export class ApRendererService { * @param orderedItems attached objects (optional) */ @bindThis - public renderOrderedCollection(id: string, totalItems: number, first?: string, last?: string, orderedItems?: IObject[]) { + public renderOrderedCollection(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: IObject[]) { const page: any = { id, type: 'OrderedCollection', @@ -756,11 +721,11 @@ export class ApRendererService { } @bindThis - private async getEmojis(names: string[]): Promise { - if (names.length === 0) return []; + private async getEmojis(names: string[]): Promise { + if (names == null || names.length === 0) return []; const allEmojis = await this.customEmojiService.localEmojisCache.fetch(); - const emojis = names.map(name => allEmojis.get(name)).filter(x => x != null); + const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull); return emojis; } diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 61d328ccac..5005612ab8 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -1,24 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as crypto from 'node:crypto'; import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; -import { Window } from 'happy-dom'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import type { MiUser } from '@/models/User.js'; +import type { User } from '@/models/entities/User.js'; import { UserKeypairService } from '@/core/UserKeypairService.js'; -import { UtilityService } from '@/core/UtilityService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; -import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; -import { assertActivityMatchesUrl, FetchAllowSoftFailMask as FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; -import type { IObject } from './type.js'; type Request = { url: string; @@ -39,9 +29,9 @@ type PrivateKey = { }; export class ApRequestCreator { - static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record }): Signed { + static createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record }): Signed { const u = new URL(args.url); - const digestHeader = args.digest ?? this.createDigest(args.body); + const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`; const request: Request = { url: u.href, @@ -64,10 +54,6 @@ export class ApRequestCreator { }; } - static createDigest(body: string) { - return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`; - } - static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record }): Signed { const u = new URL(args.url); @@ -75,7 +61,7 @@ export class ApRequestCreator { url: u.href, method: 'GET', headers: this.#objectAssignWithLcKey({ - 'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'Accept': 'application/activity+json, application/ld+json', 'Date': new Date().toUTCString(), 'Host': new URL(args.url).host, }, args.additionalHeaders), @@ -148,15 +134,14 @@ export class ApRequestService { private userKeypairService: UserKeypairService, private httpRequestService: HttpRequestService, private loggerService: LoggerService, - private utilityService: UtilityService, ) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる } @bindThis - public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise { - const body = typeof object === 'string' ? object : JSON.stringify(object); + public async signedPost(user: { id: User['id'] }, url: string, object: any) { + const body = JSON.stringify(object); const keypair = await this.userKeypairService.getUserKeypair(user.id); @@ -167,7 +152,6 @@ export class ApRequestService { }, url, body, - digest, additionalHeaders: { }, }); @@ -185,8 +169,7 @@ export class ApRequestService { * @param url URL to fetch */ @bindThis - public async signedGet(url: string, user: { id: MiUser['id'] }, allowSoftfail: FetchAllowSoftFailMask = FetchAllowSoftFailMask.Strict, followAlternate?: boolean): Promise { - const _followAlternate = followAlternate ?? true; + public async signedGet(url: string, user: { id: User['id'] }) { const keypair = await this.userKeypairService.getUserKeypair(user.id); const req = ApRequestCreator.createSignedGet({ @@ -202,64 +185,8 @@ export class ApRequestService { const res = await this.httpRequestService.send(url, { method: req.request.method, headers: req.request.headers, - }, { - throwErrorWhenResponseNotOk: true, }); - //#region リクエスト先がhtmlかつactivity+jsonへのalternate linkタグがあるとき - const contentType = res.headers.get('content-type'); - - if ( - res.ok && - (contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && - _followAlternate === true - ) { - const html = await res.text(); - const { window, happyDOM } = new Window({ - settings: { - disableJavaScriptEvaluation: true, - disableJavaScriptFileLoading: true, - disableCSSFileLoading: true, - disableComputedStyleRendering: true, - handleDisabledFileLoadingAsSuccess: true, - navigation: { - disableMainFrameNavigation: true, - disableChildFrameNavigation: true, - disableChildPageNavigation: true, - disableFallbackToSetURL: true, - }, - timer: { - maxTimeout: 0, - maxIntervalTime: 0, - maxIntervalIterations: 0, - }, - }, - }); - const document = window.document; - try { - document.documentElement.innerHTML = html; - - const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]'); - if (alternate) { - const href = alternate.getAttribute('href'); - if (href && this.utilityService.punyHost(url) === this.utilityService.punyHost(href)) { - return await this.signedGet(href, user, allowSoftfail, false); - } - } - } catch (e) { - // something went wrong parsing the HTML, ignore the whole thing - } finally { - happyDOM.close().catch(err => {}); - } - } - //#endregion - - validateContentTypeSetAsActivityPub(res); - const finalUrl = res.url; // redirects may have been involved - const activity = await res.json() as IObject; - - assertActivityMatchesUrl(url, activity, finalUrl, allowSoftfail); - - return activity; + return await res.json(); } } diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 646150455b..d3e0345c9c 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -1,49 +1,41 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, Not } from 'typeorm'; -import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; -import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; +import type { LocalUser, RemoteUser } from '@/models/entities/User.js'; +import { InstanceActorService } from '@/core/InstanceActorService.js'; +import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; +import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; -import { SystemAccountService } from '@/core/SystemAccountService.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; import { isCollectionOrOrderedCollection } from './type.js'; import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; import { ApRequestService } from './ApRequestService.js'; -import { FetchAllowSoftFailMask } from './misc/check-against-url.js'; import type { IObject, ICollection, IOrderedCollection } from './type.js'; export class Resolver { private history: Set; - private user?: MiLocalUser; + private user?: LocalUser; private logger: Logger; constructor( private config: Config, - private meta: MiMeta, private usersRepository: UsersRepository, private notesRepository: NotesRepository, private pollsRepository: PollsRepository, private noteReactionsRepository: NoteReactionsRepository, - private followRequestsRepository: FollowRequestsRepository, private utilityService: UtilityService, - private systemAccountService: SystemAccountService, + private instanceActorService: InstanceActorService, + private metaService: MetaService, private apRequestService: ApRequestService, private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, private apDbResolverService: ApDbResolverService, private loggerService: LoggerService, - private recursionLimit = 256, + private recursionLimit = 100, ) { this.history = new Set(); this.logger = this.loggerService.getLogger('ap-resolve'); @@ -54,11 +46,6 @@ export class Resolver { return Array.from(this.history); } - @bindThis - public getRecursionLimit(): number { - return this.recursionLimit; - } - @bindThis public async resolveCollection(value: string | IObject): Promise { const collection = typeof value === 'string' @@ -68,12 +55,16 @@ export class Resolver { if (isCollectionOrOrderedCollection(collection)) { return collection; } else { - throw new IdentifiableError('f100eccf-f347-43fb-9b45-96a0831fb635', `unrecognized collection type: ${collection.type}`); + throw new Error(`unrecognized collection type: ${collection.type}`); } } @bindThis - public async resolve(value: string | IObject, allowSoftfail: FetchAllowSoftFailMask = FetchAllowSoftFailMask.Strict): Promise { + public async resolve(value: string | IObject): Promise { + if (value == null) { + throw new Error('resolvee is null (or undefined)'); + } + if (typeof value !== 'string') { return value; } @@ -82,15 +73,15 @@ export class Resolver { // URLs with fragment parts cannot be resolved correctly because // the fragment part does not get transmitted over HTTP(S). // Avoid strange behaviour by not trying to resolve these at all. - throw new IdentifiableError('b94fd5b1-0e3b-4678-9df2-dad4cd515ab2', `cannot resolve URL with fragment: ${value}`); + throw new Error(`cannot resolve URL with fragment: ${value}`); } if (this.history.has(value)) { - throw new IdentifiableError('0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5', 'cannot resolve already resolved one'); + throw new Error('cannot resolve already resolved one'); } if (this.history.size > this.recursionLimit) { - throw new IdentifiableError('d592da9f-822f-4d91-83d7-4ceefabcf3d2', `hit recursion limit: ${this.utilityService.extractDbHost(value)}`); + throw new Error(`hit recursion limit: ${this.utilityService.extractDbHost(value)}`); } this.history.add(value); @@ -100,24 +91,25 @@ export class Resolver { return await this.resolveLocal(value); } - if (!this.utilityService.isFederationAllowedHost(host)) { - throw new IdentifiableError('09d79f9e-64f1-4316-9cfa-e75c4d091574', 'Instance is blocked'); + const meta = await this.metaService.fetch(); + if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) { + throw new Error('Instance is blocked'); } - if (this.meta.signToActivityPubGet && !this.user) { - this.user = await this.systemAccountService.fetch('actor'); + if (this.config.signToActivityPubGet && !this.user) { + this.user = await this.instanceActorService.getInstanceActor(); } const object = (this.user - ? await this.apRequestService.signedGet(value, this.user, allowSoftfail) as IObject - : await this.httpRequestService.getActivityJson(value, undefined, allowSoftfail)) as IObject; + ? await this.apRequestService.signedGet(value, this.user) as IObject + : await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject; - if ( + if (object == null || ( Array.isArray(object['@context']) ? !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' - ) { - throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', 'invalid response'); + )) { + throw new Error('invalid response'); } return object; @@ -126,7 +118,7 @@ export class Resolver { @bindThis private resolveLocal(url: string): Promise { const parsed = this.apDbResolverService.parseUri(url); - if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', 'resolveLocal: not local'); + if (!parsed.local) throw new Error('resolveLocal: not local'); switch (parsed.type) { case 'notes': @@ -141,7 +133,7 @@ export class Resolver { }); case 'users': return this.usersRepository.findOneByOrFail({ id: parsed.id }) - .then(user => this.apRendererService.renderPerson(user as MiLocalUser)); + .then(user => this.apRendererService.renderPerson(user as LocalUser)); case 'questions': // Polls are indexed by the note they are attached to. return Promise.all([ @@ -153,26 +145,15 @@ export class Resolver { return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(async reaction => this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null }))); case 'follows': - return this.followRequestsRepository.findOneBy({ id: parsed.id }) - .then(async followRequest => { - if (followRequest == null) throw new IdentifiableError('a9d946e5-d276-47f8-95fb-f04230289bb0', 'resolveLocal: invalid follow request ID'); - const [follower, followee] = await Promise.all([ - this.usersRepository.findOneBy({ - id: followRequest.followerId, - host: IsNull(), - }), - this.usersRepository.findOneBy({ - id: followRequest.followeeId, - host: Not(IsNull()), - }), - ]); - if (follower == null || followee == null) { - throw new IdentifiableError('06ae3170-1796-4d93-a697-2611ea6d83b6', 'resolveLocal: follower or followee does not exist'); - } - return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url)); - }); + // rest should be + if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI'); + + return Promise.all( + [parsed.id, parsed.rest].map(id => this.usersRepository.findOneByOrFail({ id })), + ) + .then(([follower, followee]) => this.apRendererService.addContext(this.apRendererService.renderFollow(follower as LocalUser | RemoteUser, followee as LocalUser | RemoteUser, url))); default: - throw new IdentifiableError('7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0', `resolveLocal: type ${parsed.type} unhandled`); + throw new Error(`resolveLocal: type ${parsed.type} unhandled`); } } } @@ -183,9 +164,6 @@ export class ApResolverService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -198,11 +176,9 @@ export class ApResolverService { @Inject(DI.noteReactionsRepository) private noteReactionsRepository: NoteReactionsRepository, - @Inject(DI.followRequestsRepository) - private followRequestsRepository: FollowRequestsRepository, - private utilityService: UtilityService, - private systemAccountService: SystemAccountService, + private instanceActorService: InstanceActorService, + private metaService: MetaService, private apRequestService: ApRequestService, private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, @@ -215,14 +191,13 @@ export class ApResolverService { public createResolver(): Resolver { return new Resolver( this.config, - this.meta, this.usersRepository, this.notesRepository, this.pollsRepository, this.noteReactionsRepository, - this.followRequestsRepository, this.utilityService, - this.systemAccountService, + this.instanceActorService, + this.metaService, this.apRequestService, this.httpRequestService, this.apRendererService, diff --git a/packages/backend/src/core/activitypub/JsonLdService.ts b/packages/backend/src/core/activitypub/LdSignatureService.ts similarity index 65% rename from packages/backend/src/core/activitypub/JsonLdService.ts rename to packages/backend/src/core/activitypub/LdSignatureService.ts index 100d4fa19f..20fe2a0a77 100644 --- a/packages/backend/src/core/activitypub/JsonLdService.ts +++ b/packages/backend/src/core/activitypub/LdSignatureService.ts @@ -1,20 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as crypto from 'node:crypto'; import { Injectable } from '@nestjs/common'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; -import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js'; -import { validateContentTypeSetAsJsonLD } from './misc/validator.js'; -import type { JsonLdDocument } from 'jsonld'; -import type { JsonLd as JsonLdObject, RemoteDocument } from 'jsonld/jsonld-spec.js'; +import { CONTEXTS } from './misc/contexts.js'; -// RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017 +// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017 -class JsonLd { +class LdSignature { public debug = false; public preLoad = true; public loderTimeout = 5000; @@ -26,21 +18,22 @@ class JsonLd { @bindThis public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise { - const options: { + const options = { + type: 'RsaSignature2017', + creator, + domain, + nonce: crypto.randomBytes(16).toString('hex'), + created: (created ?? new Date()).toISOString(), + } as { type: string; creator: string; domain?: string; nonce: string; created: string; - } = { - type: 'RsaSignature2017', - creator, - nonce: crypto.randomBytes(16).toString('hex'), - created: (created ?? new Date()).toISOString(), }; - if (domain) { - options.domain = domain; + if (!domain) { + delete options.domain; } const toBeSigned = await this.createVerifyData(data, options); @@ -69,7 +62,7 @@ class JsonLd { } @bindThis - public async createVerifyData(data: any, options: any): Promise { + public async createVerifyData(data: any, options: any) { const transformedOptions = { ...options, '@context': 'https://w3id.org/identity/v1', @@ -89,18 +82,10 @@ class JsonLd { } @bindThis - public async compact(data: any, context: any = CONTEXT): Promise { + public async normalize(data: any) { const customLoader = this.getLoader(); // XXX: Importing jsonld dynamically since Jest frequently fails to import it statically // https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595 - return (await import('jsonld')).default.compact(data, context, { - documentLoader: customLoader, - }); - } - - @bindThis - public async normalize(data: JsonLdDocument): Promise { - const customLoader = this.getLoader(); return (await import('jsonld')).default.normalize(data, { documentLoader: customLoader, }); @@ -108,15 +93,15 @@ class JsonLd { @bindThis private getLoader() { - return async (url: string): Promise => { - if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`); + return async (url: string): Promise => { + if (!url.match('^https?\:\/\/')) throw new Error(`Invalid URL ${url}`); if (this.preLoad) { - if (url in PRELOADED_CONTEXTS) { + if (url in CONTEXTS) { if (this.debug) console.debug(`HIT: ${url}`); return { - contextUrl: undefined, - document: PRELOADED_CONTEXTS[url], + contextUrl: null, + document: CONTEXTS[url], documentUrl: url, }; } @@ -125,7 +110,7 @@ class JsonLd { if (this.debug) console.debug(`MISS: ${url}`); const document = await this.fetchDocument(url); return { - contextUrl: undefined, + contextUrl: null, document: document, documentUrl: url, }; @@ -133,20 +118,13 @@ class JsonLd { } @bindThis - private async fetchDocument(url: string): Promise { - const json = await this.httpRequestService.send( - url, - { - headers: { - Accept: 'application/ld+json, application/json', - }, - timeout: this.loderTimeout, + private async fetchDocument(url: string) { + const json = await this.httpRequestService.send(url, { + headers: { + Accept: 'application/ld+json, application/json', }, - { - throwErrorWhenResponseNotOk: false, - validators: [validateContentTypeSetAsJsonLD], - }, - ).then(res => { + timeout: this.loderTimeout, + }, { throwErrorWhenResponseNotOk: false }).then(res => { if (!res.ok) { throw new Error(`${res.status} ${res.statusText}`); } else { @@ -154,7 +132,7 @@ class JsonLd { } }); - return json as JsonLdObject; + return json; } @bindThis @@ -166,14 +144,14 @@ class JsonLd { } @Injectable() -export class JsonLdService { +export class LdSignatureService { constructor( private httpRequestService: HttpRequestService, ) { } @bindThis - public use(): JsonLd { - return new JsonLd(this.httpRequestService); + public use(): LdSignature { + return new LdSignature(this.httpRequestService); } } diff --git a/packages/backend/src/core/activitypub/misc/check-against-url.ts b/packages/backend/src/core/activitypub/misc/check-against-url.ts deleted file mode 100644 index bbfe57f9fa..0000000000 --- a/packages/backend/src/core/activitypub/misc/check-against-url.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * SPDX-FileCopyrightText: dakkar and sharkey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { IObject } from '../type.js'; - -export enum FetchAllowSoftFailMask { - // Allow no softfail flags - Strict = 0, - // The values in tuple (requestUrl, finalUrl, objectId) are not all identical - // - // This condition is common for user-initiated lookups but should not be allowed in federation loop - // - // Allow variations: - // good example: https://alice.example.com/@user -> https://alice.example.com/user/:userId - // problematic example: https://alice.example.com/redirect?url=https://bad.example.com/ -> https://bad.example.com/ -> https://alice.example.com/somethingElse - NonCanonicalId = 1 << 0, - // Allow the final object to be at most one subdomain deeper than the request URL, similar to SPF relaxed alignment - // - // Currently no code path allows this flag to be set, but is kept in case of future use as some niche deployments do this, and we provide a pre-reviewed mechanism to opt-in. - // - // Allow variations: - // good example: https://example.com/@user -> https://activitypub.example.com/@user { id: 'https://activitypub.example.com/@user' } - // problematic example: https://example.com/@user -> https://untrusted.example.com/@user { id: 'https://untrusted.example.com/@user' } - MisalignedOrigin = 1 << 1, - // The requested URL has a different host than the returned object ID, although the final URL is still consistent with the object ID - // - // This condition is common for user-initiated lookups using an intermediate host but should not be allowed in federation loops - // - // Allow variations: - // good example: https://alice.example.com/@user@bob.example.com -> https://bob.example.com/@user { id: 'https://bob.example.com/@user' } - // problematic example: https://alice.example.com/definitelyAlice -> https://bob.example.com/@somebodyElse { id: 'https://bob.example.com/@somebodyElse' } - CrossOrigin = 1 << 2 | MisalignedOrigin, - // Allow all softfail flags - // - // do not use this flag on released code - Any = ~0, -} - -/** - * Fuzz match on whether the candidate host has authority over the request host - * - * @param requestHost The host of the requested resources - * @param candidateHost The host of final response - * @returns Whether the candidate host has authority over the request host, or if a soft fail is required for a match - */ -function hostFuzzyMatch(requestHost: string, candidateHost: string): FetchAllowSoftFailMask { - const requestFqdn = requestHost.endsWith('.') ? requestHost : `${requestHost}.`; - const candidateFqdn = candidateHost.endsWith('.') ? candidateHost : `${candidateHost}.`; - - if (requestFqdn === candidateFqdn) { - return FetchAllowSoftFailMask.Strict; - } - - // allow only one case where candidateHost is a first-level subdomain of requestHost - const requestDnsDepth = requestFqdn.split('.').length; - const candidateDnsDepth = candidateFqdn.split('.').length; - - if ((candidateDnsDepth - requestDnsDepth) !== 1) { - return FetchAllowSoftFailMask.CrossOrigin; - } - - if (`.${candidateHost}`.endsWith(`.${requestHost}`)) { - return FetchAllowSoftFailMask.MisalignedOrigin; - } - - return FetchAllowSoftFailMask.CrossOrigin; -} - -// normalize host names by removing www. prefix -function normalizeSynonymousSubdomain(url: URL | string): URL { - const urlParsed = url instanceof URL ? url : new URL(url); - const host = urlParsed.host; - const normalizedHost = host.replace(/^www\./, ''); - return new URL(urlParsed.toString().replace(host, normalizedHost)); -} - -export function assertActivityMatchesUrl(requestUrl: string | URL, activity: IObject, finalUrl: string | URL, allowSoftfail: FetchAllowSoftFailMask): FetchAllowSoftFailMask { - // must have a unique identifier to verify authority - if (!activity.id) { - throw new Error('bad Activity: missing id field'); - } - - let softfail = 0; - - // if the flag is allowed, set the flag on return otherwise throw - const requireSoftfail = (needed: FetchAllowSoftFailMask, message: string) => { - if ((allowSoftfail & needed) !== needed) { - throw new Error(message); - } - - softfail |= needed; - }; - - const requestUrlParsed = normalizeSynonymousSubdomain(requestUrl); - const idParsed = normalizeSynonymousSubdomain(activity.id); - - const finalUrlParsed = normalizeSynonymousSubdomain(finalUrl); - - // mastodon sends activities with hash in the URL - // currently it only happens with likes, deletes etc. - // but object ID never has hash - requestUrlParsed.hash = ''; - finalUrlParsed.hash = ''; - - const requestUrlSecure = requestUrlParsed.protocol === 'https:'; - const finalUrlSecure = finalUrlParsed.protocol === 'https:'; - if (requestUrlSecure && !finalUrlSecure) { - throw new Error(`bad Activity: id(${activity.id}) is not allowed to have http:// in the url`); - } - - // Compare final URL to the ID - if (finalUrlParsed.href !== idParsed.href) { - requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match response url(${finalUrlParsed.toString()})`); - - // at lease host need to match exactly (ActivityPub requirement) - if (idParsed.host !== finalUrlParsed.host) { - throw new Error(`bad Activity: id(${activity.id}) does not match response host(${finalUrlParsed.host})`); - } - } - - // Compare request URL to the ID - if (requestUrlParsed.href !== idParsed.href) { - requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match request url(${requestUrlParsed.toString()})`); - - // if cross-origin lookup is allowed, we can accept some variation between the original request URL to the final object ID (but not between the final URL and the object ID) - const hostResult = hostFuzzyMatch(requestUrlParsed.host, idParsed.host); - - requireSoftfail(hostResult, `bad Activity: id(${activity.id}) is valid but is not the same origin as request url(${requestUrlParsed.toString()})`); - } - - return softfail; -} diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index 6611e4b7f9..aee0d3629c 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -1,10 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { Context, JsonLd } from 'jsonld/jsonld-spec.js'; - /* eslint:disable:quotemark indent */ const id_v1 = { '@context': { @@ -93,7 +86,7 @@ const id_v1 = { 'accessControl': { '@id': 'perm:accessControl', '@type': '@id' }, 'writePermission': { '@id': 'perm:writePermission', '@type': '@id' }, }, -} satisfies JsonLd; +}; const security_v1 = { '@context': { @@ -144,7 +137,7 @@ const security_v1 = { 'signatureAlgorithm': 'sec:signingAlgorithm', 'signatureValue': 'sec:signatureValue', }, -} satisfies JsonLd; +}; const activitystreams = { '@context': { @@ -524,53 +517,9 @@ const activitystreams = { '@type': '@id', }, }, -} satisfies JsonLd; +}; -const context_iris = [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', -]; - -const extension_context_definition = { - Key: 'sec:Key', - // as non-standards - manuallyApprovesFollowers: 'as:manuallyApprovesFollowers', - sensitive: 'as:sensitive', - Hashtag: 'as:Hashtag', - quoteUrl: 'as:quoteUrl', - // Mastodon - toot: 'http://joinmastodon.org/ns#', - Emoji: 'toot:Emoji', - featured: 'toot:featured', - discoverable: 'toot:discoverable', - // schema - schema: 'http://schema.org#', - PropertyValue: 'schema:PropertyValue', - value: 'schema:value', - // Misskey - misskey: 'https://misskey-hub.net/ns#', - '_misskey_content': 'misskey:_misskey_content', - '_misskey_quote': 'misskey:_misskey_quote', - '_misskey_reaction': 'misskey:_misskey_reaction', - '_misskey_votes': 'misskey:_misskey_votes', - '_misskey_summary': 'misskey:_misskey_summary', - '_misskey_followedMessage': 'misskey:_misskey_followedMessage', - '_misskey_requireSigninToViewContents': 'misskey:_misskey_requireSigninToViewContents', - '_misskey_makeNotesFollowersOnlyBefore': 'misskey:_misskey_makeNotesFollowersOnlyBefore', - '_misskey_makeNotesHiddenBefore': 'misskey:_misskey_makeNotesHiddenBefore', - '_misskey_license': 'misskey:_misskey_license', - 'freeText': { - '@id': 'misskey:freeText', - '@type': 'schema:text', - }, - 'isCat': 'misskey:isCat', - // vcard - vcard: 'http://www.w3.org/2006/vcard/ns#', -} satisfies Context; - -export const CONTEXT: (string | Context)[] = [...context_iris, extension_context_definition]; - -export const PRELOADED_CONTEXTS: Record = { +export const CONTEXTS: Record = { 'https://w3id.org/identity/v1': id_v1, 'https://w3id.org/security/v1': security_v1, 'https://www.w3.org/ns/activitystreams': activitystreams, diff --git a/packages/backend/src/core/activitypub/misc/validator.ts b/packages/backend/src/core/activitypub/misc/validator.ts deleted file mode 100644 index 690beeffef..0000000000 --- a/packages/backend/src/core/activitypub/misc/validator.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { Response } from 'node-fetch'; - -export function validateContentTypeSetAsActivityPub(response: Response): void { - const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); - - if (contentType === '') { - throw new Error('Validate content type of AP response: No content-type header'); - } - if ( - contentType.startsWith('application/activity+json') || - (contentType.startsWith('application/ld+json;') && contentType.includes('https://www.w3.org/ns/activitystreams')) - ) { - return; - } - throw new Error('Validate content type of AP response: Content type is not application/activity+json or application/ld+json'); -} - -const plusJsonSuffixRegex = /^\s*(application|text)\/[a-zA-Z0-9\.\-\+]+\+json\s*(;|$)/; - -export function validateContentTypeSetAsJsonLD(response: Response): void { - const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); - - if (contentType === '') { - throw new Error('Validate content type of JSON LD: No content-type header'); - } - if ( - contentType.startsWith('application/ld+json') || - contentType.startsWith('application/json') || - plusJsonSuffixRegex.test(contentType) - ) { - return; - } - throw new Error('Validate content type of JSON LD: Content type is not application/ld+json or application/json'); -} diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index e7ece87b01..0043907c21 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -1,97 +1,96 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, MiMeta } from '@/models/_.js'; -import type { MiRemoteUser } from '@/models/User.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { DriveFilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; +import type { RemoteUser } from '@/models/entities/User.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import { MetaService } from '@/core/MetaService.js'; import { truncate } from '@/misc/truncate.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { DriveService } from '@/core/DriveService.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import { checkHttps } from '@/misc/check-https.js'; import { ApResolverService } from '../ApResolverService.js'; import { ApLoggerService } from '../ApLoggerService.js'; -import { isDocument, type IObject } from '../type.js'; +import { checkHttps } from '@/misc/check-https.js'; @Injectable() export class ApImageService { private logger: Logger; constructor( - @Inject(DI.meta) - private meta: MiMeta, + @Inject(DI.config) + private config: Config, @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + private metaService: MetaService, private apResolverService: ApResolverService, private driveService: DriveService, private apLoggerService: ApLoggerService, ) { this.logger = this.apLoggerService.logger; } - + /** * Imageを作成します。 */ @bindThis - public async createImage(actor: MiRemoteUser, value: string | IObject): Promise { + public async createImage(actor: RemoteUser, value: any): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { throw new Error('actor has been suspended'); } - const image = await this.apResolverService.createResolver().resolve(value); - - if (!isDocument(image)) return null; + const image = await this.apResolverService.createResolver().resolve(value) as any; if (image.url == null) { - return null; - } - - if (typeof image.url !== 'string') { - return null; + throw new Error('invalid image: url not privided'); } if (!checkHttps(image.url)) { - return null; + throw new Error('invalid image: unexpected schema of url: ' + image.url); } this.logger.info(`Creating the Image: ${image.url}`); - // Cache if remote file cache is on AND either - // 1. remote sensitive file is also on - // 2. or the image is not sensitive - const shouldBeCached = this.meta.cacheRemoteFiles && (this.meta.cacheRemoteSensitiveFiles || !image.sensitive); + const instance = await this.metaService.fetch(); - const file = await this.driveService.uploadFromUrl({ + let file = await this.driveService.uploadFromUrl({ url: image.url, user: actor, uri: image.url, sensitive: image.sensitive, - isLink: !shouldBeCached, - comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH), + isLink: !instance.cacheRemoteFiles, + comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH), }); - if (!file.isLink || file.url === image.url) return file; - // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、URLを更新する - await this.driveFilesRepository.update({ id: file.id }, { url: image.url, uri: image.url }); - return await this.driveFilesRepository.findOneByOrFail({ id: file.id }); + if (file.isLink) { + // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、 + // URLを更新する + if (file.url !== image.url) { + await this.driveFilesRepository.update({ id: file.id }, { + url: image.url, + uri: image.url, + }); + + file = await this.driveFilesRepository.findOneByOrFail({ id: file.id }); + } + } + + return file; } /** * Imageを解決します。 * - * ImageをリモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise { - // TODO: Misskeyに対象のImageが登録されていればそれを返す + public async resolveImage(actor: RemoteUser, value: any): Promise { + // TODO // リモートサーバーからフェッチしてきて登録 return await this.createImage(actor, value); diff --git a/packages/backend/src/core/activitypub/models/ApMentionService.ts b/packages/backend/src/core/activitypub/models/ApMentionService.ts index 2cd151fa04..c581840ca9 100644 --- a/packages/backend/src/core/activitypub/models/ApMentionService.ts +++ b/packages/backend/src/core/activitypub/models/ApMentionService.ts @@ -1,37 +1,38 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; -import type { MiUser } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import type { User } from '@/models/index.js'; +import type { Config } from '@/config.js'; import { toArray, unique } from '@/misc/prelude/array.js'; import { bindThis } from '@/decorators.js'; import { isMention } from '../type.js'; -import { Resolver } from '../ApResolverService.js'; +import { ApResolverService, Resolver } from '../ApResolverService.js'; import { ApPersonService } from './ApPersonService.js'; import type { IObject, IApMention } from '../type.js'; @Injectable() export class ApMentionService { constructor( + @Inject(DI.config) + private config: Config, + + private apResolverService: ApResolverService, private apPersonService: ApPersonService, ) { } @bindThis - public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise { - const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href)); + public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver) { + const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href as string)); - const limit = promiseLimit(2); + const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))), - )).filter(x => x != null); - + )).filter((x): x is User => x != null); + return mentionedUsers; } - + @bindThis public extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] { if (tags == null) return []; diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 8abacd293f..76757f530a 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -1,19 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import promiseLimit from 'promise-limit'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { PollsRepository, EmojisRepository, MiMeta } from '@/models/_.js'; +import type { PollsRepository, EmojisRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { MiRemoteUser } from '@/models/User.js'; -import type { MiNote } from '@/models/Note.js'; +import type { RemoteUser } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; import { toArray, toSingle, unique } from '@/misc/prelude/array.js'; -import type { MiEmoji } from '@/models/Emoji.js'; +import type { Emoji } from '@/models/entities/Emoji.js'; +import { MetaService } from '@/core/MetaService.js'; import { AppLockService } from '@/core/AppLockService.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; import type Logger from '@/logger.js'; import { IdService } from '@/core/IdService.js'; @@ -22,8 +19,8 @@ import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; +// eslint-disable-next-line @typescript-eslint/consistent-type-imports import { ApLoggerService } from '../ApLoggerService.js'; import { ApMfmService } from '../ApMfmService.js'; import { ApDbResolverService } from '../ApDbResolverService.js'; @@ -45,9 +42,6 @@ export class ApNoteService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, @@ -61,12 +55,13 @@ export class ApNoteService { // 循環参照のため / for circular dependency @Inject(forwardRef(() => ApPersonService)) private apPersonService: ApPersonService, - + private utilityService: UtilityService, private apAudienceService: ApAudienceService, private apMentionService: ApMentionService, private apImageService: ApImageService, private apQuestionService: ApQuestionService, + private metaService: MetaService, private appLockService: AppLockService, private pollService: PollService, private noteCreateService: NoteCreateService, @@ -77,108 +72,171 @@ export class ApNoteService { } @bindThis - public validateNote(object: IObject, uri: string, actor?: MiRemoteUser): Error | null { + public validateNote(object: IObject, uri: string) { const expectHost = this.utilityService.extractDbHost(uri); - const apType = getApType(object); - - if (apType == null || !validPost.includes(apType)) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${apType ?? 'undefined'}`); + + if (object == null) { + return new Error('invalid Note: object is null'); } - + + if (!validPost.includes(getApType(object))) { + return new Error(`invalid Note: invalid object type ${getApType(object)}`); + } + if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); + return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); } const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo)); if (object.attributedTo && actualHost !== expectHost) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); + return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); } - - if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed'); - } - - if (actor) { - const attribution = (object.attributedTo) ? getOneApId(object.attributedTo) : actor.uri; - - if (attribution !== actor.uri) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attribution does not match the actor that send it. attribution: ${attribution}, actor: ${actor.uri}`); - } - } - + return null; } - + /** * Noteをフェッチします。 * * Misskeyに対象のNoteが登録されていればそれを返します。 */ @bindThis - public async fetchNote(object: string | IObject): Promise { + public async fetchNote(object: string | IObject): Promise { return await this.apDbResolverService.getNoteFromApId(object); } - + /** * Noteを作成します。 */ @bindThis - public async createNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise { - // eslint-disable-next-line no-param-reassign + public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { if (resolver == null) resolver = this.apResolverService.createResolver(); - + const object = await resolver.resolve(value); - + const entryUri = getApId(value); - const err = this.validateNote(object, entryUri, actor); + const err = this.validateNote(object, entryUri); if (err) { - this.logger.error(err.message, { - resolver: { history: resolver.getHistory() }, - value, - object, + this.logger.error(`${err.message}`, { + resolver: { + history: resolver.getHistory(), + }, + value: value, + object: object, }); - throw err; + throw new Error('invalid note'); } - + const note = object as IPost; - + this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); - if (note.id == null) { - throw new Error('Refusing to create note without id'); - } - - if (!checkHttps(note.id)) { - throw new Error('unexpected schema of note.id: ' + note.id); + if (note.id && !checkHttps(note.id)) { + throw new Error('unexpected shcema of note.id: ' + note.id); } const url = getOneApHrefNullable(note.url); if (url && !checkHttps(url)) { - throw new Error('unexpected schema of note url: ' + url); + throw new Error('unexpected shcema of note url: ' + url); } - + this.logger.info(`Creating the Note: ${note.id}`); - + // 投稿者をフェッチ - if (note.attributedTo == null) { - throw new Error('invalid note.attributedTo: ' + note.attributedTo); + const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser; + + // 投稿者が凍結されていたらスキップ + if (actor.isSuspended) { + throw new Error('actor has been suspended'); } - - const uri = getOneApId(note.attributedTo); - - // ローカルで投稿者を検索し、もし凍結されていたらスキップ - // eslint-disable-next-line no-param-reassign - actor ??= await this.apPersonService.fetchPerson(uri) as MiRemoteUser | undefined; - if (actor && actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); + + const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); + let visibility = noteAudience.visibility; + const visibleUsers = noteAudience.visibleUsers; + + // Audience (to, cc) が指定されてなかった場合 + if (visibility === 'specified' && visibleUsers.length === 0) { + if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している + // こちらから匿名GET出来たものならばpublic + visibility = 'public'; + } } - + const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); - const apHashtags = extractApHashtags(note.tag); - + const apHashtags = await extractApHashtags(note.tag); + + // 添付ファイル + // TODO: attachmentは必ずしもImageではない + // TODO: attachmentは必ずしも配列ではない + // Noteがsensitiveなら添付もsensitiveにする + const limit = promiseLimit(2); + + note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : []; + const files = note.attachment + .map(attach => attach.sensitive = note.sensitive) + ? (await Promise.all(note.attachment.map(x => limit(() => this.apImageService.resolveImage(actor, x)) as Promise))) + .filter(image => image != null) + : []; + + // リプライ + const reply: Note | null = note.inReplyTo + ? await this.resolveNote(note.inReplyTo, resolver).then(x => { + if (x == null) { + this.logger.warn('Specified inReplyTo, but not found'); + throw new Error('inReplyTo not found'); + } else { + return x; + } + }).catch(async err => { + this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); + throw err; + }) + : null; + + // 引用 + let quote: Note | undefined | null; + + if (note._misskey_quote || note.quoteUrl) { + const tryResolveNote = async (uri: string): Promise<{ + status: 'ok'; + res: Note | null; + } | { + status: 'permerror' | 'temperror'; + }> => { + if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' }; + try { + const res = await this.resolveNote(uri); + if (res) { + return { + status: 'ok', + res, + }; + } else { + return { + status: 'permerror', + }; + } + } catch (e) { + return { + status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror', + }; + } + }; + + const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string')); + const results = await Promise.all(uris.map(uri => tryResolveNote(uri))); + + quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x); + if (!quote) { + if (results.some(x => x.status === 'temperror')) { + throw new Error('quote resolve failed'); + } + } + } + const cw = note.summary === '' ? null : note.summary; - + // テキストのパース let text: string | null = null; if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { @@ -188,158 +246,58 @@ export class ApNoteService { } else if (typeof note.content === 'string') { text = this.apMfmService.htmlToMfm(note.content, note.tag); } - - const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); - - //#region Contents Check - // 添付ファイルとユーザーをこのサーバーで登録する前に内容をチェックする - /** - * 禁止ワードチェック - */ - const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); - if (hasProhibitedWords) { - throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); - } - //#endregion - - // eslint-disable-next-line no-param-reassign - actor ??= await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser; - - // 解決した投稿者が凍結されていたらスキップ - if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); - } - - const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); - let visibility = noteAudience.visibility; - const visibleUsers = noteAudience.visibleUsers; - - // Audience (to, cc) が指定されてなかった場合 - if (visibility === 'specified' && visibleUsers.length === 0) { - if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している - // こちらから匿名GET出来たものならばpublic - visibility = 'public'; - } - } - - // 添付ファイル - const files: MiDriveFile[] = []; - - for (const attach of toArray(note.attachment)) { - attach.sensitive ??= note.sensitive; - const file = await this.apImageService.resolveImage(actor, attach); - if (file) files.push(file); - } - - // リプライ - const reply: MiNote | null = note.inReplyTo - ? await this.resolveNote(note.inReplyTo, { resolver }) - .then(x => { - if (x == null) { - this.logger.warn('Specified inReplyTo, but not found'); - throw new Error('inReplyTo not found'); - } - - return x; - }) - .catch(async err => { - this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); - throw err; - }) - : null; - - // 引用 - let quote: MiNote | undefined | null = null; - - if (note._misskey_quote ?? note.quoteUrl) { - const tryResolveNote = async (uri: string): Promise< - | { status: 'ok'; res: MiNote } - | { status: 'permerror' | 'temperror' } - > => { - if (!/^https?:/.test(uri)) return { status: 'permerror' }; - try { - const res = await this.resolveNote(uri); - if (res == null) return { status: 'permerror' }; - return { status: 'ok', res }; - } catch (e) { - return { - status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror', - }; - } - }; - - const uris = unique([note._misskey_quote, note.quoteUrl].filter(x => x != null)); - const results = await Promise.all(uris.map(tryResolveNote)); - - quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); - if (!quote) { - if (results.some(x => x.status === 'temperror')) { - throw new Error('quote resolve failed'); - } - } - } - + // vote if (reply && reply.hasPoll) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); - + const tryCreateVote = async (name: string, index: number): Promise => { if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) { this.logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); } else if (index >= 0) { this.logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); await this.pollService.vote(actor, reply, index); - + // リモートフォロワーにUpdate配信 this.pollService.deliverQuestionUpdate(reply.id); } return null; }; - + if (note.name) { return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name)); } } - + const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => { this.logger.info(`extractEmojis: ${e}`); - return []; + return [] as Emoji[]; }); - + const apEmojis = emojis.map(emoji => emoji.name); - - try { - return await this.noteCreateService.create(actor, { - createdAt: note.published ? new Date(note.published) : null, - files, - reply, - renote: quote, - name: note.name, - cw, - text, - localOnly: false, - visibility, - visibleUsers, - apMentions, - apHashtags, - apEmojis, - poll, - uri: note.id, - url: url, - }, silent); - } catch (err: any) { - if (err.name !== 'duplicated') { - throw err; - } - this.logger.info('The note is already inserted while creating itself, reading again'); - const duplicate = await this.fetchNote(value); - if (!duplicate) { - throw new Error('The note creation failed with duplication error even when there is no duplication'); - } - return duplicate; - } + + const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); + + return await this.noteCreateService.create(actor, { + createdAt: note.published ? new Date(note.published) : null, + files, + reply, + renote: quote, + name: note.name, + cw, + text, + localOnly: false, + visibility, + visibleUsers, + apMentions, + apHashtags, + apEmojis, + poll, + uri: note.id, + url: url, + }, silent); } - + /** * Noteを解決します。 * @@ -347,93 +305,94 @@ export class ApNoteService { * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise { - const uri = getApId(value); - - if (!this.utilityService.isFederationAllowedUri(uri)) { - throw new StatusError('blocked host', 451); - } - + public async resolveNote(value: string | IObject, resolver?: Resolver): Promise { + const uri = typeof value === 'string' ? value : value.id; + if (uri == null) throw new Error('missing uri'); + + // ブロックしてたら中断 + const meta = await this.metaService.fetch(); + if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451); + const unlock = await this.appLockService.getApLock(uri); - + try { //#region このサーバーに既に登録されていたらそれを返す const exist = await this.fetchNote(uri); - if (exist) return exist; + + if (exist) { + return exist; + } //#endregion - - if (this.utilityService.isUriLocal(uri)) { + + if (uri.startsWith(this.config.url)) { throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note'); } - + // リモートサーバーからフェッチしてきて登録 // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 - const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri; - return await this.createNote(createFrom, undefined, options.resolver, true); + return await this.createNote(uri, resolver, true); } finally { unlock(); } } - + @bindThis - public async extractEmojis(tags: IObject | IObject[], host: string): Promise { - // eslint-disable-next-line no-param-reassign + public async extractEmojis(tags: IObject | IObject[], host: string): Promise { host = this.utilityService.toPuny(host); - + + if (!tags) return []; + const eomjiTags = toArray(tags).filter(isEmoji); const existingEmojis = await this.emojisRepository.findBy({ host, - name: In(eomjiTags.map(tag => tag.name.replaceAll(':', ''))), + name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))), }); - + return await Promise.all(eomjiTags.map(async tag => { - const name = tag.name.replaceAll(':', ''); + const name = tag.name!.replaceAll(':', ''); tag.icon = toSingle(tag.icon); - + const exists = existingEmojis.find(x => x.name === name); - + if (exists) { - if ((exists.updatedAt == null) + if ((tag.updated != null && exists.updatedAt == null) || (tag.id != null && exists.uri == null) - || (new Date(tag.updated) > exists.updatedAt) - || (tag.icon.url !== exists.originalUrl) + || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt) + || (tag.icon!.url !== exists.originalUrl) ) { await this.emojisRepository.update({ host, name, }, { uri: tag.id, - originalUrl: tag.icon.url, - publicUrl: tag.icon.url, + originalUrl: tag.icon!.url, + publicUrl: tag.icon!.url, updatedAt: new Date(), - // _misskey_license が存在しなければ `null` - license: (tag._misskey_license?.freeText ?? null) }); - - const emoji = await this.emojisRepository.findOneBy({ host, name }); - if (emoji == null) throw new Error('emoji update failed'); - return emoji; + + return await this.emojisRepository.findOneBy({ + host, + name, + }) as Emoji; } - + return exists; } - + this.logger.info(`register emoji host=${host}, name=${name}`); - - return await this.emojisRepository.insertOne({ - id: this.idService.gen(), + + return await this.emojisRepository.insert({ + id: this.idService.genId(), host, name, uri: tag.id, - originalUrl: tag.icon.url, - publicUrl: tag.icon.url, + originalUrl: tag.icon!.url, + publicUrl: tag.icon!.url, updatedAt: new Date(), aliases: [], - // _misskey_license が存在しなければ `null` - license: (tag._misskey_license?.freeText ?? null) - }); + } as Partial).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); })); } } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index e52078ed0f..f52ebed107 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -1,40 +1,36 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; import { DataSource } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; +import type { BlockingsRepository, MutingsRepository, FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; -import { MiUser } from '@/models/User.js'; +import type { LocalUser, RemoteUser } from '@/models/entities/User.js'; +import { User } from '@/models/entities/User.js'; import { truncate } from '@/misc/truncate.js'; import type { CacheService } from '@/core/CacheService.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import type Logger from '@/logger.js'; -import type { MiNote } from '@/models/Note.js'; +import type { Note } from '@/models/entities/Note.js'; import type { IdService } from '@/core/IdService.js'; import type { MfmService } from '@/core/MfmService.js'; +import type { Emoji } from '@/models/entities/Emoji.js'; import { toArray } from '@/misc/prelude/array.js'; import type { GlobalEventService } from '@/core/GlobalEventService.js'; import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import type { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; -import { MiUserProfile } from '@/models/UserProfile.js'; -import { MiUserPublickey } from '@/models/UserPublickey.js'; +import { UserProfile } from '@/models/entities/UserProfile.js'; +import { UserPublickey } from '@/models/entities/UserPublickey.js'; import type UsersChart from '@/core/chart/charts/users.js'; import type InstanceChart from '@/core/chart/charts/instance.js'; import type { HashtagService } from '@/core/HashtagService.js'; -import { MiUserNotePining } from '@/models/UserNotePining.js'; +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 { RoleService } from '@/core/RoleService.js'; +import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js'; import { checkHttps } from '@/misc/check-https.js'; @@ -45,15 +41,13 @@ import type { ApNoteService } from './ApNoteService.js'; import type { ApMfmService } from '../ApMfmService.js'; import type { ApResolverService, Resolver } from '../ApResolverService.js'; import type { ApLoggerService } from '../ApLoggerService.js'; - +// eslint-disable-next-line @typescript-eslint/consistent-type-imports import type { ApImageService } from './ApImageService.js'; -import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js'; +import type { IActor, IObject } from '../type.js'; const nameLength = 128; const summaryLength = 2048; -type Field = Record<'name' | 'value', string>; - @Injectable() export class ApPersonService implements OnModuleInit { private utilityService: UtilityService; @@ -61,6 +55,7 @@ export class ApPersonService implements OnModuleInit { private driveFileEntityService: DriveFileEntityService; private idService: IdService; private globalEventService: GlobalEventService; + private metaService: MetaService; private federatedInstanceService: FederatedInstanceService; private fetchInstanceMetadataService: FetchInstanceMetadataService; private cacheService: CacheService; @@ -82,9 +77,6 @@ export class ApPersonService implements OnModuleInit { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.db) private db: DataSource, @@ -103,16 +95,33 @@ export class ApPersonService implements OnModuleInit { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - private roleService: RoleService, + //private utilityService: UtilityService, + //private userEntityService: UserEntityService, + //private idService: IdService, + //private globalEventService: GlobalEventService, + //private metaService: MetaService, + //private federatedInstanceService: FederatedInstanceService, + //private fetchInstanceMetadataService: FetchInstanceMetadataService, + //private cacheService: CacheService, + //private apResolverService: ApResolverService, + //private apNoteService: ApNoteService, + //private apImageService: ApImageService, + //private apMfmService: ApMfmService, + //private mfmService: MfmService, + //private hashtagService: HashtagService, + //private usersChart: UsersChart, + //private instanceChart: InstanceChart, + //private apLoggerService: ApLoggerService, ) { } - onModuleInit(): void { + onModuleInit() { this.utilityService = this.moduleRef.get('UtilityService'); this.userEntityService = this.moduleRef.get('UserEntityService'); this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.idService = this.moduleRef.get('IdService'); this.globalEventService = this.moduleRef.get('GlobalEventService'); + this.metaService = this.moduleRef.get('MetaService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService'); this.cacheService = this.moduleRef.get('CacheService'); @@ -129,6 +138,12 @@ export class ApPersonService implements OnModuleInit { this.logger = this.apLoggerService.logger; } + private punyHost(url: string): string { + const urlObj = new URL(url); + const host = `${this.utilityService.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`; + return host; + } + /** * Validate and convert to actor object * @param x Fetched object @@ -136,7 +151,11 @@ export class ApPersonService implements OnModuleInit { */ @bindThis private validateActor(x: IObject, uri: string): IActor { - const expectHost = this.utilityService.punyHost(uri); + const expectHost = this.punyHost(uri); + + if (x == null) { + throw new Error('invalid Actor: object is null'); + } if (!isActor(x)) { throw new Error(`invalid Actor type '${x.type}'`); @@ -150,36 +169,6 @@ export class ApPersonService implements OnModuleInit { throw new Error('invalid Actor: wrong inbox'); } - if (this.utilityService.punyHost(x.inbox) !== expectHost) { - throw new Error('invalid Actor: inbox has different host'); - } - - const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined); - if (sharedInboxObject != null) { - const sharedInbox = getApId(sharedInboxObject); - if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && new URL(sharedInbox).host === expectHost)) { - this.logger.warn(`invalid Actor: skipping wrong shared inbox, expected host: ${expectHost}, actual URL: ${sharedInbox}`); - x.sharedInbox = undefined; - if (x.endpoints?.sharedInbox) { - x.endpoints.sharedInbox = undefined; - } - } - } - - for (const collection of ['outbox', 'followers', 'following'] as (keyof IActor)[]) { - const xCollection = (x as IActor)[collection]; - if (xCollection != null) { - const collectionUri = getApId(xCollection); - if (typeof collectionUri === 'string' && collectionUri.length > 0) { - if (this.utilityService.punyHost(collectionUri) !== expectHost) { - throw new Error(`invalid Actor: ${collection} has different host`); - } - } else if (collectionUri != null) { - throw new Error(`invalid Actor: wrong ${collection}`); - } - } - } - if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) { throw new Error('invalid Actor: wrong username'); } @@ -203,7 +192,7 @@ export class ApPersonService implements OnModuleInit { x.summary = truncate(x.summary, summaryLength); } - const idHost = this.utilityService.punyHost(x.id); + const idHost = this.punyHost(x.id); if (idHost !== expectHost) { throw new Error('invalid Actor: id has different host'); } @@ -213,7 +202,7 @@ export class ApPersonService implements OnModuleInit { throw new Error('invalid Actor: publicKey.id is not a string'); } - const publicKeyIdHost = this.utilityService.punyHost(x.publicKey.id); + const publicKeyIdHost = this.punyHost(x.publicKey.id); if (publicKeyIdHost !== expectHost) { throw new Error('invalid Actor: publicKey.id has different host'); } @@ -228,20 +217,22 @@ export class ApPersonService implements OnModuleInit { * Misskeyに対象のPersonが登録されていればそれを返し、登録がなければnullを返します。 */ @bindThis - public async fetchPerson(uri: string): Promise { - const cached = this.cacheService.uriPersonCache.get(uri) as MiLocalUser | MiRemoteUser | null | undefined; + public async fetchPerson(uri: string): Promise { + if (typeof uri !== 'string') throw new Error('uri is not string'); + + const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null; if (cached) return cached; // URIがこのサーバーを指しているならデータベースからフェッチ if (uri.startsWith(`${this.config.url}/`)) { const id = uri.split('/').pop(); - const u = await this.usersRepository.findOneBy({ id }) as MiLocalUser | null; + const u = await this.usersRepository.findOneBy({ id }) as LocalUser; if (u) this.cacheService.uriPersonCache.set(uri, u); return u; } //#region このサーバーに既に登録されていたらそれを返す - const exist = await this.usersRepository.findOneBy({ uri }) as MiLocalUser | MiRemoteUser | null; + const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser; if (exist) { this.cacheService.uriPersonCache.set(uri, exist); @@ -252,172 +243,83 @@ export class ApPersonService implements OnModuleInit { return null; } - private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise>> { - if (user == null) throw new Error('failed to create user: user is null'); - - const [avatar, banner] = await Promise.all([icon, image].map(img => { - // icon and image may be arrays - // see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon - if (Array.isArray(img)) { - img = img.find(item => item && item.url) ?? null; - } - - // if we have an explicitly missing image, return an - // explicitly-null set of values - if ((img == null) || (typeof img === 'object' && img.url == null)) { - return { id: null, url: null, blurhash: null }; - } - - return this.apImageService.resolveImage(user, img).catch(() => null); - })); - - if (((avatar != null && avatar.id != null) || (banner != null && banner.id != null)) - && !(await this.roleService.getUserPolicies(user.id)).canUpdateBioMedia) { - return {}; - } - - /* - we don't want to return nulls on errors! if the database fields - are already null, nothing changes; if the database has old - values, we should keep those. The exception is if the remote has - actually removed the images: in that case, the block above - returns the special {id:null}&c value, and we return those - */ - return { - ...( avatar ? { - avatarId: avatar.id, - avatarUrl: avatar.url ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null, - avatarBlurhash: avatar.blurhash, - } : {}), - ...( banner ? { - bannerId: banner.id, - bannerUrl: banner.url ? this.driveFileEntityService.getPublicUrl(banner) : null, - bannerBlurhash: banner.blurhash, - } : {}), - }; - } - /** * Personを作成します。 */ @bindThis - public async createPerson(uri: string, resolver?: Resolver): Promise { + public async createPerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); - const host = this.utilityService.punyHost(uri); - if (host === this.utilityService.toPuny(this.config.host)) { + if (uri.startsWith(this.config.url)) { throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); } - // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); - const object = await resolver.resolve(uri); - if (object.id == null) throw new Error('invalid object.id: ' + object.id); + const object = await resolver.resolve(uri) as any; const person = this.validateActor(object, uri); this.logger.info(`Creating the Person: ${person.id}`); - const fields = this.analyzeAttachments(person.attachment ?? []); + const host = this.punyHost(object.id); - const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32); + const { fields } = this.analyzeAttachments(person.attachment ?? []); - const isBot = getApType(object) === 'Service' || getApType(object) === 'Application'; + const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32); - const [followingVisibility, followersVisibility] = await Promise.all( - [ - this.isPublicCollection(person.following, resolver), - this.isPublicCollection(person.followers, resolver), - ].map((p): Promise<'public' | 'private'> => p - .then(isPublic => isPublic ? 'public' : 'private') - .catch(err => { - if (!(err instanceof StatusError) || err.isRetryable) { - this.logger.error('error occurred while fetching following/followers collection', { stack: err }); - } - return 'private'; - }), - ), - ); + const isBot = getApType(object) === 'Service'; const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); const url = getOneApHrefNullable(person.url); - if (person.id == null) { - throw new Error('Refusing to create person without id'); - } - if (url && !checkHttps(url)) { throw new Error('unexpected schema of person url: ' + url); } // Create user - let user: MiRemoteUser | null = null; - - //#region カスタム絵文字取得 - const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host) - .then(_emojis => _emojis.map(emoji => emoji.name)) - .catch(err => { - this.logger.error('error occurred while fetching user emojis', { stack: err }); - return []; - }); - //#endregion - + let user: RemoteUser; try { - // Start transaction + // Start transaction await this.db.transaction(async transactionalEntityManager => { - user = await transactionalEntityManager.save(new MiUser({ - id: this.idService.gen(), + user = await transactionalEntityManager.save(new User({ + id: this.idService.genId(), avatarId: null, bannerId: null, + createdAt: new Date(), lastFetchedAt: new Date(), name: truncate(person.name, nameLength), - isLocked: person.manuallyApprovesFollowers, + isLocked: !!person.manuallyApprovesFollowers, movedToUri: person.movedTo, movedAt: person.movedTo ? new Date() : null, alsoKnownAs: person.alsoKnownAs, - isExplorable: person.discoverable, + isExplorable: !!person.discoverable, username: person.preferredUsername, - usernameLower: person.preferredUsername?.toLowerCase(), + usernameLower: person.preferredUsername!.toLowerCase(), host, inbox: person.inbox, - sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null, + sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined), followersUri: person.followers ? getApId(person.followers) : undefined, featured: person.featured ? getApId(person.featured) : undefined, uri: person.id, tags, isBot, isCat: (person as any).isCat === true, - requireSigninToViewContents: (person as any).requireSigninToViewContents === true, - makeNotesFollowersOnlyBefore: (person as any).makeNotesFollowersOnlyBefore ?? null, - makeNotesHiddenBefore: (person as any).makeNotesHiddenBefore ?? null, - emojis, - })) as MiRemoteUser; + })) as RemoteUser; - let _description: string | null = null; - - if (person._misskey_summary) { - _description = truncate(person._misskey_summary, summaryLength); - } else if (person.summary) { - _description = this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag); - } - - await transactionalEntityManager.save(new MiUserProfile({ + await transactionalEntityManager.save(new UserProfile({ userId: user.id, - description: _description, - followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null, - url, + description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, + url: url, fields, - followingVisibility, - followersVisibility, - birthday: bday?.[0] ?? null, + birthday: bday ? bday[0] : null, location: person['vcard:Address'] ?? null, userHost: host, })); if (person.publicKey) { - await transactionalEntityManager.save(new MiUserPublickey({ + await transactionalEntityManager.save(new UserPublickey({ userId: user.id, keyId: person.publicKey.id, keyPem: person.publicKey.publicKeyPem, @@ -425,63 +327,95 @@ export class ApPersonService implements OnModuleInit { } }); } catch (e) { - // duplicate key error + // duplicate key error if (isDuplicateKeyValueError(e)) { - // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 - const u = await this.usersRepository.findOneBy({ uri: person.id }); - if (u == null) throw new Error('already registered'); + // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 + const u = await this.usersRepository.findOneBy({ + uri: person.id, + }); - user = u as MiRemoteUser; + if (u) { + user = u as RemoteUser; + } else { + throw new Error('already registered'); + } } else { this.logger.error(e instanceof Error ? e : new Error(e as string)); throw e; } } - if (user == null) throw new Error('failed to create user: user is null'); - - // Register to the cache - this.cacheService.uriPersonCache.set(user.uri, user); - // Register host - if (this.meta.enableStatsForFederatedInstances) { - this.federatedInstanceService.fetchOrRegister(host).then(i => { - this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); - if (this.meta.enableChartsForFederatedInstances) { - this.instanceChart.newUser(i.host); - } - this.fetchInstanceMetadataService.fetchInstanceMetadata(i); - }); - } + this.federatedInstanceService.fetch(host).then(async i => { + this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); + this.fetchInstanceMetadataService.fetchInstanceMetadata(i); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.newUser(i.host); + } + }); - this.usersChart.update(user, true); + this.usersChart.update(user!, true); // ハッシュタグ更新 - this.hashtagService.updateUsertags(user, tags); + this.hashtagService.updateUsertags(user!, tags); //#region アバターとヘッダー画像をフェッチ - try { - const updates = await this.resolveAvatarAndBanner(user, person.icon, person.image); - await this.usersRepository.update(user.id, updates); - user = { ...user, ...updates }; + const [avatar, banner] = await Promise.all([ + person.icon, + person.image, + ].map(img => + img == null + ? Promise.resolve(null) + : this.apImageService.resolveImage(user!, img).catch(() => null), + )); - // Register to the cache - this.cacheService.uriPersonCache.set(user.uri, user); - } catch (err) { - this.logger.error('error occurred while fetching user avatar/banner', { stack: err }); - } + const avatarId = avatar ? avatar.id : null; + const bannerId = banner ? banner.id : null; + const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null; + const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null; + const avatarBlurhash = avatar ? avatar.blurhash : null; + const bannerBlurhash = banner ? banner.blurhash : null; + + await this.usersRepository.update(user!.id, { + avatarId, + bannerId, + avatarUrl, + bannerUrl, + avatarBlurhash, + bannerBlurhash, + }); + + user!.avatarId = avatarId; + user!.bannerId = bannerId; + user!.avatarUrl = avatarUrl; + user!.bannerUrl = bannerUrl; + user!.avatarBlurhash = avatarBlurhash; + user!.bannerBlurhash = bannerBlurhash; //#endregion - await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err)); + //#region カスタム絵文字取得 + const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => { + this.logger.info(`extractEmojis: ${err}`); + return [] as Emoji[]; + }); - return user; + const emojiNames = emojis.map(emoji => emoji.name); + + await this.usersRepository.update(user!.id, { + emojis: emojiNames, + }); + //#endregion + + await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err)); + + return user!; } /** * Personの情報を更新します。 * Misskeyに対象のPersonが登録されていなければ無視します。 * もしアカウントの移行が確認された場合、アカウント移行処理を行います。 - * + * * @param uri URI of Person * @param resolver Resolver * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します) @@ -492,14 +426,18 @@ export class ApPersonService implements OnModuleInit { if (typeof uri !== 'string') throw new Error('uri is not string'); // URIがこのサーバーを指しているならスキップ - if (this.utilityService.isUriLocal(uri)) return; + if (uri.startsWith(`${this.config.url}/`)) { + return; + } //#region このサーバーに既に登録されているか - const exist = await this.fetchPerson(uri) as MiRemoteUser | null; - if (exist === null) return; + const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null; + + if (exist === null) { + return; + } //#endregion - // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); const object = hint ?? await resolver.resolve(uri); @@ -508,96 +446,77 @@ export class ApPersonService implements OnModuleInit { this.logger.info(`Updating the Person: ${person.id}`); + // アバターとヘッダー画像をフェッチ + const [avatar, banner] = await Promise.all([ + person.icon, + person.image, + ].map(img => + img == null + ? Promise.resolve(null) + : this.apImageService.resolveImage(exist, img).catch(() => null), + )); + // カスタム絵文字取得 const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => { this.logger.info(`extractEmojis: ${e}`); - return []; + return [] as Emoji[]; }); const emojiNames = emojis.map(emoji => emoji.name); - const fields = this.analyzeAttachments(person.attachment ?? []); + const { fields } = this.analyzeAttachments(person.attachment ?? []); - const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32); - - const [followingVisibility, followersVisibility] = await Promise.all( - [ - this.isPublicCollection(person.following, resolver), - this.isPublicCollection(person.followers, resolver), - ].map((p): Promise<'public' | 'private' | undefined> => p - .then(isPublic => isPublic ? 'public' : 'private') - .catch(err => { - if (!(err instanceof StatusError) || err.isRetryable) { - this.logger.error('error occurred while fetching following/followers collection', { stack: err }); - // Do not update the visibiility on transient errors. - return undefined; - } - return 'private'; - }), - ), - ); + const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32); const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); const url = getOneApHrefNullable(person.url); - if (person.id == null) { - throw new Error('Refusing to update person without id'); - } - - if (url != null) { - if (!checkHttps(url)) { - throw new Error('unexpected schema of person url: ' + url); - } - - if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { - throw new Error(`person url <> uri host mismatch: ${url} <> ${person.id}`); - } + if (url && !checkHttps(url)) { + throw new Error('unexpected schema of person url: ' + url); } const updates = { lastFetchedAt: new Date(), inbox: person.inbox, - sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null, + sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined), followersUri: person.followers ? getApId(person.followers) : undefined, - featured: person.featured ? getApId(person.featured) : undefined, + featured: person.featured, emojis: emojiNames, name: truncate(person.name, nameLength), tags, - isBot: getApType(object) === 'Service' || getApType(object) === 'Application', + isBot: getApType(object) === 'Service', isCat: (person as any).isCat === true, - isLocked: person.manuallyApprovesFollowers, + isLocked: !!person.manuallyApprovesFollowers, movedToUri: person.movedTo ?? null, alsoKnownAs: person.alsoKnownAs ?? null, - isExplorable: person.discoverable, - ...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))), - } as Partial & Pick; + isExplorable: !!person.discoverable, + } as Partial & Pick; - const moving = ((): boolean => { + const moving = // 移行先がない→ある - if ( - exist.movedToUri === null && - updates.movedToUri - ) return true; - + (!exist.movedToUri && updates.movedToUri) || // 移行先がある→別のもの - if ( - exist.movedToUri !== null && - updates.movedToUri !== null && - exist.movedToUri !== updates.movedToUri - ) return true; - + (exist.movedToUri !== updates.movedToUri && exist.movedToUri && updates.movedToUri); // 移行先がある→ない、ない→ないは無視 - return false; - })(); if (moving) updates.movedAt = new Date(); - // Update user - if (!(await this.usersRepository.update({ id: exist.id, isDeleted: false }, updates)).affected) { - return 'skip'; + if (avatar) { + updates.avatarId = avatar.id; + updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar'); + updates.avatarBlurhash = avatar.blurhash; } + if (banner) { + updates.bannerId = banner.id; + updates.bannerUrl = this.driveFileEntityService.getPublicUrl(banner); + updates.bannerBlurhash = banner.blurhash; + } + + // Update user + await this.usersRepository.update(exist.id, updates); + if (person.publicKey) { await this.userPublickeysRepository.update({ userId: exist.id }, { keyId: person.publicKey.id, @@ -605,22 +524,11 @@ export class ApPersonService implements OnModuleInit { }); } - let _description: string | null = null; - - if (person._misskey_summary) { - _description = truncate(person._misskey_summary, summaryLength); - } else if (person.summary) { - _description = this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag); - } - await this.userProfilesRepository.update({ userId: exist.id }, { - url, + url: url, fields, - description: _description, - followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null, - followingVisibility, - followersVisibility, - birthday: bday?.[0] ?? null, + description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, + birthday: bday ? bday[0] : null, location: person['vcard:Address'] ?? null, }); @@ -630,10 +538,11 @@ export class ApPersonService implements OnModuleInit { this.hashtagService.updateUsertags(exist, tags); // 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする - await this.followingsRepository.update( - { followerId: exist.id }, - { followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null }, - ); + await this.followingsRepository.update({ + followerId: exist.id, + }, { + followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined), + }); await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err)); @@ -670,23 +579,28 @@ export class ApPersonService implements OnModuleInit { * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolvePerson(uri: string, resolver?: Resolver): Promise { + public async resolvePerson(uri: string, resolver?: Resolver): Promise { + if (typeof uri !== 'string') throw new Error('uri is not string'); + //#region このサーバーに既に登録されていたらそれを返す const exist = await this.fetchPerson(uri); - if (exist) return exist; + + if (exist) { + return exist; + } //#endregion // リモートサーバーからフェッチしてきて登録 - // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); return await this.createPerson(uri, resolver); } @bindThis - // TODO: `attachments`が`IObject`だった場合、返り値が`[]`になるようだが構わないのか? - public analyzeAttachments(attachments: IObject | IObject[] | undefined): Field[] { - const fields: Field[] = []; - + public analyzeAttachments(attachments: IObject | IObject[] | undefined) { + const fields: { + name: string, + value: string + }[] = []; if (Array.isArray(attachments)) { for (const attachment of attachments.filter(isPropertyValue)) { fields.push({ @@ -696,12 +610,12 @@ export class ApPersonService implements OnModuleInit { } } - return fields; + return { fields }; } @bindThis - public async updateFeatured(userId: MiUser['id'], resolver?: Resolver): Promise { - const user = await this.usersRepository.findOneByOrFail({ id: userId, isDeleted: false }); + public async updateFeatured(userId: User['id'], resolver?: Resolver) { + const user = await this.usersRepository.findOneByOrFail({ id: userId }); if (!this.userEntityService.isRemoteUser(user)) return; if (!user.featured) return; @@ -718,26 +632,24 @@ export class ApPersonService implements OnModuleInit { const items = await Promise.all(toArray(unresolvedItems).map(x => _resolver.resolve(x))); // Resolve and regist Notes - const limit = promiseLimit(2); + const limit = promiseLimit(2); const featuredNotes = await Promise.all(items .filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも .slice(0, 5) - .map(item => limit(() => this.apNoteService.resolveNote(item, { - resolver: _resolver, - sentFrom: new URL(user.uri), - })))); + .map(item => limit(() => this.apNoteService.resolveNote(item, _resolver)))); await this.db.transaction(async transactionalEntityManager => { - await transactionalEntityManager.delete(MiUserNotePining, { userId: user.id }); + await transactionalEntityManager.delete(UserNotePining, { userId: user.id }); // とりあえずidを別の時間で生成して順番を維持 let td = 0; - for (const note of featuredNotes.filter(x => x != null)) { + for (const note of featuredNotes.filter(note => note != null)) { td -= 1000; - transactionalEntityManager.insert(MiUserNotePining, { - id: this.idService.gen(Date.now() + td), + transactionalEntityManager.insert(UserNotePining, { + id: this.idService.genId(new Date(Date.now() + td)), + createdAt: new Date(), userId: user.id, - noteId: note.id, + noteId: note!.id, }); } }); @@ -749,7 +661,7 @@ export class ApPersonService implements OnModuleInit { * @param movePreventUris ここに列挙されたURIにsrc.movedToUriが含まれる場合、移行処理はしない(無限ループ防止) */ @bindThis - private async processRemoteMove(src: MiRemoteUser, movePreventUris: string[] = []): Promise { + private async processRemoteMove(src: RemoteUser, movePreventUris: string[] = []): Promise { if (!src.movedToUri) return 'skip: no movedToUri'; if (src.uri === src.movedToUri) return 'skip: movedTo itself (src)'; // ??? if (movePreventUris.length > 10) return 'skip: too many moves'; @@ -759,7 +671,7 @@ export class ApPersonService implements OnModuleInit { if (dst && this.userEntityService.isLocalUser(dst)) { // targetがローカルユーザーだった場合データベースから引っ張ってくる - dst = await this.usersRepository.findOneByOrFail({ uri: src.movedToUri }) as MiLocalUser; + dst = await this.usersRepository.findOneByOrFail({ uri: src.movedToUri }) as LocalUser; } else if (dst) { if (movePreventUris.includes(src.movedToUri)) return 'skip: circular move'; @@ -767,7 +679,7 @@ export class ApPersonService implements OnModuleInit { await this.updatePerson(src.movedToUri, undefined, undefined, [...movePreventUris, src.uri]); dst = await this.fetchPerson(src.movedToUri) ?? dst; } else { - if (this.utilityService.isUriLocal(src.movedToUri)) { + if (src.movedToUri.startsWith(`${this.config.url}/`)) { // ローカルユーザーっぽいのにfetchPersonで見つからないということはmovedToUriが間違っている return 'failed: movedTo is local but not found'; } @@ -776,7 +688,7 @@ export class ApPersonService implements OnModuleInit { // (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする) dst = await this.resolvePerson(src.movedToUri); } - + if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; // ??? if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; // ??? if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri'; @@ -791,16 +703,4 @@ export class ApPersonService implements OnModuleInit { return 'ok'; } - - @bindThis - private async isPublicCollection(collection: string | ICollection | IOrderedCollection | undefined, resolver: Resolver): Promise { - if (collection) { - const resolved = await resolver.resolveCollection(collection); - if (resolved.first || (resolved as ICollection).items || (resolved as IOrderedCollection).orderedItems) { - return true; - } - } - - return false; - } } diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index a2cdaf02ca..13a2f0fa5c 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -1,22 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, NotesRepository, PollsRepository } from '@/models/_.js'; +import type { NotesRepository, PollsRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { IPoll } from '@/models/Poll.js'; -import type { MiRemoteUser } from '@/models/User.js'; +import type { IPoll } from '@/models/entities/Poll.js'; import type Logger from '@/logger.js'; -import { bindThis } from '@/decorators.js'; -import { getOneApId, isQuestion } from '../type.js'; -import { UtilityService } from '@/core/UtilityService.js'; +import { isQuestion } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApResolverService } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js'; -import type { IObject } from '../type.js'; +import type { IObject, IQuestion } from '../type.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ApQuestionService { @@ -26,9 +19,6 @@ export class ApQuestionService { @Inject(DI.config) private config: Config, - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -37,32 +27,39 @@ export class ApQuestionService { private apResolverService: ApResolverService, private apLoggerService: ApLoggerService, - private utilityService: UtilityService, ) { this.logger = this.apLoggerService.logger; } @bindThis public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise { - // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); const question = await resolver.resolve(source); - if (!isQuestion(question)) throw new Error('invalid type'); - const multiple = question.oneOf === undefined; - if (multiple && question.anyOf === undefined) throw new Error('invalid question'); + if (!isQuestion(question)) { + throw new Error('invalid type'); + } + const multiple = !question.oneOf; const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null; - const choices = question[multiple ? 'anyOf' : 'oneOf'] - ?.map((x) => x.name) - .filter(x => x != null) - ?? []; + if (multiple && !question.anyOf) { + throw new Error('invalid question'); + } - const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0); + const choices = question[multiple ? 'anyOf' : 'oneOf']! + .map((x, i) => x.name!); - return { choices, votes, multiple, expiresAt }; + const votes = question[multiple ? 'anyOf' : 'oneOf']! + .map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0); + + return { + choices, + votes, + multiple, + expiresAt, + }; } /** @@ -71,49 +68,34 @@ export class ApQuestionService { * @returns true if updated */ @bindThis - public async updateQuestion(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver): Promise { + public async updateQuestion(value: any, resolver?: Resolver) { const uri = typeof value === 'string' ? value : value.id; - if (uri == null) throw new Error('uri is null'); // URIがこのサーバーを指しているならスキップ - if (this.utilityService.isUriLocal(uri)) throw new Error('uri points local'); + if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local'); //#region このサーバーに既に登録されているか const note = await this.notesRepository.findOneBy({ uri }); - if (note == null) throw new Error('Question is not registered'); + if (note == null) throw new Error('Question is not registed'); const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); - if (poll == null) throw new Error('Question is not registered'); - - const user = await this.usersRepository.findOneBy({ id: poll.userId }); - if (user == null) throw new Error('Question is not registered'); + if (poll == null) throw new Error('Question is not registed'); //#endregion // resolve new Question object - // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); - const question = await resolver.resolve(value); + const question = await resolver.resolve(value) as IQuestion; this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); - if (!isQuestion(question)) throw new Error('object is not a Question'); - - const attribution = (question.attributedTo) ? getOneApId(question.attributedTo) : user.uri; - const attributionMatchesExisting = attribution === user.uri; - const actorMatchesAttribution = (actor) ? attribution === actor.uri : true; - - if (!attributionMatchesExisting || !actorMatchesAttribution) { - throw new Error('Refusing to ingest update for poll by different user'); - } + if (question.type !== 'Question') throw new Error('object is not a Question'); const apChoices = question.oneOf ?? question.anyOf; - if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices); let changed = false; for (const choice of poll.choices) { const oldCount = poll.votes[poll.choices.indexOf(choice)]; - const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems; - if (newCount == null || !(Number.isInteger(newCount) && newCount >= 0)) throw new Error('invalid newCount: ' + newCount); + const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems; if (oldCount !== newCount) { changed = true; @@ -121,7 +103,9 @@ export class ApQuestionService { } } - await this.pollsRepository.update({ noteId: note.id }, { votes: poll.votes }); + await this.pollsRepository.update({ noteId: note.id }, { + votes: poll.votes, + }); return changed; } diff --git a/packages/backend/src/core/activitypub/models/icon.ts b/packages/backend/src/core/activitypub/models/icon.ts index 5722507a3b..50794a937d 100644 --- a/packages/backend/src/core/activitypub/models/icon.ts +++ b/packages/backend/src/core/activitypub/models/icon.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export type IIcon = { type: string; mediaType?: string; diff --git a/packages/backend/src/core/activitypub/models/identifier.ts b/packages/backend/src/core/activitypub/models/identifier.ts index dce4f410b4..f6c3bb8c88 100644 --- a/packages/backend/src/core/activitypub/models/identifier.ts +++ b/packages/backend/src/core/activitypub/models/identifier.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export type IIdentifier = { type: string; name: string; diff --git a/packages/backend/src/core/activitypub/models/tag.ts b/packages/backend/src/core/activitypub/models/tag.ts index f75cc45f7e..803846a0b0 100644 --- a/packages/backend/src/core/activitypub/models/tag.ts +++ b/packages/backend/src/core/activitypub/models/tag.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { toArray } from '@/misc/prelude/array.js'; import { isHashtag } from '../type.js'; import type { IObject, IApHashtag } from '../type.js'; -export function extractApHashtags(tags: IObject | IObject[] | null | undefined): string[] { +export function extractApHashtags(tags: IObject | IObject[] | null | undefined) { if (tags == null) return []; const hashtags = extractApHashtagObjects(tags); @@ -15,7 +10,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined): return hashtags.map(tag => { const m = tag.name.match(/^#(.+)/); return m ? m[1] : null; - }).filter(x => x != null); + }).filter((x): x is string => x != null); } export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] { diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 72732b01df..625135da6c 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export type Obj = { [x: string]: any }; export type ApObject = IObject | string | (IObject | string)[]; @@ -12,11 +7,6 @@ export interface IObject { id?: string; name?: string | null; summary?: string; - _misskey_summary?: string; - _misskey_followedMessage?: string | null; - _misskey_requireSigninToViewContents?: boolean; - _misskey_makeNotesFollowersOnlyBefore?: number | null; - _misskey_makeNotesHiddenBefore?: number | null; published?: string; cc?: ApObject; to?: ApObject; @@ -29,7 +19,6 @@ export interface IObject { endTime?: Date; icon?: any; image?: any; - mediaType?: string; url?: ApObject | string; href?: string; tag?: IObject | IObject[]; @@ -64,14 +53,11 @@ export function getApId(value: string | IObject): string { /** * Get ActivityStreams Object type - * - * タイプ判定ができなかった場合に、あえてエラーではなくnullを返すようにしている。 - * 詳細: https://github.com/misskey-dev/misskey/issues/14239 */ -export function getApType(value: IObject): string | null { +export function getApType(value: IObject): string { if (typeof value.type === 'string') return value.type; if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0]; - return null; + throw new Error('cannot detect type'); } export function getOneApHrefNullable(value: ApObject | undefined): string | undefined { @@ -104,23 +90,19 @@ export interface IActivity extends IObject { export interface ICollection extends IObject { type: 'Collection'; totalItems: number; - first?: IObject | string; - items?: ApObject; + items: ApObject; } export interface IOrderedCollection extends IObject { type: 'OrderedCollection'; totalItems: number; - first?: IObject | string; - orderedItems?: ApObject; + orderedItems: ApObject; } export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event']; -export const isPost = (object: IObject): object is IPost => { - const type = getApType(object); - return type != null && validPost.includes(type); -}; +export const isPost = (object: IObject): object is IPost => + validPost.includes(getApType(object)); export interface IPost extends IObject { type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event'; @@ -167,10 +149,8 @@ export const isTombstone = (object: IObject): object is ITombstone => export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application']; -export const isActor = (object: IObject): object is IActor => { - const type = getApType(object); - return type != null && validActor.includes(type); -}; +export const isActor = (object: IObject): object is IActor => + validActor.includes(getApType(object)); export interface IActor extends IObject { type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application'; @@ -214,6 +194,7 @@ export interface IApPropertyValue extends IObject { } export const isPropertyValue = (object: IObject): object is IApPropertyValue => + object && getApType(object) === 'PropertyValue' && typeof object.name === 'string' && 'value' in object && @@ -242,11 +223,6 @@ export interface IApEmoji extends IObject { type: 'Emoji'; name: string; updated: string; - // Misskey拡張。後方互換性のためにoptional。 - // 将来の拡張性を考慮してobjectにしている - _misskey_license?: { - freeText: string | null; - }; } export const isEmoji = (object: IObject): object is IApEmoji => @@ -258,19 +234,15 @@ export interface IKey extends IObject { publicKeyPem: string | Buffer; } -export const validDocumentTypes = ['Audio', 'Document', 'Image', 'Page', 'Video']; - export interface IApDocument extends IObject { - type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video'; + type: 'Document'; + name: string | null; + mediaType: string; } -export const isDocument = (object: IObject): object is IApDocument => { - const type = getApType(object); - return type != null && validDocumentTypes.includes(type); -}; - -export interface IApImage extends IApDocument { +export interface IApImage extends IObject { type: 'Image'; + name: string | null; } export interface ICreate extends IActivity { @@ -345,12 +317,8 @@ export const isAccept = (object: IObject): object is IAccept => getApType(object export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject'; export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add'; export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove'; -export const isLike = (object: IObject): object is ILike => { - const type = getApType(object); - return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type); -}; +export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact'; export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce'; export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move'; -export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note'; diff --git a/packages/backend/src/core/chart/ChartLoggerService.ts b/packages/backend/src/core/chart/ChartLoggerService.ts index 20815ea968..afd3bab5a2 100644 --- a/packages/backend/src/core/chart/ChartLoggerService.ts +++ b/packages/backend/src/core/chart/ChartLoggerService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; @@ -14,6 +9,6 @@ export class ChartLoggerService { constructor( private loggerService: LoggerService, ) { - this.logger = this.loggerService.getLogger('chart', 'white'); + this.logger = this.loggerService.getLogger('chart', 'white', process.env.NODE_ENV !== 'test'); } } diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts index f04c561063..b0e9e534df 100644 --- a/packages/backend/src/core/chart/ChartManagementService.ts +++ b/packages/backend/src/core/chart/ChartManagementService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; @@ -23,7 +18,7 @@ import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class ChartManagementService implements OnApplicationShutdown { private charts; - private saveIntervalId: NodeJS.Timeout; + private saveIntervalId: NodeJS.Timer; constructor( private federationChart: FederationChart, @@ -58,9 +53,9 @@ export class ChartManagementService implements OnApplicationShutdown { @bindThis public async start() { // 20分おきにメモリ情報をDBに書き込み - this.saveIntervalId = setInterval(async () => { + this.saveIntervalId = setInterval(() => { for (const chart of this.charts) { - await chart.save(); + chart.save(); } }, 1000 * 60 * 20); } @@ -69,9 +64,9 @@ export class ChartManagementService implements OnApplicationShutdown { public async dispose(): Promise { clearInterval(this.saveIntervalId); if (process.env.NODE_ENV !== 'test') { - for (const chart of this.charts) { - await chart.save(); - } + await Promise.all( + this.charts.map(chart => chart.save()), + ); } } diff --git a/packages/backend/src/core/chart/charts/active-users.ts b/packages/backend/src/core/chart/charts/active-users.ts index 05905f3782..bc0ba25cbb 100644 --- a/packages/backend/src/core/chart/charts/active-users.ts +++ b/packages/backend/src/core/chart/charts/active-users.ts @@ -1,15 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; -import type { MiUser } from '@/models/User.js'; +import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/active-users.js'; @@ -22,15 +16,15 @@ const year = 1000 * 60 * 60 * 24 * 365; /** * アクティブユーザーに関するチャート */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class ActiveUsersChart extends Chart { // eslint-disable-line import/no-default-export +export default class ActiveUsersChart extends Chart { constructor( @Inject(DI.db) private db: DataSource, private appLockService: AppLockService, private chartLoggerService: ChartLoggerService, - private idService: IdService, ) { super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema); } @@ -44,21 +38,20 @@ export default class ActiveUsersChart extends Chart { // eslint-d } @bindThis - public async read(user: { id: MiUser['id'], host: null }): Promise { - const createdAt = this.idService.parse(user.id).date; + public async read(user: { id: User['id'], host: null, createdAt: User['createdAt'] }): Promise { await this.commit({ 'read': [user.id], - 'registeredWithinWeek': (Date.now() - createdAt.getTime() < week) ? [user.id] : [], - 'registeredWithinMonth': (Date.now() - createdAt.getTime() < month) ? [user.id] : [], - 'registeredWithinYear': (Date.now() - createdAt.getTime() < year) ? [user.id] : [], - 'registeredOutsideWeek': (Date.now() - createdAt.getTime() > week) ? [user.id] : [], - 'registeredOutsideMonth': (Date.now() - createdAt.getTime() > month) ? [user.id] : [], - 'registeredOutsideYear': (Date.now() - createdAt.getTime() > year) ? [user.id] : [], + 'registeredWithinWeek': (Date.now() - user.createdAt.getTime() < week) ? [user.id] : [], + 'registeredWithinMonth': (Date.now() - user.createdAt.getTime() < month) ? [user.id] : [], + 'registeredWithinYear': (Date.now() - user.createdAt.getTime() < year) ? [user.id] : [], + 'registeredOutsideWeek': (Date.now() - user.createdAt.getTime() > week) ? [user.id] : [], + 'registeredOutsideMonth': (Date.now() - user.createdAt.getTime() > month) ? [user.id] : [], + 'registeredOutsideYear': (Date.now() - user.createdAt.getTime() > year) ? [user.id] : [], }); } @bindThis - public async write(user: { id: MiUser['id'], host: null }): Promise { + public async write(user: { id: User['id'], host: null, createdAt: User['createdAt'] }): Promise { await this.commit({ 'write': [user.id], }); diff --git a/packages/backend/src/core/chart/charts/ap-request.ts b/packages/backend/src/core/chart/charts/ap-request.ts index 04e771a95b..ce377460c8 100644 --- a/packages/backend/src/core/chart/charts/ap-request.ts +++ b/packages/backend/src/core/chart/charts/ap-request.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; @@ -16,8 +11,9 @@ import type { KVs } from '../core.js'; /** * Chart about ActivityPub requests */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class ApRequestChart extends Chart { // eslint-disable-line import/no-default-export +export default class ApRequestChart extends Chart { constructor( @Inject(DI.db) private db: DataSource, diff --git a/packages/backend/src/core/chart/charts/drive.ts b/packages/backend/src/core/chart/charts/drive.ts index 613e074a9f..b63db591fb 100644 --- a/packages/backend/src/core/chart/charts/drive.ts +++ b/packages/backend/src/core/chart/charts/drive.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @@ -17,8 +12,9 @@ import type { KVs } from '../core.js'; /** * ドライブに関するチャート */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class DriveChart extends Chart { // eslint-disable-line import/no-default-export +export default class DriveChart extends Chart { constructor( @Inject(DI.db) private db: DataSource, @@ -38,7 +34,7 @@ export default class DriveChart extends Chart { // eslint-disable } @bindThis - public async update(file: MiDriveFile, isAdditional: boolean): Promise { + public async update(file: DriveFile, isAdditional: boolean): Promise { const fileSizeKb = file.size / 1000; await this.commit(file.userHost === null ? { 'local.incCount': isAdditional ? 1 : 0, diff --git a/packages/backend/src/core/chart/charts/entities/active-users.ts b/packages/backend/src/core/chart/charts/entities/active-users.ts index fc2b88a2bb..e291e37c1b 100644 --- a/packages/backend/src/core/chart/charts/entities/active-users.ts +++ b/packages/backend/src/core/chart/charts/entities/active-users.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'activeUsers'; diff --git a/packages/backend/src/core/chart/charts/entities/ap-request.ts b/packages/backend/src/core/chart/charts/entities/ap-request.ts index 93e47e081b..3a9f3dacfd 100644 --- a/packages/backend/src/core/chart/charts/entities/ap-request.ts +++ b/packages/backend/src/core/chart/charts/entities/ap-request.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'apRequest'; diff --git a/packages/backend/src/core/chart/charts/entities/drive.ts b/packages/backend/src/core/chart/charts/entities/drive.ts index 4ea16da38c..4bf5bb729e 100644 --- a/packages/backend/src/core/chart/charts/entities/drive.ts +++ b/packages/backend/src/core/chart/charts/entities/drive.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'drive'; diff --git a/packages/backend/src/core/chart/charts/entities/federation.ts b/packages/backend/src/core/chart/charts/entities/federation.ts index 5ed7804343..a8466b0b4c 100644 --- a/packages/backend/src/core/chart/charts/entities/federation.ts +++ b/packages/backend/src/core/chart/charts/entities/federation.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'federation'; diff --git a/packages/backend/src/core/chart/charts/entities/instance.ts b/packages/backend/src/core/chart/charts/entities/instance.ts index d0cac3e73f..06962120e2 100644 --- a/packages/backend/src/core/chart/charts/entities/instance.ts +++ b/packages/backend/src/core/chart/charts/entities/instance.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'instance'; diff --git a/packages/backend/src/core/chart/charts/entities/notes.ts b/packages/backend/src/core/chart/charts/entities/notes.ts index 325236ab35..9387dbfb2c 100644 --- a/packages/backend/src/core/chart/charts/entities/notes.ts +++ b/packages/backend/src/core/chart/charts/entities/notes.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'notes'; diff --git a/packages/backend/src/core/chart/charts/entities/per-user-drive.ts b/packages/backend/src/core/chart/charts/entities/per-user-drive.ts index 25d4619dde..6111640ea0 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-drive.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-drive.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'perUserDrive'; diff --git a/packages/backend/src/core/chart/charts/entities/per-user-following.ts b/packages/backend/src/core/chart/charts/entities/per-user-following.ts index 1618bd22f3..4118daa474 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-following.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-following.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'perUserFollowing'; diff --git a/packages/backend/src/core/chart/charts/entities/per-user-notes.ts b/packages/backend/src/core/chart/charts/entities/per-user-notes.ts index 30404b2e48..c1fa174452 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-notes.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-notes.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'perUserNotes'; diff --git a/packages/backend/src/core/chart/charts/entities/per-user-pv.ts b/packages/backend/src/core/chart/charts/entities/per-user-pv.ts index 7a903afad4..64c8ed1fb1 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-pv.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-pv.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'perUserPv'; diff --git a/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts b/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts index bb62bb2386..5e1a6c7b30 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'perUserReaction'; diff --git a/packages/backend/src/core/chart/charts/entities/test-grouped.ts b/packages/backend/src/core/chart/charts/entities/test-grouped.ts index 599c1dc136..66b6e8e864 100644 --- a/packages/backend/src/core/chart/charts/entities/test-grouped.ts +++ b/packages/backend/src/core/chart/charts/entities/test-grouped.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'testGrouped'; diff --git a/packages/backend/src/core/chart/charts/entities/test-intersection.ts b/packages/backend/src/core/chart/charts/entities/test-intersection.ts index d29b39716c..a3bdcb367f 100644 --- a/packages/backend/src/core/chart/charts/entities/test-intersection.ts +++ b/packages/backend/src/core/chart/charts/entities/test-intersection.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'testIntersection'; diff --git a/packages/backend/src/core/chart/charts/entities/test-unique.ts b/packages/backend/src/core/chart/charts/entities/test-unique.ts index bdaa1716ed..b2cfb71b05 100644 --- a/packages/backend/src/core/chart/charts/entities/test-unique.ts +++ b/packages/backend/src/core/chart/charts/entities/test-unique.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'testUnique'; diff --git a/packages/backend/src/core/chart/charts/entities/test.ts b/packages/backend/src/core/chart/charts/entities/test.ts index c80ff55c99..7cba21e16a 100644 --- a/packages/backend/src/core/chart/charts/entities/test.ts +++ b/packages/backend/src/core/chart/charts/entities/test.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'test'; diff --git a/packages/backend/src/core/chart/charts/entities/users.ts b/packages/backend/src/core/chart/charts/entities/users.ts index f94a5029d7..c0b83094ae 100644 --- a/packages/backend/src/core/chart/charts/entities/users.ts +++ b/packages/backend/src/core/chart/charts/entities/users.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Chart from '../../core.js'; export const name = 'users'; diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts index c9b43cc66d..ae4eb6e48d 100644 --- a/packages/backend/src/core/chart/charts/federation.ts +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -1,13 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { FollowingsRepository, InstancesRepository, MiMeta } from '@/models/_.js'; +import type { FollowingsRepository, InstancesRepository } from '@/models/index.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; +import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; @@ -17,21 +13,20 @@ import type { KVs } from '../core.js'; /** * フェデレーションに関するチャート */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class FederationChart extends Chart { // eslint-disable-line import/no-default-export +export default class FederationChart extends Chart { constructor( @Inject(DI.db) private db: DataSource, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, + private metaService: MetaService, private appLockService: AppLockService, private chartLoggerService: ChartLoggerService, ) { @@ -44,9 +39,11 @@ export default class FederationChart extends Chart { // eslint-di } protected async tickMinor(): Promise>> { + const meta = await this.metaService.fetch(); + const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance') .select('instance.host') - .where('instance.suspensionState != \'none\''); + .where('instance.isSuspended = true'); const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f') .select('f.followerHost') @@ -64,21 +61,21 @@ export default class FederationChart extends Chart { // eslint-di this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followerHost)') .where('following.followerHost IS NOT NULL') - .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`) .setParameters(pubsubSubQuery.getParameters()) @@ -87,16 +84,16 @@ export default class FederationChart extends Chart { // eslint-di this.instancesRepository.createQueryBuilder('instance') .select('COUNT(instance.id)') .where(`instance.host IN (${ subInstancesQuery.getQuery() })`) - .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) - .andWhere('instance.suspensionState = \'none\'') + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere('instance.isSuspended = false') .andWhere('instance.isNotResponding = false') .getRawOne() .then(x => parseInt(x.count, 10)), this.instancesRepository.createQueryBuilder('instance') .select('COUNT(instance.id)') .where(`instance.host IN (${ pubInstancesQuery.getQuery() })`) - .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) - .andWhere('instance.suspensionState = \'none\'') + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere('instance.isSuspended = false') .andWhere('instance.isNotResponding = false') .getRawOne() .then(x => parseInt(x.count, 10)), diff --git a/packages/backend/src/core/chart/charts/instance.ts b/packages/backend/src/core/chart/charts/instance.ts index 97f3bc6f2b..8ca88d80e3 100644 --- a/packages/backend/src/core/chart/charts/instance.ts +++ b/packages/backend/src/core/chart/charts/instance.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { DriveFilesRepository, FollowingsRepository, UsersRepository, NotesRepository } from '@/models/_.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiNote } from '@/models/Note.js'; +import type { DriveFilesRepository, FollowingsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { Note } from '@/models/entities/Note.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -20,8 +15,9 @@ import type { KVs } from '../core.js'; /** * インスタンスごとのチャート */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class InstanceChart extends Chart { // eslint-disable-line import/no-default-export +export default class InstanceChart extends Chart { constructor( @Inject(DI.db) private db: DataSource, @@ -97,7 +93,7 @@ export default class InstanceChart extends Chart { // eslint-disa } @bindThis - public async updateNote(host: string, note: MiNote, isAdditional: boolean): Promise { + public async updateNote(host: string, note: Note, isAdditional: boolean): Promise { await this.commit({ 'notes.total': isAdditional ? 1 : -1, 'notes.inc': isAdditional ? 1 : 0, @@ -128,7 +124,7 @@ export default class InstanceChart extends Chart { // eslint-disa } @bindThis - public async updateDrive(file: MiDriveFile, isAdditional: boolean): Promise { + public async updateDrive(file: DriveFile, isAdditional: boolean): Promise { const fileSizeKb = file.size / 1000; await this.commit({ 'drive.totalFiles': isAdditional ? 1 : -1, diff --git a/packages/backend/src/core/chart/charts/notes.ts b/packages/backend/src/core/chart/charts/notes.ts index f763b5fffa..23dc248fec 100644 --- a/packages/backend/src/core/chart/charts/notes.ts +++ b/packages/backend/src/core/chart/charts/notes.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { Not, IsNull, DataSource } from 'typeorm'; -import type { NotesRepository } from '@/models/_.js'; -import type { MiNote } from '@/models/Note.js'; +import type { NotesRepository } from '@/models/index.js'; +import type { Note } from '@/models/entities/Note.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @@ -18,8 +13,9 @@ import type { KVs } from '../core.js'; /** * ノートに関するチャート */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class NotesChart extends Chart { // eslint-disable-line import/no-default-export +export default class NotesChart extends Chart { constructor( @Inject(DI.db) private db: DataSource, @@ -50,7 +46,7 @@ export default class NotesChart extends Chart { // eslint-disable } @bindThis - public async update(note: MiNote, isAdditional: boolean): Promise { + public async update(note: Note, isAdditional: boolean): Promise { const prefix = note.userHost === null ? 'local' : 'remote'; await this.commit({ diff --git a/packages/backend/src/core/chart/charts/per-user-drive.ts b/packages/backend/src/core/chart/charts/per-user-drive.ts index 404964d8b7..ffba04b041 100644 --- a/packages/backend/src/core/chart/charts/per-user-drive.ts +++ b/packages/backend/src/core/chart/charts/per-user-drive.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { DriveFilesRepository } from '@/models/_.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; @@ -19,8 +14,9 @@ import type { KVs } from '../core.js'; /** * ユーザーごとのドライブに関するチャート */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class PerUserDriveChart extends Chart { // eslint-disable-line import/no-default-export +export default class PerUserDriveChart extends Chart { constructor( @Inject(DI.db) private db: DataSource, @@ -52,7 +48,7 @@ export default class PerUserDriveChart extends Chart { // eslint- } @bindThis - public async update(file: MiDriveFile, isAdditional: boolean): Promise { + public async update(file: DriveFile, isAdditional: boolean): Promise { const fileSizeKb = file.size / 1000; await this.commit({ 'totalCount': isAdditional ? 1 : -1, diff --git a/packages/backend/src/core/chart/charts/per-user-following.ts b/packages/backend/src/core/chart/charts/per-user-following.ts index 588ac638de..aea6d44a9a 100644 --- a/packages/backend/src/core/chart/charts/per-user-following.ts +++ b/packages/backend/src/core/chart/charts/per-user-following.ts @@ -1,15 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { Not, IsNull, DataSource } from 'typeorm'; -import type { MiUser } from '@/models/User.js'; +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 type { FollowingsRepository } from '@/models/_.js'; +import type { FollowingsRepository } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; @@ -19,8 +14,9 @@ import type { KVs } from '../core.js'; /** * ユーザーごとのフォローに関するチャート */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class PerUserFollowingChart extends Chart { // eslint-disable-line import/no-default-export +export default class PerUserFollowingChart extends Chart { constructor( @Inject(DI.db) private db: DataSource, @@ -61,7 +57,7 @@ export default class PerUserFollowingChart extends Chart { // esl } @bindThis - public async update(follower: { id: MiUser['id']; host: MiUser['host']; }, followee: { id: MiUser['id']; host: MiUser['host']; }, isFollow: boolean): Promise { + public async update(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, isFollow: boolean): Promise { const prefixFollower = this.userEntityService.isLocalUser(follower) ? 'local' : 'remote'; const prefixFollowee = this.userEntityService.isLocalUser(followee) ? 'local' : 'remote'; diff --git a/packages/backend/src/core/chart/charts/per-user-notes.ts b/packages/backend/src/core/chart/charts/per-user-notes.ts index e4900772bb..d8966f34c1 100644 --- a/packages/backend/src/core/chart/charts/per-user-notes.ts +++ b/packages/backend/src/core/chart/charts/per-user-notes.ts @@ -1,15 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { MiUser } from '@/models/User.js'; -import type { MiNote } from '@/models/Note.js'; +import type { User } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; @@ -19,8 +14,9 @@ import type { KVs } from '../core.js'; /** * ユーザーごとのノートに関するチャート */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class PerUserNotesChart extends Chart { // eslint-disable-line import/no-default-export +export default class PerUserNotesChart extends Chart { constructor( @Inject(DI.db) private db: DataSource, @@ -49,7 +45,7 @@ export default class PerUserNotesChart extends Chart { // eslint- } @bindThis - public update(user: { id: MiUser['id'] }, note: MiNote, isAdditional: boolean): void { + public update(user: { id: User['id'] }, note: Note, isAdditional: boolean): void { this.commit({ 'total': isAdditional ? 1 : -1, 'inc': isAdditional ? 1 : 0, diff --git a/packages/backend/src/core/chart/charts/per-user-pv.ts b/packages/backend/src/core/chart/charts/per-user-pv.ts index 31708fefa8..53c89d8a9a 100644 --- a/packages/backend/src/core/chart/charts/per-user-pv.ts +++ b/packages/backend/src/core/chart/charts/per-user-pv.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { MiUser } from '@/models/User.js'; +import type { User } from '@/models/entities/User.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @@ -17,8 +12,9 @@ import type { KVs } from '../core.js'; /** * ユーザーごとのプロフィール被閲覧数に関するチャート */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class PerUserPvChart extends Chart { // eslint-disable-line import/no-default-export +export default class PerUserPvChart extends Chart { constructor( @Inject(DI.db) private db: DataSource, @@ -38,7 +34,7 @@ export default class PerUserPvChart extends Chart { // eslint-dis } @bindThis - public async commitByUser(user: { id: MiUser['id'] }, key: string): Promise { + public async commitByUser(user: { id: User['id'] }, key: string): Promise { await this.commit({ 'upv.user': [key], 'pv.user': 1, @@ -46,7 +42,7 @@ export default class PerUserPvChart extends Chart { // eslint-dis } @bindThis - public async commitByVisitor(user: { id: MiUser['id'] }, key: string): Promise { + public async commitByVisitor(user: { id: User['id'] }, key: string): Promise { await this.commit({ 'upv.visitor': [key], 'pv.visitor': 1, diff --git a/packages/backend/src/core/chart/charts/per-user-reactions.ts b/packages/backend/src/core/chart/charts/per-user-reactions.ts index c29c4d2870..7bc6d4b521 100644 --- a/packages/backend/src/core/chart/charts/per-user-reactions.ts +++ b/packages/backend/src/core/chart/charts/per-user-reactions.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { MiUser } from '@/models/User.js'; -import type { MiNote } from '@/models/Note.js'; +import type { User } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -19,8 +14,9 @@ import type { KVs } from '../core.js'; /** * ユーザーごとのリアクションに関するチャート */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class PerUserReactionsChart extends Chart { // eslint-disable-line import/no-default-export +export default class PerUserReactionsChart extends Chart { constructor( @Inject(DI.db) private db: DataSource, @@ -41,7 +37,7 @@ export default class PerUserReactionsChart extends Chart { // esl } @bindThis - public async update(user: { id: MiUser['id'], host: MiUser['host'] }, note: MiNote): Promise { + public async update(user: { id: User['id'], host: User['host'] }, note: Note): Promise { const prefix = this.userEntityService.isLocalUser(user) ? 'local' : 'remote'; this.commit({ [`${prefix}.count`]: 1, diff --git a/packages/backend/src/core/chart/charts/test-grouped.ts b/packages/backend/src/core/chart/charts/test-grouped.ts index 7a2844f4ed..128967bc65 100644 --- a/packages/backend/src/core/chart/charts/test-grouped.ts +++ b/packages/backend/src/core/chart/charts/test-grouped.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; @@ -16,8 +11,9 @@ import type { KVs } from '../core.js'; /** * For testing */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class TestGroupedChart extends Chart { // eslint-disable-line import/no-default-export +export default class TestGroupedChart extends Chart { private total = {} as Record; constructor( diff --git a/packages/backend/src/core/chart/charts/test-intersection.ts b/packages/backend/src/core/chart/charts/test-intersection.ts index b8d0556c9f..6b4eed9062 100644 --- a/packages/backend/src/core/chart/charts/test-intersection.ts +++ b/packages/backend/src/core/chart/charts/test-intersection.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; @@ -16,8 +11,9 @@ import type { KVs } from '../core.js'; /** * For testing */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class TestIntersectionChart extends Chart { // eslint-disable-line import/no-default-export +export default class TestIntersectionChart extends Chart { constructor( @Inject(DI.db) private db: DataSource, diff --git a/packages/backend/src/core/chart/charts/test-unique.ts b/packages/backend/src/core/chart/charts/test-unique.ts index f94e008059..5d2b3f8ab1 100644 --- a/packages/backend/src/core/chart/charts/test-unique.ts +++ b/packages/backend/src/core/chart/charts/test-unique.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; @@ -16,8 +11,9 @@ import type { KVs } from '../core.js'; /** * For testing */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class TestUniqueChart extends Chart { // eslint-disable-line import/no-default-export +export default class TestUniqueChart extends Chart { constructor( @Inject(DI.db) private db: DataSource, diff --git a/packages/backend/src/core/chart/charts/test.ts b/packages/backend/src/core/chart/charts/test.ts index a90dc8f99b..238351d8b3 100644 --- a/packages/backend/src/core/chart/charts/test.ts +++ b/packages/backend/src/core/chart/charts/test.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; @@ -16,8 +11,9 @@ import type { KVs } from '../core.js'; /** * For testing */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class TestChart extends Chart { // eslint-disable-line import/no-default-export +export default class TestChart extends Chart { public total = 0; // publicにするのはテストのため constructor( diff --git a/packages/backend/src/core/chart/charts/users.ts b/packages/backend/src/core/chart/charts/users.ts index d148fc629b..7bc3602439 100644 --- a/packages/backend/src/core/chart/charts/users.ts +++ b/packages/backend/src/core/chart/charts/users.ts @@ -1,15 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import { Not, IsNull, DataSource } from 'typeorm'; -import type { MiUser } from '@/models/User.js'; +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 type { UsersRepository } from '@/models/_.js'; +import type { UsersRepository } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; @@ -19,8 +14,9 @@ import type { KVs } from '../core.js'; /** * ユーザー数に関するチャート */ +// eslint-disable-next-line import/no-default-export @Injectable() -export default class UsersChart extends Chart { // eslint-disable-line import/no-default-export +export default class UsersChart extends Chart { constructor( @Inject(DI.db) private db: DataSource, @@ -52,7 +48,7 @@ export default class UsersChart extends Chart { // eslint-disable } @bindThis - public async update(user: { id: MiUser['id'], host: MiUser['host'] }, isAdditional: boolean): Promise { + public async update(user: { id: User['id'], host: User['host'] }, isAdditional: boolean): Promise { const prefix = this.userEntityService.isLocalUser(user) ? 'local' : 'remote'; await this.commit({ diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts index af5485a46e..d352adcc1f 100644 --- a/packages/backend/src/core/chart/core.ts +++ b/packages/backend/src/core/chart/core.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - /** * チャートエンジン * @@ -14,8 +9,7 @@ import { EntitySchema, LessThan, Between } from 'typeorm'; import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc/prelude/time.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import { MiRepository, miRepository } from '@/models/_.js'; -import type { DataSource, Repository } from 'typeorm'; +import type { Repository, DataSource } from 'typeorm'; const COLUMN_PREFIX = '___' as const; const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const; @@ -95,29 +89,6 @@ type ToJsonSchema = { }; export function getJsonSchema(schema: S): ToJsonSchema>> { - const unflatten = (str: string, parent: Record) => { - const keys = str.split('.'); - const key = keys.shift(); - const nextKey = keys[0]; - - if (key == null) return; - - if (parent.properties[key] == null) { - parent.properties[key] = nextKey ? { - type: 'object', - properties: {}, - required: [], - } : { - type: 'array', - items: { - type: 'number', - }, - }; - } - - if (nextKey) unflatten(keys.join('.'), parent.properties[key] as Record); - }; - const jsonSchema = { type: 'object', properties: {} as Record, @@ -125,7 +96,10 @@ export function getJsonSchema(schema: S): ToJsonSchema>>; @@ -146,10 +120,10 @@ export default abstract class Chart { group: string | null; }[] = []; // ↓にしたいけどfindOneとかで型エラーになる - //private repositoryForHour: Repository> & MiRepository>; - //private repositoryForDay: Repository> & MiRepository>; - private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>; - private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>; + //private repositoryForHour: Repository>; + //private repositoryForDay: Repository>; + private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>; + private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }>; /** * 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用) @@ -212,10 +186,6 @@ export default abstract class Chart { } { const createEntity = (span: 'hour' | 'day'): EntitySchema => new EntitySchema({ name: - span === 'hour' ? `ChartX${name}` : - span === 'day' ? `ChartDayX${name}` : - new Error('not happen') as never, - tableName: span === 'hour' ? `__chart__${camelToSnake(name)}` : span === 'day' ? `__chart_day__${camelToSnake(name)}` : new Error('not happen') as never, @@ -276,15 +246,15 @@ export default abstract class Chart { this.logger = logger; const { hour, day } = Chart.schemaToEntity(name, schema, grouped); - this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>); - this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>); + this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour); + this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day); } @bindThis private convertRawRecord(x: RawRecord): KVs { const kvs = {} as Record; for (const k of Object.keys(x).filter((k) => k.startsWith(COLUMN_PREFIX)) as (keyof Columns)[]) { - kvs[(k as string).substring(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number; + kvs[(k as string).substr(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number; } return kvs as KVs; } @@ -392,11 +362,11 @@ export default abstract class Chart { } // 新規ログ挿入 - log = await repository.insertOne({ + log = await repository.insert({ date: date, ...(group ? { group: group } : {}), ...columns, - }) as RawRecord; + }).then(x => repository.findOneByOrFail(x.identifiers[0])) as RawRecord; this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`); @@ -464,15 +434,13 @@ export default abstract class Chart { } } - // bake cardinality + // bake unique count for (const [k, v] of Object.entries(finalDiffs)) { if (this.schema[k].uniqueIncrement) { const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof Columns; const tempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique; - const cardinalityOfHour = new Set([...(v as string[]), ...(logHour[tempColumnName] as unknown as string[])]).size; - const cardinalityOfDay = new Set([...(v as string[]), ...(logDay[tempColumnName] as unknown as string[])]).size; - queryForHour[name] = cardinalityOfHour; - queryForDay[name] = cardinalityOfDay; + 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; } } @@ -644,7 +612,7 @@ export default abstract class Chart { // 要求された範囲にログがひとつもなかったら if (logs.length === 0) { // もっとも新しいログを持ってくる - // (すくなくともひとつログが無いと補間できないため) + // (すくなくともひとつログが無いと隙間埋めできないため) const recentLog = await repository.findOne({ where: group ? { group: group, @@ -659,9 +627,9 @@ export default abstract class Chart { } // 要求された範囲の最も古い箇所に位置するログが存在しなかったら - } else if (!isTimeSame(new Date(logs.at(-1)!.date * 1000), gt)) { + } else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) { // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する - // (補間できないため) + // (隙間埋めできないため) const outdatedLog = await repository.findOne({ where: { date: LessThan(Chart.dateToTimestamp(gt)), @@ -690,7 +658,7 @@ export default abstract class Chart { if (log) { chart.unshift(this.convertRawRecord(log)); } else { - // 補間 + // 隙間埋め const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current)); const data = latest ? this.convertRawRecord(latest) : null; chart.unshift(this.getNewLog(data)); diff --git a/packages/backend/src/core/chart/entities.ts b/packages/backend/src/core/chart/entities.ts index e424f2c8c5..b44e2e38b7 100644 --- a/packages/backend/src/core/chart/entities.ts +++ b/packages/backend/src/core/chart/entities.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { entity as FederationChart } from './charts/entities/federation.js'; import { entity as NotesChart } from './charts/entities/notes.js'; import { entity as UsersChart } from './charts/entities/users.js'; diff --git a/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts b/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts deleted file mode 100644 index 1e23c194c5..0000000000 --- a/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import type { AbuseReportNotificationRecipientRepository, MiAbuseReportNotificationRecipient } from '@/models/_.js'; -import { bindThis } from '@/decorators.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { Packed } from '@/misc/json-schema.js'; -import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; - -@Injectable() -export class AbuseReportNotificationRecipientEntityService { - constructor( - @Inject(DI.abuseReportNotificationRecipientRepository) - private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository, - private userEntityService: UserEntityService, - private systemWebhookEntityService: SystemWebhookEntityService, - ) { - } - - @bindThis - public async pack( - src: MiAbuseReportNotificationRecipient['id'] | MiAbuseReportNotificationRecipient, - opts?: { - users: Map>, - webhooks: Map>, - }, - ): Promise> { - const recipient = typeof src === 'object' - ? src - : await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: src }); - const user = recipient.userId - ? (opts?.users.get(recipient.userId) ?? await this.userEntityService.pack<'UserLite'>(recipient.userId)) - : undefined; - const webhook = recipient.systemWebhookId - ? (opts?.webhooks.get(recipient.systemWebhookId) ?? await this.systemWebhookEntityService.pack(recipient.systemWebhookId)) - : undefined; - - return { - id: recipient.id, - isActive: recipient.isActive, - updatedAt: recipient.updatedAt.toISOString(), - name: recipient.name, - method: recipient.method, - userId: recipient.userId ?? undefined, - user: user, - systemWebhookId: recipient.systemWebhookId ?? undefined, - systemWebhook: webhook, - }; - } - - @bindThis - public async packMany( - src: MiAbuseReportNotificationRecipient['id'][] | MiAbuseReportNotificationRecipient[], - ): Promise[]> { - const objs = src.filter((it): it is MiAbuseReportNotificationRecipient => typeof it === 'object'); - const ids = src.filter((it): it is MiAbuseReportNotificationRecipient['id'] => typeof it === 'string'); - if (ids.length > 0) { - objs.push( - ...await this.abuseReportNotificationRecipientRepository.findBy({ id: In(ids) }), - ); - } - - const userIds = objs.map(it => it.userId).filter(x => x != null); - const users: Map> = (userIds.length > 0) - ? await this.userEntityService.packMany(userIds) - .then(it => new Map(it.map(it => [it.id, it]))) - : new Map(); - - const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(x => x != null); - const systemWebhooks: Map> = (systemWebhookIds.length > 0) - ? await this.systemWebhookEntityService.packMany(systemWebhookIds) - .then(it => new Map(it.map(it => [it.id, it]))) - : new Map(); - - return Promise - .all( - objs.map(it => this.pack(it, { users: users, webhooks: systemWebhooks })), - ) - .then(it => it.sort((a, b) => a.id.localeCompare(b.id))); - } -} - diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts index 70ead890ab..7f8240b8b2 100644 --- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts +++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts @@ -1,17 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { AbuseUserReportsRepository } from '@/models/_.js'; +import type { AbuseUserReportsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; -import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; -import type { Packed } from '@/misc/json-schema.js'; +import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class AbuseUserReportEntityService { @@ -20,63 +13,40 @@ export class AbuseUserReportEntityService { private abuseUserReportsRepository: AbuseUserReportsRepository, private userEntityService: UserEntityService, - private idService: IdService, ) { } @bindThis public async pack( - src: MiAbuseUserReport['id'] | MiAbuseUserReport, - hint?: { - packedReporter?: Packed<'UserDetailedNotMe'>, - packedTargetUser?: Packed<'UserDetailedNotMe'>, - packedAssignee?: Packed<'UserDetailedNotMe'>, - }, + src: AbuseUserReport['id'] | AbuseUserReport, ) { const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src }); return await awaitAll({ id: report.id, - createdAt: this.idService.parse(report.id).date.toISOString(), + createdAt: report.createdAt.toISOString(), comment: report.comment, resolved: report.resolved, reporterId: report.reporterId, targetUserId: report.targetUserId, assigneeId: report.assigneeId, - reporter: hint?.packedReporter ?? this.userEntityService.pack(report.reporter ?? report.reporterId, null, { - schema: 'UserDetailedNotMe', + reporter: this.userEntityService.pack(report.reporter ?? report.reporterId, null, { + detail: true, }), - targetUser: hint?.packedTargetUser ?? this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, { - schema: 'UserDetailedNotMe', + targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, { + detail: true, }), - assignee: report.assigneeId ? hint?.packedAssignee ?? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, { - schema: 'UserDetailedNotMe', + assignee: report.assigneeId ? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, { + detail: true, }) : null, forwarded: report.forwarded, - resolvedAs: report.resolvedAs, - moderationNote: report.moderationNote, }); } @bindThis - public async packMany( - reports: MiAbuseUserReport[], + public packMany( + reports: any[], ) { - const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId); - const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId); - const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(x => x != null); - const _userMap = await this.userEntityService.packMany( - [..._reporters, ..._targetUsers, ..._assignees], - null, - { schema: 'UserDetailedNotMe' }, - ).then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all( - reports.map(report => { - const packedReporter = _userMap.get(report.reporterId); - const packedTargetUser = _userMap.get(report.targetUserId); - const packedAssignee = report.assigneeId != null ? _userMap.get(report.assigneeId) : undefined; - return this.pack(report, { packedReporter, packedTargetUser, packedAssignee }); - }), - ); + return Promise.all(reports.map(x => this.pack(x))); } } diff --git a/packages/backend/src/core/entities/AnnouncementEntityService.ts b/packages/backend/src/core/entities/AnnouncementEntityService.ts deleted file mode 100644 index 90b04d0229..0000000000 --- a/packages/backend/src/core/entities/AnnouncementEntityService.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { AnnouncementsRepository, AnnouncementReadsRepository, MiAnnouncement, MiUser } from '@/models/_.js'; -import type { Packed } from '@/misc/json-schema.js'; -import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; - -@Injectable() -export class AnnouncementEntityService { - constructor( - @Inject(DI.announcementsRepository) - private announcementsRepository: AnnouncementsRepository, - - @Inject(DI.announcementReadsRepository) - private announcementReadsRepository: AnnouncementReadsRepository, - - private idService: IdService, - ) { - } - - @bindThis - public async pack( - src: MiAnnouncement['id'] | MiAnnouncement & { isRead?: boolean | null }, - me?: { id: MiUser['id'] } | null | undefined, - ): Promise> { - const announcement = typeof src === 'object' - ? src - : await this.announcementsRepository.findOneByOrFail({ - id: src, - }) as MiAnnouncement & { isRead?: boolean | null }; - - if (me && announcement.isRead === undefined) { - announcement.isRead = await this.announcementReadsRepository - .countBy({ - announcementId: announcement.id, - userId: me.id, - }) - .then((count: number) => count > 0); - } - - return { - id: announcement.id, - createdAt: this.idService.parse(announcement.id).date.toISOString(), - updatedAt: announcement.updatedAt?.toISOString() ?? null, - title: announcement.title, - text: announcement.text, - imageUrl: announcement.imageUrl, - icon: announcement.icon, - display: announcement.display, - forYou: announcement.userId === me?.id, - needConfirmationToRead: announcement.needConfirmationToRead, - silence: announcement.silence, - isRead: announcement.isRead !== null ? announcement.isRead : undefined, - }; - } - - @bindThis - public async packMany( - announcements: (MiAnnouncement['id'] | MiAnnouncement & { isRead?: boolean | null } | MiAnnouncement)[], - me?: { id: MiUser['id'] } | null | undefined, - ) : Promise[]> { - return (await Promise.allSettled(announcements.map(x => this.pack(x, me)))) - .filter(result => result.status === 'fulfilled') - .map(result => (result as PromiseFulfilledResult>).value); - } -} diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts index 1f8c8ae3e8..328511f5df 100644 --- a/packages/backend/src/core/entities/AntennaEntityService.ts +++ b/packages/backend/src/core/entities/AntennaEntityService.ts @@ -1,35 +1,27 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { AntennasRepository } from '@/models/_.js'; +import type { AntennasRepository } from '@/models/index.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { MiAntenna } from '@/models/Antenna.js'; +import type { Antenna } from '@/models/entities/Antenna.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; @Injectable() export class AntennaEntityService { constructor( @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, - - private idService: IdService, ) { } @bindThis public async pack( - src: MiAntenna['id'] | MiAntenna, + src: Antenna['id'] | Antenna, ): Promise> { const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src }); return { id: antenna.id, - createdAt: this.idService.parse(antenna.id).date.toISOString(), + createdAt: antenna.createdAt.toISOString(), name: antenna.name, keywords: antenna.keywords, excludeKeywords: antenna.excludeKeywords, @@ -37,14 +29,11 @@ export class AntennaEntityService { userListId: antenna.userListId, users: antenna.users, caseSensitive: antenna.caseSensitive, - localOnly: antenna.localOnly, - excludeBots: antenna.excludeBots, + notify: antenna.notify, withReplies: antenna.withReplies, withFile: antenna.withFile, - excludeNotesInSensitiveChannel: antenna.excludeNotesInSensitiveChannel, isActive: antenna.isActive, hasUnreadNote: false, // TODO - notify: false, // 後方互換性のため }; } } diff --git a/packages/backend/src/core/entities/AppEntityService.ts b/packages/backend/src/core/entities/AppEntityService.ts index 785b84689a..0b4c3935c7 100644 --- a/packages/backend/src/core/entities/AppEntityService.ts +++ b/packages/backend/src/core/entities/AppEntityService.ts @@ -1,14 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { AccessTokensRepository, AppsRepository } from '@/models/_.js'; +import type { AccessTokensRepository, AppsRepository } from '@/models/index.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { MiApp } from '@/models/App.js'; -import type { MiUser } from '@/models/User.js'; +import type { App } from '@/models/entities/App.js'; +import type { User } from '@/models/entities/User.js'; import { bindThis } from '@/decorators.js'; @Injectable() @@ -24,8 +19,8 @@ export class AppEntityService { @bindThis public async pack( - src: MiApp['id'] | MiApp, - me?: { id: MiUser['id'] } | null | undefined, + src: App['id'] | App, + me?: { id: User['id'] } | null | undefined, options?: { detail?: boolean, includeSecret?: boolean, diff --git a/packages/backend/src/core/entities/AuthSessionEntityService.ts b/packages/backend/src/core/entities/AuthSessionEntityService.ts index 72873680c9..b7edc8494e 100644 --- a/packages/backend/src/core/entities/AuthSessionEntityService.ts +++ b/packages/backend/src/core/entities/AuthSessionEntityService.ts @@ -1,16 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { AuthSessionsRepository } from '@/models/_.js'; +import type { AuthSessionsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { MiAuthSession } from '@/models/AuthSession.js'; -import type { MiUser } from '@/models/User.js'; -import { bindThis } from '@/decorators.js'; +import type { AuthSession } from '@/models/entities/AuthSession.js'; +import type { User } from '@/models/entities/User.js'; import { AppEntityService } from './AppEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class AuthSessionEntityService { @@ -24,8 +19,8 @@ export class AuthSessionEntityService { @bindThis public async pack( - src: MiAuthSession['id'] | MiAuthSession, - me?: { id: MiUser['id'] } | null | undefined, + src: AuthSession['id'] | AuthSession, + me?: { id: User['id'] } | null | undefined, ) { const session = typeof src === 'object' ? src : await this.authSessionsRepository.findOneByOrFail({ id: src }); diff --git a/packages/backend/src/core/entities/BlockingEntityService.ts b/packages/backend/src/core/entities/BlockingEntityService.ts index 1e699032e2..e169c7e90a 100644 --- a/packages/backend/src/core/entities/BlockingEntityService.ts +++ b/packages/backend/src/core/entities/BlockingEntityService.ts @@ -1,17 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { BlockingsRepository } from '@/models/_.js'; +import type { BlockingsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { MiBlocking } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; +import type { Blocking } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; import { UserEntityService } from './UserEntityService.js'; @Injectable() @@ -21,38 +15,31 @@ export class BlockingEntityService { private blockingsRepository: BlockingsRepository, private userEntityService: UserEntityService, - private idService: IdService, ) { } @bindThis public async pack( - src: MiBlocking['id'] | MiBlocking, - me?: { id: MiUser['id'] } | null | undefined, - hint?: { - blockee?: Packed<'UserDetailedNotMe'>, - }, + src: Blocking['id'] | Blocking, + me?: { id: User['id'] } | null | undefined, ): Promise> { const blocking = typeof src === 'object' ? src : await this.blockingsRepository.findOneByOrFail({ id: src }); return await awaitAll({ id: blocking.id, - createdAt: this.idService.parse(blocking.id).date.toISOString(), + createdAt: blocking.createdAt.toISOString(), blockeeId: blocking.blockeeId, - blockee: hint?.blockee ?? this.userEntityService.pack(blocking.blockeeId, me, { - schema: 'UserDetailedNotMe', + blockee: this.userEntityService.pack(blocking.blockeeId, me, { + detail: true, }), }); } @bindThis - public async packMany( - blockings: MiBlocking[], - me: { id: MiUser['id'] }, + public packMany( + blockings: any[], + me: { id: User['id'] }, ) { - const _blockees = blockings.map(({ blockee, blockeeId }) => blockee ?? blockeeId); - const _userMap = await this.userEntityService.packMany(_blockees, me, { schema: 'UserDetailedNotMe' }) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(blockings.map(blocking => this.pack(blocking, me, { blockee: _userMap.get(blocking.blockeeId) }))); + return Promise.all(blockings.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts index 1ba7ca8e57..15ffd44861 100644 --- a/packages/backend/src/core/entities/ChannelEntityService.ts +++ b/packages/backend/src/core/entities/ChannelEntityService.ts @@ -1,17 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NotesRepository } from '@/models/_.js'; +import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository, NotesRepository } from '@/models/index.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiChannel } from '@/models/Channel.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Channel } from '@/models/entities/Channel.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js'; import { NoteEntityService } from './NoteEntityService.js'; import { In } from 'typeorm'; @@ -31,19 +25,21 @@ export class ChannelEntityService { @Inject(DI.notesRepository) private notesRepository: NotesRepository, + @Inject(DI.noteUnreadsRepository) + private noteUnreadsRepository: NoteUnreadsRepository, + @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, private noteEntityService: NoteEntityService, private driveFileEntityService: DriveFileEntityService, - private idService: IdService, ) { } @bindThis public async pack( - src: MiChannel['id'] | MiChannel, - me?: { id: MiUser['id'] } | null | undefined, + src: Channel['id'] | Channel, + me?: { id: User['id'] } | null | undefined, detailed?: boolean, ): Promise> { const channel = typeof src === 'object' ? src : await this.channelsRepository.findOneByOrFail({ id: src }); @@ -51,19 +47,17 @@ export class ChannelEntityService { const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null; - const isFollowing = meId ? await this.channelFollowingsRepository.exists({ - where: { - followerId: meId, - followeeId: channel.id, - }, - }) : false; + const hasUnreadNote = meId ? (await this.noteUnreadsRepository.findOneBy({ noteChannelId: channel.id, userId: meId })) != null : undefined; - const isFavorited = meId ? await this.channelFavoritesRepository.exists({ - where: { - userId: meId, - channelId: channel.id, - }, - }) : false; + const following = meId ? await this.channelFollowingsRepository.findOneBy({ + followerId: meId, + followeeId: channel.id, + }) : null; + + const favorite = meId ? await this.channelFavoritesRepository.findOneBy({ + userId: meId, + channelId: channel.id, + }) : null; const pinnedNotes = channel.pinnedNoteIds.length > 0 ? await this.notesRepository.find({ where: { @@ -73,7 +67,7 @@ export class ChannelEntityService { return { id: channel.id, - createdAt: this.idService.parse(channel.id).date.toISOString(), + createdAt: channel.createdAt.toISOString(), lastNotedAt: channel.lastNotedAt ? channel.lastNotedAt.toISOString() : null, name: channel.name, description: channel.description, @@ -84,13 +78,11 @@ export class ChannelEntityService { isArchived: channel.isArchived, usersCount: channel.usersCount, notesCount: channel.notesCount, - isSensitive: channel.isSensitive, - allowRenoteToExternal: channel.allowRenoteToExternal, ...(me ? { - isFollowing, - isFavorited, - hasUnreadNote: false, // 後方互換性のため + isFollowing: following != null, + isFavorited: favorite != null, + hasUnreadNote, } : {}), ...(detailed ? { diff --git a/packages/backend/src/core/entities/ChatEntityService.ts b/packages/backend/src/core/entities/ChatEntityService.ts deleted file mode 100644 index 6bce2413fd..0000000000 --- a/packages/backend/src/core/entities/ChatEntityService.ts +++ /dev/null @@ -1,385 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { MiUser, ChatMessagesRepository, MiChatMessage, ChatRoomsRepository, MiChatRoom, MiChatRoomInvitation, ChatRoomInvitationsRepository, MiChatRoomMembership, ChatRoomMembershipsRepository } from '@/models/_.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; -import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; -import { UserEntityService } from './UserEntityService.js'; -import { DriveFileEntityService } from './DriveFileEntityService.js'; -import { In } from 'typeorm'; - -@Injectable() -export class ChatEntityService { - constructor( - @Inject(DI.chatMessagesRepository) - private chatMessagesRepository: ChatMessagesRepository, - - @Inject(DI.chatRoomsRepository) - private chatRoomsRepository: ChatRoomsRepository, - - @Inject(DI.chatRoomInvitationsRepository) - private chatRoomInvitationsRepository: ChatRoomInvitationsRepository, - - @Inject(DI.chatRoomMembershipsRepository) - private chatRoomMembershipsRepository: ChatRoomMembershipsRepository, - - private userEntityService: UserEntityService, - private driveFileEntityService: DriveFileEntityService, - private idService: IdService, - ) { - } - - @bindThis - public async packMessageDetailed( - src: MiChatMessage['id'] | MiChatMessage, - me?: { id: MiUser['id'] }, - options?: { - _hint_?: { - packedFiles?: Map | null>; - packedUsers?: Map>; - packedRooms?: Map | null>; - }; - }, - ): Promise> { - const packedUsers = options?._hint_?.packedUsers; - const packedFiles = options?._hint_?.packedFiles; - const packedRooms = options?._hint_?.packedRooms; - - const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src }); - - const reactions: { user: Packed<'UserLite'>; reaction: string; }[] = []; - - for (const record of message.reactions) { - const [userId, reaction] = record.split('/'); - reactions.push({ - user: packedUsers?.get(userId) ?? await this.userEntityService.pack(userId), - reaction, - }); - } - - return { - id: message.id, - createdAt: this.idService.parse(message.id).date.toISOString(), - text: message.text, - fromUserId: message.fromUserId, - fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId, me), - toUserId: message.toUserId, - toUser: message.toUserId ? (packedUsers?.get(message.toUserId) ?? await this.userEntityService.pack(message.toUser ?? message.toUserId, me)) : undefined, - toRoomId: message.toRoomId, - toRoom: message.toRoomId ? (packedRooms?.get(message.toRoomId) ?? await this.packRoom(message.toRoom ?? message.toRoomId, me)) : undefined, - fileId: message.fileId, - file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null, - reactions, - }; - } - - @bindThis - public async packMessagesDetailed( - messages: MiChatMessage[], - me: { id: MiUser['id'] }, - ) { - if (messages.length === 0) return []; - - const excludeMe = (x: MiUser | string) => { - if (typeof x === 'string') { - return x !== me.id; - } else { - return x.id !== me.id; - } - }; - - const users = [ - ...messages.map((m) => m.fromUser ?? m.fromUserId).filter(excludeMe), - ...messages.map((m) => m.toUser ?? m.toUserId).filter(x => x != null).filter(excludeMe), - ]; - - const reactedUserIds = messages.flatMap(x => x.reactions.map(r => r.split('/')[0])); - - for (const reactedUserId of reactedUserIds) { - if (!users.some(x => typeof x === 'string' ? x === reactedUserId : x.id === reactedUserId)) { - users.push(reactedUserId); - } - } - - const [packedUsers, packedFiles, packedRooms] = await Promise.all([ - this.userEntityService.packMany(users, me) - .then(users => new Map(users.map(u => [u.id, u]))), - this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)) - .then(files => new Map(files.map(f => [f.id, f]))), - this.packRooms(messages.map(m => m.toRoom ?? m.toRoomId).filter(x => x != null), me) - .then(rooms => new Map(rooms.map(r => [r.id, r]))), - ]); - - return Promise.all(messages.map(message => this.packMessageDetailed(message, me, { _hint_: { packedUsers, packedFiles, packedRooms } }))); - } - - @bindThis - public async packMessageLiteFor1on1( - src: MiChatMessage['id'] | MiChatMessage, - options?: { - _hint_?: { - packedFiles: Map | null>; - }; - }, - ): Promise> { - const packedFiles = options?._hint_?.packedFiles; - - const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src }); - - const reactions: { reaction: string; }[] = []; - - for (const record of message.reactions) { - const [userId, reaction] = record.split('/'); - reactions.push({ - reaction, - }); - } - - return { - id: message.id, - createdAt: this.idService.parse(message.id).date.toISOString(), - text: message.text, - fromUserId: message.fromUserId, - toUserId: message.toUserId!, - fileId: message.fileId, - file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null, - reactions, - }; - } - - @bindThis - public async packMessagesLiteFor1on1( - messages: MiChatMessage[], - ) { - if (messages.length === 0) return []; - - const [packedFiles] = await Promise.all([ - this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)) - .then(files => new Map(files.map(f => [f.id, f]))), - ]); - - return Promise.all(messages.map(message => this.packMessageLiteFor1on1(message, { _hint_: { packedFiles } }))); - } - - @bindThis - public async packMessageLiteForRoom( - src: MiChatMessage['id'] | MiChatMessage, - options?: { - _hint_?: { - packedFiles: Map | null>; - packedUsers: Map>; - }; - }, - ): Promise> { - const packedFiles = options?._hint_?.packedFiles; - const packedUsers = options?._hint_?.packedUsers; - - const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src }); - - const reactions: { user: Packed<'UserLite'>; reaction: string; }[] = []; - - for (const record of message.reactions) { - const [userId, reaction] = record.split('/'); - reactions.push({ - user: packedUsers?.get(userId) ?? await this.userEntityService.pack(userId), - reaction, - }); - } - - return { - id: message.id, - createdAt: this.idService.parse(message.id).date.toISOString(), - text: message.text, - fromUserId: message.fromUserId, - fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId), - toRoomId: message.toRoomId!, - fileId: message.fileId, - file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null, - reactions, - }; - } - - @bindThis - public async packMessagesLiteForRoom( - messages: MiChatMessage[], - ) { - if (messages.length === 0) return []; - - const users = messages.map(x => x.fromUser ?? x.fromUserId); - const reactedUserIds = messages.flatMap(x => x.reactions.map(r => r.split('/')[0])); - - for (const reactedUserId of reactedUserIds) { - if (!users.some(x => typeof x === 'string' ? x === reactedUserId : x.id === reactedUserId)) { - users.push(reactedUserId); - } - } - - const [packedUsers, packedFiles] = await Promise.all([ - this.userEntityService.packMany(users) - .then(users => new Map(users.map(u => [u.id, u]))), - this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)) - .then(files => new Map(files.map(f => [f.id, f]))), - ]); - - return Promise.all(messages.map(message => this.packMessageLiteForRoom(message, { _hint_: { packedFiles, packedUsers } }))); - } - - @bindThis - public async packRoom( - src: MiChatRoom['id'] | MiChatRoom, - me?: { id: MiUser['id'] }, - options?: { - _hint_?: { - packedOwners: Map>; - myMemberships?: Map; - myInvitations?: Map; - }; - }, - ): Promise> { - const room = typeof src === 'object' ? src : await this.chatRoomsRepository.findOneByOrFail({ id: src }); - - const membership = me && me.id !== room.ownerId ? (options?._hint_?.myMemberships?.get(room.id) ?? await this.chatRoomMembershipsRepository.findOneBy({ roomId: room.id, userId: me.id })) : null; - const invitation = me && me.id !== room.ownerId ? (options?._hint_?.myInvitations?.get(room.id) ?? await this.chatRoomInvitationsRepository.findOneBy({ roomId: room.id, userId: me.id })) : null; - - return { - id: room.id, - createdAt: this.idService.parse(room.id).date.toISOString(), - name: room.name, - description: room.description, - ownerId: room.ownerId, - owner: options?._hint_?.packedOwners.get(room.ownerId) ?? await this.userEntityService.pack(room.owner ?? room.ownerId, me), - isMuted: membership != null ? membership.isMuted : false, - invitationExists: invitation != null, - }; - } - - @bindThis - public async packRooms( - rooms: (MiChatRoom | MiChatRoom['id'])[], - me: { id: MiUser['id'] }, - ) { - if (rooms.length === 0) return []; - - const _rooms = rooms.filter((room): room is MiChatRoom => typeof room !== 'string'); - if (_rooms.length !== rooms.length) { - _rooms.push( - ...await this.chatRoomsRepository.find({ - where: { - id: In(rooms.filter((room): room is string => typeof room === 'string')), - }, - relations: ['owner'], - }), - ); - } - - const owners = _rooms.map(x => x.owner ?? x.ownerId); - - const [packedOwners, myMemberships, myInvitations] = await Promise.all([ - this.userEntityService.packMany(owners, me) - .then(users => new Map(users.map(u => [u.id, u]))), - this.chatRoomMembershipsRepository.find({ - where: { - roomId: In(_rooms.map(x => x.id)), - userId: me.id, - }, - }).then(memberships => new Map(_rooms.map(r => [r.id, memberships.find(m => m.roomId === r.id)]))), - this.chatRoomInvitationsRepository.find({ - where: { - roomId: In(_rooms.map(x => x.id)), - userId: me.id, - }, - }).then(invitations => new Map(_rooms.map(r => [r.id, invitations.find(i => i.roomId === r.id)]))), - ]); - - return Promise.all(_rooms.map(room => this.packRoom(room, me, { _hint_: { packedOwners, myMemberships, myInvitations } }))); - } - - @bindThis - public async packRoomInvitation( - src: MiChatRoomInvitation['id'] | MiChatRoomInvitation, - me: { id: MiUser['id'] }, - options?: { - _hint_?: { - packedRooms: Map>; - packedUsers: Map>; - }; - }, - ): Promise> { - const invitation = typeof src === 'object' ? src : await this.chatRoomInvitationsRepository.findOneByOrFail({ id: src }); - - return { - id: invitation.id, - createdAt: this.idService.parse(invitation.id).date.toISOString(), - roomId: invitation.roomId, - room: options?._hint_?.packedRooms.get(invitation.roomId) ?? await this.packRoom(invitation.room ?? invitation.roomId, me), - userId: invitation.userId, - user: options?._hint_?.packedUsers.get(invitation.userId) ?? await this.userEntityService.pack(invitation.user ?? invitation.userId, me), - }; - } - - @bindThis - public async packRoomInvitations( - invitations: MiChatRoomInvitation[], - me: { id: MiUser['id'] }, - ) { - if (invitations.length === 0) return []; - - return Promise.all(invitations.map(invitation => this.packRoomInvitation(invitation, me))); - } - - @bindThis - public async packRoomMembership( - src: MiChatRoomMembership['id'] | MiChatRoomMembership, - me: { id: MiUser['id'] }, - options?: { - populateUser?: boolean; - populateRoom?: boolean; - _hint_?: { - packedRooms: Map>; - packedUsers: Map>; - }; - }, - ): Promise> { - const membership = typeof src === 'object' ? src : await this.chatRoomMembershipsRepository.findOneByOrFail({ id: src }); - - return { - id: membership.id, - createdAt: this.idService.parse(membership.id).date.toISOString(), - userId: membership.userId, - user: options?.populateUser ? (options._hint_?.packedUsers.get(membership.userId) ?? await this.userEntityService.pack(membership.user ?? membership.userId, me)) : undefined, - roomId: membership.roomId, - room: options?.populateRoom ? (options._hint_?.packedRooms.get(membership.roomId) ?? await this.packRoom(membership.room ?? membership.roomId, me)) : undefined, - }; - } - - @bindThis - public async packRoomMemberships( - memberships: MiChatRoomMembership[], - me: { id: MiUser['id'] }, - options: { - populateUser?: boolean; - populateRoom?: boolean; - } = {}, - ) { - if (memberships.length === 0) return []; - - const users = memberships.map(x => x.user ?? x.userId); - const rooms = memberships.map(x => x.room ?? x.roomId); - - const [packedUsers, packedRooms] = await Promise.all([ - this.userEntityService.packMany(users, me) - .then(users => new Map(users.map(u => [u.id, u]))), - this.packRooms(rooms, me) - .then(rooms => new Map(rooms.map(r => [r.id, r]))), - ]); - - return Promise.all(memberships.map(membership => this.packRoomMembership(membership, me, { ...options, _hint_: { packedUsers, packedRooms } }))); - } -} diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts index d915645906..33d3c53806 100644 --- a/packages/backend/src/core/entities/ClipEntityService.ts +++ b/packages/backend/src/core/entities/ClipEntityService.ts @@ -1,17 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { ClipNotesRepository, ClipFavoritesRepository, ClipsRepository, MiUser } from '@/models/_.js'; +import type { ClipFavoritesRepository, ClipsRepository, User } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; -import type { MiClip } from '@/models/Clip.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { Clip } from '@/models/entities/Clip.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; import { UserEntityService } from './UserEntityService.js'; @Injectable() @@ -20,52 +14,41 @@ export class ClipEntityService { @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, - @Inject(DI.clipNotesRepository) - private clipNotesRepository: ClipNotesRepository, - @Inject(DI.clipFavoritesRepository) private clipFavoritesRepository: ClipFavoritesRepository, private userEntityService: UserEntityService, - private idService: IdService, ) { } @bindThis public async pack( - src: MiClip['id'] | MiClip, - me?: { id: MiUser['id'] } | null | undefined, - hint?: { - packedUser?: Packed<'UserLite'> - }, + src: Clip['id'] | Clip, + me?: { id: User['id'] } | null | undefined, ): Promise> { const meId = me ? me.id : null; const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src }); return await awaitAll({ id: clip.id, - createdAt: this.idService.parse(clip.id).date.toISOString(), + createdAt: clip.createdAt.toISOString(), lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null, userId: clip.userId, - user: hint?.packedUser ?? this.userEntityService.pack(clip.user ?? clip.userId), + user: this.userEntityService.pack(clip.user ?? clip.userId), name: clip.name, description: clip.description, isPublic: clip.isPublic, favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }), - isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined, - notesCount: (meId === clip.userId) ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined, + isFavorited: meId ? await this.clipFavoritesRepository.findOneBy({ clipId: clip.id, userId: meId }).then(x => x != null) : undefined, }); } @bindThis - public async packMany( - clips: MiClip[], - me?: { id: MiUser['id'] } | null | undefined, + public packMany( + clips: Clip[], + me?: { id: User['id'] } | null | undefined, ) { - const _users = clips.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, me) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(clips.map(clip => this.pack(clip, me, { packedUser: _userMap.get(clip.userId) }))); + return Promise.all(clips.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index a6f7f369a6..d82f36d971 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -1,22 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; +import { DataSource, In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, MiMeta } from '@/models/_.js'; +import type { NotesRepository, DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type { Packed } from '@/misc/json-schema.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { User } from '@/models/entities/User.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; import { appendQuery, query } from '@/misc/prelude/url.js'; import { deepClone } from '@/misc/clone.js'; -import { bindThis } from '@/decorators.js'; -import { isMimeImage } from '@/misc/is-mime-image.js'; -import { IdService } from '@/core/IdService.js'; import { UtilityService } from '../UtilityService.js'; import { VideoProcessingService } from '../VideoProcessingService.js'; import { UserEntityService } from './UserEntityService.js'; @@ -27,6 +19,9 @@ type PackOptions = { self?: boolean, withUser?: boolean, }; +import { bindThis } from '@/decorators.js'; +import { isMimeImage } from '@/misc/is-mime-image.js'; +import { isNotNull } from '@/misc/is-not-null.js'; @Injectable() export class DriveFileEntityService { @@ -34,8 +29,11 @@ export class DriveFileEntityService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -47,10 +45,9 @@ export class DriveFileEntityService { private utilityService: UtilityService, private driveFolderEntityService: DriveFolderEntityService, private videoProcessingService: VideoProcessingService, - private idService: IdService, ) { } - + @bindThis public validateFileName(name: string): boolean { return ( @@ -63,7 +60,7 @@ export class DriveFileEntityService { } @bindThis - public getPublicProperties(file: MiDriveFile): MiDriveFile['properties'] { + public getPublicProperties(file: DriveFile): DriveFile['properties'] { if (file.properties.orientation != null) { const properties = deepClone(file.properties); if (file.properties.orientation >= 5) { @@ -88,17 +85,17 @@ export class DriveFileEntityService { } @bindThis - public getThumbnailUrl(file: MiDriveFile): string | null { + public getThumbnailUrl(file: DriveFile): string | null { if (file.type.startsWith('video')) { if (file.thumbnailUrl) return file.thumbnailUrl; - return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url); + return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url ?? file.uri); } else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { // 動画ではなくリモートかつメディアプロキシ return this.getProxiedUrl(file.uri, 'static'); } - if (file.uri != null && file.isLink && this.meta.proxyRemoteFiles) { + if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { // リモートかつ期限切れはローカルプロキシを試みる // 従来は/files/${thumbnailAccessKey}にアクセスしていたが、 // /filesはメディアプロキシにリダイレクトするようにしたため直接メディアプロキシを指定する @@ -111,14 +108,14 @@ export class DriveFileEntityService { } @bindThis - public getPublicUrl(file: MiDriveFile, mode?: 'avatar'): string { // static = thumbnail + public getPublicUrl(file: DriveFile, mode?: 'avatar'): string { // static = thumbnail // リモートかつメディアプロキシ if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { return this.getProxiedUrl(file.uri, mode); } // リモートかつ期限切れはローカルプロキシを試みる - if (file.uri != null && file.isLink && this.meta.proxyRemoteFiles) { + if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { const key = file.webpublicAccessKey; if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外 @@ -137,7 +134,7 @@ export class DriveFileEntityService { } @bindThis - public async calcDriveUsageOf(user: MiUser['id'] | { id: MiUser['id'] }): Promise { + public async calcDriveUsageOf(user: User['id'] | { id: User['id'] }): Promise { const id = typeof user === 'object' ? user.id : user; const { sum } = await this.driveFilesRepository @@ -147,7 +144,7 @@ export class DriveFileEntityService { .select('SUM(file.size)', 'sum') .getRawOne(); - return parseInt(sum, 10) || 0; + return parseInt(sum, 10) ?? 0; } @bindThis @@ -159,7 +156,7 @@ export class DriveFileEntityService { .select('SUM(file.size)', 'sum') .getRawOne(); - return parseInt(sum, 10) || 0; + return parseInt(sum, 10) ?? 0; } @bindThis @@ -171,7 +168,7 @@ export class DriveFileEntityService { .select('SUM(file.size)', 'sum') .getRawOne(); - return parseInt(sum, 10) || 0; + return parseInt(sum, 10) ?? 0; } @bindThis @@ -183,12 +180,12 @@ export class DriveFileEntityService { .select('SUM(file.size)', 'sum') .getRawOne(); - return parseInt(sum, 10) || 0; + return parseInt(sum, 10) ?? 0; } @bindThis public async pack( - src: MiDriveFile['id'] | MiDriveFile, + src: DriveFile['id'] | DriveFile, options?: PackOptions, ): Promise> { const opts = Object.assign({ @@ -200,7 +197,7 @@ export class DriveFileEntityService { return await awaitAll>({ id: file.id, - createdAt: this.idService.parse(file.id).date.toISOString(), + createdAt: file.createdAt.toISOString(), name: file.name, type: file.type, md5: file.md5, @@ -222,11 +219,8 @@ export class DriveFileEntityService { @bindThis public async packNullable( - src: MiDriveFile['id'] | MiDriveFile, + src: DriveFile['id'] | DriveFile, options?: PackOptions, - hint?: { - packedUser?: Packed<'UserLite'> - }, ): Promise | null> { const opts = Object.assign({ detail: false, @@ -238,7 +232,7 @@ export class DriveFileEntityService { return await awaitAll>({ id: file.id, - createdAt: this.idService.parse(file.id).date.toISOString(), + createdAt: file.createdAt.toISOString(), name: file.name, type: file.type, md5: file.md5, @@ -253,26 +247,23 @@ export class DriveFileEntityService { folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { detail: true, }) : null, - userId: file.userId, - user: (opts.withUser && file.userId) ? hint?.packedUser ?? this.userEntityService.pack(file.userId) : null, + userId: opts.withUser ? file.userId : null, + user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null, }); } @bindThis public async packMany( - files: MiDriveFile[], + files: DriveFile[], options?: PackOptions, ): Promise[]> { - const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null); - const _userMap = await this.userEntityService.packMany(_user) - .then(users => new Map(users.map(user => [user.id, user]))); - const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {}))); - return items.filter(x => x != null); + const items = await Promise.all(files.map(f => this.packNullable(f, options))); + return items.filter((x): x is Packed<'DriveFile'> => x != null); } @bindThis public async packManyByIdsMap( - fileIds: MiDriveFile['id'][], + fileIds: DriveFile['id'][], options?: PackOptions, ): Promise['id'], Packed<'DriveFile'> | null>> { if (fileIds.length === 0) return new Map(); @@ -287,11 +278,11 @@ export class DriveFileEntityService { @bindThis public async packManyByIds( - fileIds: MiDriveFile['id'][], + fileIds: DriveFile['id'][], options?: PackOptions, ): Promise[]> { if (fileIds.length === 0) return []; const filesMap = await this.packManyByIdsMap(fileIds, options); - return fileIds.map(id => filesMap.get(id)).filter(x => x != null); + return fileIds.map(id => filesMap.get(id)).filter(isNotNull); } } diff --git a/packages/backend/src/core/entities/DriveFolderEntityService.ts b/packages/backend/src/core/entities/DriveFolderEntityService.ts index 299f23ad38..13929b145f 100644 --- a/packages/backend/src/core/entities/DriveFolderEntityService.ts +++ b/packages/backend/src/core/entities/DriveFolderEntityService.ts @@ -1,17 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/_.js'; +import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; -import type { MiDriveFolder } from '@/models/DriveFolder.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { DriveFolder } from '@/models/entities/DriveFolder.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; @Injectable() export class DriveFolderEntityService { @@ -21,14 +15,12 @@ export class DriveFolderEntityService { @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - - private idService: IdService, ) { } @bindThis public async pack( - src: MiDriveFolder['id'] | MiDriveFolder, + src: DriveFolder['id'] | DriveFolder, options?: { detail: boolean }, @@ -41,7 +33,7 @@ export class DriveFolderEntityService { return await awaitAll({ id: folder.id, - createdAt: this.idService.parse(folder.id).date.toISOString(), + createdAt: folder.createdAt.toISOString(), name: folder.name, parentId: folder.parentId, diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 490d3f2511..4a18cd1b3b 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -1,14 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository, MiRole, RolesRepository } from '@/models/_.js'; +import type { EmojisRepository } from '@/models/index.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { MiEmoji } from '@/models/Emoji.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { Emoji } from '@/models/entities/Emoji.js'; import { bindThis } from '@/decorators.js'; @Injectable() @@ -16,14 +11,12 @@ export class EmojiEntityService { constructor( @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, - @Inject(DI.rolesRepository) - private rolesRepository: RolesRepository, ) { } @bindThis public async packSimple( - src: MiEmoji['id'] | MiEmoji, + src: Emoji['id'] | Emoji, ): Promise> { const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); @@ -33,7 +26,6 @@ export class EmojiEntityService { category: emoji.category, // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) url: emoji.publicUrl || emoji.originalUrl, - localOnly: emoji.localOnly ? true : undefined, isSensitive: emoji.isSensitive ? true : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, }; @@ -48,7 +40,7 @@ export class EmojiEntityService { @bindThis public async packDetailed( - src: MiEmoji['id'] | MiEmoji, + src: Emoji['id'] | Emoji, ): Promise> { const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); @@ -70,90 +62,8 @@ export class EmojiEntityService { @bindThis public packDetailedMany( emojis: any[], - ): Promise[]> { + ) { return Promise.all(emojis.map(x => this.packDetailed(x))); } - - @bindThis - public async packDetailedAdmin( - src: MiEmoji['id'] | MiEmoji, - hint?: { - roles?: Map - }, - ): Promise> { - const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); - - const roles = Array.of(); - if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0) { - if (hint?.roles) { - const hintRoles = hint.roles; - roles.push( - ...emoji.roleIdsThatCanBeUsedThisEmojiAsReaction - .filter(x => hintRoles.has(x)) - .map(x => hintRoles.get(x)!), - ); - } else { - roles.push( - ...await this.rolesRepository.findBy({ id: In(emoji.roleIdsThatCanBeUsedThisEmojiAsReaction) }), - ); - } - - roles.sort((a, b) => { - if (a.displayOrder !== b.displayOrder) { - return b.displayOrder - a.displayOrder; - } - - return a.id.localeCompare(b.id); - }); - } - - return { - id: emoji.id, - updatedAt: emoji.updatedAt?.toISOString() ?? null, - name: emoji.name, - host: emoji.host, - uri: emoji.uri, - type: emoji.type, - aliases: emoji.aliases, - category: emoji.category, - publicUrl: emoji.publicUrl, - originalUrl: emoji.originalUrl, - license: emoji.license, - localOnly: emoji.localOnly, - isSensitive: emoji.isSensitive, - roleIdsThatCanBeUsedThisEmojiAsReaction: roles.map(it => ({ id: it.id, name: it.name })), - }; - } - - @bindThis - public async packDetailedAdminMany( - emojis: MiEmoji['id'][] | MiEmoji[], - hint?: { - roles?: Map - }, - ): Promise[]> { - // IDのみの要素をピックアップし、DBからレコードを取り出して他の値を補完する - const emojiEntities = emojis.filter(x => typeof x === 'object') as MiEmoji[]; - const emojiIdOnlyList = emojis.filter(x => typeof x === 'string') as string[]; - if (emojiIdOnlyList.length > 0) { - emojiEntities.push(...await this.emojisRepository.findBy({ id: In(emojiIdOnlyList) })); - } - - // 特定ロール専用の絵文字である場合、そのロール情報をあらかじめまとめて取得しておく(pack側で都度取得も出来るが負荷が高いので) - let hintRoles: Map; - if (hint?.roles) { - hintRoles = hint.roles; - } else { - const roles = Array.of(); - const roleIds = [...new Set(emojiEntities.flatMap(x => x.roleIdsThatCanBeUsedThisEmojiAsReaction))]; - if (roleIds.length > 0) { - roles.push(...await this.rolesRepository.findBy({ id: In(roleIds) })); - } - - hintRoles = new Map(roles.map(x => [x.id, x])); - } - - return Promise.all(emojis.map(x => this.packDetailedAdmin(x, { roles: hintRoles }))); - } } diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts index 7b0150f5b6..e52a591884 100644 --- a/packages/backend/src/core/entities/FlashEntityService.ts +++ b/packages/backend/src/core/entities/FlashEntityService.ts @@ -1,16 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { FlashLikesRepository, FlashsRepository } from '@/models/_.js'; +import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiFlash } from '@/models/Flash.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Flash } from '@/models/entities/Flash.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; import { UserEntityService } from './UserEntityService.js'; @Injectable() @@ -18,71 +14,42 @@ export class FlashEntityService { constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, + @Inject(DI.flashLikesRepository) private flashLikesRepository: FlashLikesRepository, + private userEntityService: UserEntityService, - private idService: IdService, ) { } @bindThis public async pack( - src: MiFlash['id'] | MiFlash, - me?: { id: MiUser['id'] } | null | undefined, - hint?: { - packedUser?: Packed<'UserLite'>, - likedFlashIds?: MiFlash['id'][], - }, + src: Flash['id'] | Flash, + me?: { id: User['id'] } | null | undefined, ): Promise> { const meId = me ? me.id : null; const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src }); - // { schema: 'UserDetailed' } すると無限ループするので注意 - const user = hint?.packedUser ?? await this.userEntityService.pack(flash.user ?? flash.userId, me); - - let isLiked = undefined; - if (meId) { - isLiked = hint?.likedFlashIds - ? hint.likedFlashIds.includes(flash.id) - : await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }); - } - - return { + return await awaitAll({ id: flash.id, - createdAt: this.idService.parse(flash.id).date.toISOString(), + createdAt: flash.createdAt.toISOString(), updatedAt: flash.updatedAt.toISOString(), userId: flash.userId, - user: user, + user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { detail: true } すると無限ループするので注意 title: flash.title, summary: flash.summary, script: flash.script, - visibility: flash.visibility, likedCount: flash.likedCount, - isLiked: isLiked, - }; + isLiked: meId ? await this.flashLikesRepository.findOneBy({ flashId: flash.id, userId: meId }).then(x => x != null) : undefined, + }); } @bindThis - public async packMany( - flashes: MiFlash[], - me?: { id: MiUser['id'] } | null | undefined, + public packMany( + flashs: Flash[], + me?: { id: User['id'] } | null | undefined, ) { - const _users = flashes.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, me) - .then(users => new Map(users.map(u => [u.id, u]))); - const _likedFlashIds = me - ? await this.flashLikesRepository.createQueryBuilder('flashLike') - .select('flashLike.flashId') - .where('flashLike.userId = :userId', { userId: me.id }) - .getRawMany<{ flashLike_flashId: string }>() - .then(likes => [...new Set(likes.map(like => like.flashLike_flashId))]) - : []; - return Promise.all( - flashes.map(flash => this.pack(flash, me, { - packedUser: _userMap.get(flash.userId), - likedFlashIds: _likedFlashIds, - })), - ); + return Promise.all(flashs.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/FlashLikeEntityService.ts b/packages/backend/src/core/entities/FlashLikeEntityService.ts index 6e0b9d6e11..0351ec3014 100644 --- a/packages/backend/src/core/entities/FlashLikeEntityService.ts +++ b/packages/backend/src/core/entities/FlashLikeEntityService.ts @@ -1,14 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { FlashLikesRepository } from '@/models/_.js'; -import type { } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiFlashLike } from '@/models/FlashLike.js'; +import type { FlashLikesRepository } from '@/models/index.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { FlashLike } from '@/models/entities/FlashLike.js'; import { bindThis } from '@/decorators.js'; import { FlashEntityService } from './FlashEntityService.js'; @@ -24,8 +19,8 @@ export class FlashLikeEntityService { @bindThis public async pack( - src: MiFlashLike['id'] | MiFlashLike, - me?: { id: MiUser['id'] } | null | undefined, + src: FlashLike['id'] | FlashLike, + me?: { id: User['id'] } | null | undefined, ) { const like = typeof src === 'object' ? src : await this.flashLikesRepository.findOneByOrFail({ id: src }); @@ -38,7 +33,7 @@ export class FlashLikeEntityService { @bindThis public packMany( likes: any[], - me: { id: MiUser['id'] }, + me: { id: User['id'] }, ) { return Promise.all(likes.map(x => this.pack(x, me))); } diff --git a/packages/backend/src/core/entities/FollowRequestEntityService.ts b/packages/backend/src/core/entities/FollowRequestEntityService.ts index 0101ec8aa7..c2edc6a13a 100644 --- a/packages/backend/src/core/entities/FollowRequestEntityService.ts +++ b/packages/backend/src/core/entities/FollowRequestEntityService.ts @@ -1,17 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { FollowRequestsRepository } from '@/models/_.js'; -import type { } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiFollowRequest } from '@/models/FollowRequest.js'; -import { bindThis } from '@/decorators.js'; -import type { Packed } from '@/misc/json-schema.js'; +import type { FollowRequestsRepository } from '@/models/index.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { FollowRequest } from '@/models/entities/FollowRequest.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class FollowRequestEntityService { @@ -25,38 +19,16 @@ export class FollowRequestEntityService { @bindThis public async pack( - src: MiFollowRequest['id'] | MiFollowRequest, - me?: { id: MiUser['id'] } | null | undefined, - hint?: { - packedFollower?: Packed<'UserLite'>, - packedFollowee?: Packed<'UserLite'>, - }, + src: FollowRequest['id'] | FollowRequest, + me?: { id: User['id'] } | null | undefined, ) { const request = typeof src === 'object' ? src : await this.followRequestsRepository.findOneByOrFail({ id: src }); return { id: request.id, - follower: hint?.packedFollower ?? await this.userEntityService.pack(request.followerId, me), - followee: hint?.packedFollowee ?? await this.userEntityService.pack(request.followeeId, me), + follower: await this.userEntityService.pack(request.followerId, me), + followee: await this.userEntityService.pack(request.followeeId, me), }; } - - @bindThis - public async packMany( - requests: MiFollowRequest[], - me?: { id: MiUser['id'] } | null | undefined, - ) { - const _followers = requests.map(({ follower, followerId }) => follower ?? followerId); - const _followees = requests.map(({ followee, followeeId }) => followee ?? followeeId); - const _userMap = await this.userEntityService.packMany([..._followers, ..._followees], me) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all( - requests.map(req => { - const packedFollower = _userMap.get(req.followerId); - const packedFollowee = _userMap.get(req.followeeId); - return this.pack(req, me, { packedFollower, packedFollowee }); - }), - ); - } } diff --git a/packages/backend/src/core/entities/FollowingEntityService.ts b/packages/backend/src/core/entities/FollowingEntityService.ts index d2dbaf2270..55ba4e67ad 100644 --- a/packages/backend/src/core/entities/FollowingEntityService.ts +++ b/packages/backend/src/core/entities/FollowingEntityService.ts @@ -1,39 +1,33 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository } from '@/models/_.js'; +import type { FollowingsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiFollowing } from '@/models/Following.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Following } from '@/models/entities/Following.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; import { UserEntityService } from './UserEntityService.js'; -type LocalFollowerFollowing = MiFollowing & { +type LocalFollowerFollowing = Following & { followerHost: null; followerInbox: null; followerSharedInbox: null; }; -type RemoteFollowerFollowing = MiFollowing & { +type RemoteFollowerFollowing = Following & { followerHost: string; followerInbox: string; followerSharedInbox: string; }; -type LocalFolloweeFollowing = MiFollowing & { +type LocalFolloweeFollowing = Following & { followeeHost: null; followeeInbox: null; followeeSharedInbox: null; }; -type RemoteFolloweeFollowing = MiFollowing & { +type RemoteFolloweeFollowing = Following & { followeeHost: string; followeeInbox: string; followeeSharedInbox: string; @@ -46,42 +40,37 @@ export class FollowingEntityService { private followingsRepository: FollowingsRepository, private userEntityService: UserEntityService, - private idService: IdService, ) { } @bindThis - public isLocalFollower(following: MiFollowing): following is LocalFollowerFollowing { + public isLocalFollower(following: Following): following is LocalFollowerFollowing { return following.followerHost == null; } @bindThis - public isRemoteFollower(following: MiFollowing): following is RemoteFollowerFollowing { + public isRemoteFollower(following: Following): following is RemoteFollowerFollowing { return following.followerHost != null; } @bindThis - public isLocalFollowee(following: MiFollowing): following is LocalFolloweeFollowing { + public isLocalFollowee(following: Following): following is LocalFolloweeFollowing { return following.followeeHost == null; } @bindThis - public isRemoteFollowee(following: MiFollowing): following is RemoteFolloweeFollowing { + public isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing { return following.followeeHost != null; } @bindThis public async pack( - src: MiFollowing['id'] | MiFollowing, - me?: { id: MiUser['id'] } | null | undefined, + src: Following['id'] | Following, + me?: { id: User['id'] } | null | undefined, opts?: { populateFollowee?: boolean; populateFollower?: boolean; }, - hint?: { - packedFollowee?: Packed<'UserDetailedNotMe'>, - packedFollower?: Packed<'UserDetailedNotMe'>, - }, ): Promise> { const following = typeof src === 'object' ? src : await this.followingsRepository.findOneByOrFail({ id: src }); @@ -89,38 +78,28 @@ export class FollowingEntityService { return await awaitAll({ id: following.id, - createdAt: this.idService.parse(following.id).date.toISOString(), + createdAt: following.createdAt.toISOString(), followeeId: following.followeeId, followerId: following.followerId, - followee: opts.populateFollowee ? hint?.packedFollowee ?? this.userEntityService.pack(following.followee ?? following.followeeId, me, { - schema: 'UserDetailedNotMe', + followee: opts.populateFollowee ? this.userEntityService.pack(following.followee ?? following.followeeId, me, { + detail: true, }) : undefined, - follower: opts.populateFollower ? hint?.packedFollower ?? this.userEntityService.pack(following.follower ?? following.followerId, me, { - schema: 'UserDetailedNotMe', + follower: opts.populateFollower ? this.userEntityService.pack(following.follower ?? following.followerId, me, { + detail: true, }) : undefined, }); } @bindThis - public async packMany( - followings: MiFollowing[], - me?: { id: MiUser['id'] } | null | undefined, + public packMany( + followings: any[], + me?: { id: User['id'] } | null | undefined, opts?: { populateFollowee?: boolean; populateFollower?: boolean; }, ) { - const _followees = opts?.populateFollowee ? followings.map(({ followee, followeeId }) => followee ?? followeeId) : []; - const _followers = opts?.populateFollower ? followings.map(({ follower, followerId }) => follower ?? followerId) : []; - const _userMap = await this.userEntityService.packMany([..._followees, ..._followers], me, { schema: 'UserDetailedNotMe' }) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all( - followings.map(following => { - const packedFollowee = opts?.populateFollowee ? _userMap.get(following.followeeId) : undefined; - const packedFollower = opts?.populateFollower ? _userMap.get(following.followerId) : undefined; - return this.pack(following, me, opts, { packedFollowee, packedFollower }); - }), - ); + return Promise.all(followings.map(x => this.pack(x, me, opts))); } } diff --git a/packages/backend/src/core/entities/GalleryLikeEntityService.ts b/packages/backend/src/core/entities/GalleryLikeEntityService.ts index f199a81b4d..db46045db3 100644 --- a/packages/backend/src/core/entities/GalleryLikeEntityService.ts +++ b/packages/backend/src/core/entities/GalleryLikeEntityService.ts @@ -1,15 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { GalleryLikesRepository } from '@/models/_.js'; -import type { } from '@/models/Blocking.js'; -import type { MiGalleryLike } from '@/models/GalleryLike.js'; -import { bindThis } from '@/decorators.js'; +import type { GalleryLikesRepository } from '@/models/index.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { GalleryLike } from '@/models/entities/GalleryLike.js'; import { GalleryPostEntityService } from './GalleryPostEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class GalleryLikeEntityService { @@ -23,7 +18,7 @@ export class GalleryLikeEntityService { @bindThis public async pack( - src: MiGalleryLike['id'] | MiGalleryLike, + src: GalleryLike['id'] | GalleryLike, me?: any, ) { const like = typeof src === 'object' ? src : await this.galleryLikesRepository.findOneByOrFail({ id: src }); diff --git a/packages/backend/src/core/entities/GalleryPostEntityService.ts b/packages/backend/src/core/entities/GalleryPostEntityService.ts index 9746a4c1af..632c75304f 100644 --- a/packages/backend/src/core/entities/GalleryPostEntityService.ts +++ b/packages/backend/src/core/entities/GalleryPostEntityService.ts @@ -1,18 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/_.js'; +import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiGalleryPost } from '@/models/GalleryPost.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { GalleryPost } from '@/models/entities/GalleryPost.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js'; @@ -27,27 +21,23 @@ export class GalleryPostEntityService { private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, - private idService: IdService, ) { } @bindThis public async pack( - src: MiGalleryPost['id'] | MiGalleryPost, - me?: { id: MiUser['id'] } | null | undefined, - hint?: { - packedUser?: Packed<'UserLite'> - }, + src: GalleryPost['id'] | GalleryPost, + me?: { id: User['id'] } | null | undefined, ): Promise> { const meId = me ? me.id : null; const post = typeof src === 'object' ? src : await this.galleryPostsRepository.findOneByOrFail({ id: src }); return await awaitAll({ id: post.id, - createdAt: this.idService.parse(post.id).date.toISOString(), + createdAt: post.createdAt.toISOString(), updatedAt: post.updatedAt.toISOString(), userId: post.userId, - user: hint?.packedUser ?? this.userEntityService.pack(post.user ?? post.userId, me), + user: this.userEntityService.pack(post.user ?? post.userId, me), title: post.title, description: post.description, fileIds: post.fileIds, @@ -56,19 +46,16 @@ export class GalleryPostEntityService { tags: post.tags.length > 0 ? post.tags : undefined, isSensitive: post.isSensitive, likedCount: post.likedCount, - isLiked: meId ? await this.galleryLikesRepository.exists({ where: { postId: post.id, userId: meId } }) : undefined, + isLiked: meId ? await this.galleryLikesRepository.findOneBy({ postId: post.id, userId: meId }).then(x => x != null) : undefined, }); } @bindThis - public async packMany( - posts: MiGalleryPost[], - me?: { id: MiUser['id'] } | null | undefined, + public packMany( + posts: GalleryPost[], + me?: { id: User['id'] } | null | undefined, ) { - const _users = posts.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, me) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(posts.map(post => this.pack(post, me, { packedUser: _userMap.get(post.userId) }))); + return Promise.all(posts.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/HashtagEntityService.ts b/packages/backend/src/core/entities/HashtagEntityService.ts index d798b15807..2cd79b8f8c 100644 --- a/packages/backend/src/core/entities/HashtagEntityService.ts +++ b/packages/backend/src/core/entities/HashtagEntityService.ts @@ -1,23 +1,25 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { HashtagsRepository } from '@/models/index.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; -import type { MiHashtag } from '@/models/Hashtag.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { Hashtag } from '@/models/entities/Hashtag.js'; import { bindThis } from '@/decorators.js'; +import { UserEntityService } from './UserEntityService.js'; @Injectable() export class HashtagEntityService { constructor( + @Inject(DI.hashtagsRepository) + private hashtagsRepository: HashtagsRepository, + + private userEntityService: UserEntityService, ) { } @bindThis public async pack( - src: MiHashtag, + src: Hashtag, ): Promise> { return { tag: src.name, @@ -32,7 +34,7 @@ export class HashtagEntityService { @bindThis public packMany( - hashtags: MiHashtag[], + hashtags: Hashtag[], ) { return Promise.all(hashtags.map(x => this.pack(x))); } diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 3688cfb363..3bf84ed375 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -1,25 +1,20 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { Packed } from '@/misc/json-schema.js'; -import type { MiInstance } from '@/models/Instance.js'; -import { bindThis } from '@/decorators.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; -import { MiMeta } from '@/models/_.js'; +import type { InstancesRepository } from '@/models/index.js'; +import type { Packed } from '@/misc/json-schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { Instance } from '@/models/entities/Instance.js'; +import { MetaService } from '@/core/MetaService.js'; +import { bindThis } from '@/decorators.js'; +import { UtilityService } from '../UtilityService.js'; @Injectable() export class InstanceEntityService { constructor( - @Inject(DI.meta) - private meta: MiMeta, + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, - private roleService: RoleService, + private metaService: MetaService, private utilityService: UtilityService, ) { @@ -27,12 +22,9 @@ export class InstanceEntityService { @bindThis public async pack( - instance: MiInstance, - me?: { id: MiUser['id']; } | null | undefined, + instance: Instance, ): Promise> { - const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; - const softwareSuspended = this.utilityService.isDeliverSuspendedSoftware(instance); - + const meta = await this.metaService.fetch(); return { id: instance.id, firstRetrievedAt: instance.firstRetrievedAt.toISOString(), @@ -42,9 +34,8 @@ export class InstanceEntityService { followingCount: instance.followingCount, followersCount: instance.followersCount, isNotResponding: instance.isNotResponding, - isSuspended: instance.suspensionState !== 'none' || Boolean(softwareSuspended), - suspensionState: instance.suspensionState === 'none' && softwareSuspended ? 'softwareSuspended' : instance.suspensionState, - isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host), + isSuspended: instance.isSuspended, + isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host), softwareName: instance.softwareName, softwareVersion: instance.softwareVersion, openRegistrations: instance.openRegistrations, @@ -52,23 +43,18 @@ export class InstanceEntityService { description: instance.description, maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, - isSilenced: this.utilityService.isSilencedHost(this.meta.silencedHosts, instance.host), - isMediaSilenced: this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, instance.host), iconUrl: instance.iconUrl, faviconUrl: instance.faviconUrl, themeColor: instance.themeColor, infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, - latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null, - moderationNote: iAmModerator ? instance.moderationNote : null, }; } @bindThis public packMany( - instances: MiInstance[], - me?: { id: MiUser['id']; } | null | undefined, + instances: Instance[], ) { - return Promise.all(instances.map(x => this.pack(x, me))); + return Promise.all(instances.map(x => this.pack(x))); } } diff --git a/packages/backend/src/core/entities/InviteCodeEntityService.ts b/packages/backend/src/core/entities/InviteCodeEntityService.ts deleted file mode 100644 index 5d3e823a2a..0000000000 --- a/packages/backend/src/core/entities/InviteCodeEntityService.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { RegistrationTicketsRepository } from '@/models/_.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/json-schema.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js'; -import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; -import { UserEntityService } from './UserEntityService.js'; - -@Injectable() -export class InviteCodeEntityService { - constructor( - @Inject(DI.registrationTicketsRepository) - private registrationTicketsRepository: RegistrationTicketsRepository, - - private userEntityService: UserEntityService, - private idService: IdService, - ) { - } - - @bindThis - public async pack( - src: MiRegistrationTicket['id'] | MiRegistrationTicket, - me?: { id: MiUser['id'] } | null | undefined, - hints?: { - packedCreatedBy?: Packed<'UserLite'>, - packedUsedBy?: Packed<'UserLite'>, - }, - ): Promise> { - const target = typeof src === 'object' ? src : await this.registrationTicketsRepository.findOneOrFail({ - where: { - id: src, - }, - relations: ['createdBy', 'usedBy'], - }); - - return await awaitAll({ - id: target.id, - code: target.code, - expiresAt: target.expiresAt ? target.expiresAt.toISOString() : null, - createdAt: this.idService.parse(target.id).date.toISOString(), - createdBy: target.createdBy ? hints?.packedCreatedBy ?? await this.userEntityService.pack(target.createdBy, me) : null, - usedBy: target.usedBy ? hints?.packedUsedBy ?? await this.userEntityService.pack(target.usedBy, me) : null, - usedAt: target.usedAt ? target.usedAt.toISOString() : null, - used: !!target.usedAt, - }); - } - - @bindThis - public async packMany( - tickets: MiRegistrationTicket[], - me: { id: MiUser['id'] }, - ) { - const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(x => x != null); - const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(x => x != null); - const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all( - tickets.map(ticket => { - const packedCreatedBy = ticket.createdById != null ? _userMap.get(ticket.createdById) : undefined; - const packedUsedBy = ticket.usedById != null ? _userMap.get(ticket.usedById) : undefined; - return this.pack(ticket, me, { packedCreatedBy, packedUsedBy }); - }), - ); - } -} diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts deleted file mode 100644 index 02783dc450..0000000000 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ /dev/null @@ -1,176 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Brackets } from 'typeorm'; -import { Inject, Injectable } from '@nestjs/common'; -import JSON5 from 'json5'; -import type { Packed } from '@/misc/json-schema.js'; -import type { MiMeta } from '@/models/Meta.js'; -import type { AdsRepository } from '@/models/_.js'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { bindThis } from '@/decorators.js'; -import { SystemAccountService } from '@/core/SystemAccountService.js'; -import type { Config } from '@/config.js'; -import { DI } from '@/di-symbols.js'; -import { DEFAULT_POLICIES } from '@/core/RoleService.js'; - -@Injectable() -export class MetaEntityService { - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.meta) - private meta: MiMeta, - - @Inject(DI.adsRepository) - private adsRepository: AdsRepository, - - private systemAccountService: SystemAccountService, - ) { } - - @bindThis - public async pack(meta?: MiMeta): Promise> { - let instance = meta; - - if (!instance) { - instance = this.meta; - } - - const ads = await this.adsRepository.createQueryBuilder('ads') - .where('ads.expiresAt > :now', { now: new Date() }) - .andWhere('ads.startsAt <= :now', { now: new Date() }) - .andWhere(new Brackets(qb => { - // 曜日のビットフラグを確認する - qb.where('ads.dayOfWeek & :dayOfWeek > 0', { dayOfWeek: 1 << new Date().getDay() }) - .orWhere('ads.dayOfWeek = 0'); - })) - .getMany(); - - // クライアントの手間を減らすためあらかじめJSONに変換しておく - let defaultLightTheme = null; - let defaultDarkTheme = null; - if (instance.defaultLightTheme) { - try { - defaultLightTheme = JSON.stringify(JSON5.parse(instance.defaultLightTheme)); - } catch (e) { - } - } - if (instance.defaultDarkTheme) { - try { - defaultDarkTheme = JSON.stringify(JSON5.parse(instance.defaultDarkTheme)); - } catch (e) { - } - } - - const packed: Packed<'MetaLite'> = { - maintainerName: instance.maintainerName, - maintainerEmail: instance.maintainerEmail, - - version: this.config.version, - providesTarball: this.config.publishTarballInsteadOfProvideRepositoryUrl, - - name: instance.name, - shortName: instance.shortName, - uri: this.config.url, - description: instance.description, - langs: instance.langs, - tosUrl: instance.termsOfServiceUrl, - repositoryUrl: instance.repositoryUrl, - feedbackUrl: instance.feedbackUrl, - impressumUrl: instance.impressumUrl, - privacyPolicyUrl: instance.privacyPolicyUrl, - inquiryUrl: instance.inquiryUrl, - disableRegistration: instance.disableRegistration, - emailRequiredForSignup: instance.emailRequiredForSignup, - enableHcaptcha: instance.enableHcaptcha, - hcaptchaSiteKey: instance.hcaptchaSiteKey, - enableMcaptcha: instance.enableMcaptcha, - mcaptchaSiteKey: instance.mcaptchaSitekey, - mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl, - enableRecaptcha: instance.enableRecaptcha, - recaptchaSiteKey: instance.recaptchaSiteKey, - enableTurnstile: instance.enableTurnstile, - turnstileSiteKey: instance.turnstileSiteKey, - enableTestcaptcha: instance.enableTestcaptcha, - googleAnalyticsMeasurementId: instance.googleAnalyticsMeasurementId, - swPublickey: instance.swPublicKey, - themeColor: instance.themeColor, - mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png', - bannerUrl: instance.bannerUrl, - infoImageUrl: instance.infoImageUrl, - serverErrorImageUrl: instance.serverErrorImageUrl, - notFoundImageUrl: instance.notFoundImageUrl, - iconUrl: instance.iconUrl, - backgroundImageUrl: instance.backgroundImageUrl, - logoImageUrl: instance.logoImageUrl, - maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, - defaultLightTheme, - defaultDarkTheme, - ads: ads.map(ad => ({ - id: ad.id, - url: ad.url, - place: ad.place, - ratio: ad.ratio, - imageUrl: ad.imageUrl, - dayOfWeek: ad.dayOfWeek, - })), - notesPerOneAd: instance.notesPerOneAd, - enableEmail: instance.enableEmail, - enableServiceWorker: instance.enableServiceWorker, - - translatorAvailable: instance.deeplAuthKey != null, - - serverRules: instance.serverRules, - - policies: { ...DEFAULT_POLICIES, ...instance.policies }, - - sentryForFrontend: this.config.sentryForFrontend ?? null, - mediaProxy: this.config.mediaProxy, - enableUrlPreview: instance.urlPreviewEnabled, - noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local', - maxFileSize: this.config.maxFileSize, - federation: this.meta.federation, - }; - - return packed; - } - - @bindThis - public async packDetailed(meta?: MiMeta): Promise> { - let instance = meta; - - if (!instance) { - instance = this.meta; - } - - const packed = await this.pack(instance); - - const proxyAccount = await this.systemAccountService.fetch('proxy'); - - const packDetailed: Packed<'MetaDetailed'> = { - ...packed, - cacheRemoteFiles: instance.cacheRemoteFiles, - cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles, - requireSetup: this.meta.rootUserId == null, - proxyAccountName: proxyAccount.username, - features: { - localTimeline: instance.policies.ltlAvailable, - globalTimeline: instance.policies.gtlAvailable, - registration: !instance.disableRegistration, - emailRequiredForSignup: instance.emailRequiredForSignup, - hcaptcha: instance.enableHcaptcha, - recaptcha: instance.enableRecaptcha, - turnstile: instance.enableTurnstile, - objectStorage: instance.useObjectStorage, - serviceWorker: instance.enableServiceWorker, - miauth: true, - }, - }; - - return packDetailed; - } -} - diff --git a/packages/backend/src/core/entities/ModerationLogEntityService.ts b/packages/backend/src/core/entities/ModerationLogEntityService.ts index bf1b2a002c..7058e38af9 100644 --- a/packages/backend/src/core/entities/ModerationLogEntityService.ts +++ b/packages/backend/src/core/entities/ModerationLogEntityService.ts @@ -1,18 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { ModerationLogsRepository } from '@/models/_.js'; +import type { ModerationLogsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { } from '@/models/Blocking.js'; -import { MiModerationLog } from '@/models/ModerationLog.js'; -import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; -import type { Packed } from '@/misc/json-schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { ModerationLog } from '@/models/entities/ModerationLog.js'; import { UserEntityService } from './UserEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class ModerationLogEntityService { @@ -21,39 +14,32 @@ export class ModerationLogEntityService { private moderationLogsRepository: ModerationLogsRepository, private userEntityService: UserEntityService, - private idService: IdService, ) { } @bindThis public async pack( - src: MiModerationLog['id'] | MiModerationLog, - hint?: { - packedUser?: Packed<'UserDetailedNotMe'>, - }, + src: ModerationLog['id'] | ModerationLog, ) { const log = typeof src === 'object' ? src : await this.moderationLogsRepository.findOneByOrFail({ id: src }); return await awaitAll({ id: log.id, - createdAt: this.idService.parse(log.id).date.toISOString(), + createdAt: log.createdAt.toISOString(), type: log.type, info: log.info, userId: log.userId, - user: hint?.packedUser ?? this.userEntityService.pack(log.user ?? log.userId, null, { - schema: 'UserDetailedNotMe', + user: this.userEntityService.pack(log.user ?? log.userId, null, { + detail: true, }), }); } @bindThis - public async packMany( - reports: MiModerationLog[], + public packMany( + reports: any[], ) { - const _users = reports.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, null, { schema: 'UserDetailedNotMe' }) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(reports.map(report => this.pack(report, { packedUser: _userMap.get(report.userId) }))); + return Promise.all(reports.map(x => this.pack(x))); } } diff --git a/packages/backend/src/core/entities/MutingEntityService.ts b/packages/backend/src/core/entities/MutingEntityService.ts index d361a20271..561d53292e 100644 --- a/packages/backend/src/core/entities/MutingEntityService.ts +++ b/packages/backend/src/core/entities/MutingEntityService.ts @@ -1,18 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { MutingsRepository } from '@/models/_.js'; +import type { MutingsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiMuting } from '@/models/Muting.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Muting } from '@/models/entities/Muting.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; import { UserEntityService } from './UserEntityService.js'; @Injectable() @@ -22,40 +16,33 @@ export class MutingEntityService { private mutingsRepository: MutingsRepository, private userEntityService: UserEntityService, - private idService: IdService, ) { } @bindThis public async pack( - src: MiMuting['id'] | MiMuting, - me?: { id: MiUser['id'] } | null | undefined, - hints?: { - packedMutee?: Packed<'UserDetailedNotMe'>, - }, + src: Muting['id'] | Muting, + me?: { id: User['id'] } | null | undefined, ): Promise> { const muting = typeof src === 'object' ? src : await this.mutingsRepository.findOneByOrFail({ id: src }); return await awaitAll({ id: muting.id, - createdAt: this.idService.parse(muting.id).date.toISOString(), + createdAt: muting.createdAt.toISOString(), expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null, muteeId: muting.muteeId, - mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, { - schema: 'UserDetailedNotMe', + mutee: this.userEntityService.pack(muting.muteeId, me, { + detail: true, }), }); } @bindThis - public async packMany( - mutings: MiMuting[], - me: { id: MiUser['id'] }, + public packMany( + mutings: any[], + me: { id: User['id'] }, ) { - const _mutees = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId); - const _userMap = await this.userEntityService.packMany(_mutees, me, { schema: 'UserDetailedNotMe' }) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) }))); + return Promise.all(mutings.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 92caad908c..32269a4101 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -1,66 +1,35 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; +import { DataSource, In } from 'typeorm'; +import * as mfm from 'mfm-js'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { Packed } from '@/misc/json-schema.js'; +import { nyaize } from '@/misc/nyaize.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiNote } from '@/models/Note.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js'; +import type { User } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { NoteReaction } from '@/models/entities/NoteReaction.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, DriveFilesRepository } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; -import { DebounceLoader } from '@/misc/loader.js'; -import { IdService } from '@/core/IdService.js'; -import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; +import { isNotNull } from '@/misc/is-not-null.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { ReactionService } from '../ReactionService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js'; -// is-renote.tsとよしなにリンク -function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } { - return ( - note.renote != null && - note.reply == null && - note.text == null && - note.cw == null && - (note.fileIds == null || note.fileIds.length === 0) && - !note.hasPoll - ); -} - -function getAppearNoteIds(notes: MiNote[]): Set { - const appearNoteIds = new Set(); - for (const note of notes) { - if (isPureRenote(note)) { - appearNoteIds.add(note.renoteId); - } else { - appearNoteIds.add(note.id); - } - } - return appearNoteIds; -} - @Injectable() export class NoteEntityService implements OnModuleInit { private userEntityService: UserEntityService; private driveFileEntityService: DriveFileEntityService; private customEmojiService: CustomEmojiService; private reactionService: ReactionService; - private reactionsBufferingService: ReactionsBufferingService; - private idService: IdService; - private noteLoader = new DebounceLoader(this.findNoteOrFail); - + constructor( private moduleRef: ModuleRef, - @Inject(DI.meta) - private meta: MiMeta, + @Inject(DI.db) + private db: DataSource, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -83,12 +52,13 @@ export class NoteEntityService implements OnModuleInit { @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + //private userEntityService: UserEntityService, //private driveFileEntityService: DriveFileEntityService, //private customEmojiService: CustomEmojiService, //private reactionService: ReactionService, - //private reactionsBufferingService: ReactionsBufferingService, - //private idService: IdService, ) { } @@ -97,87 +67,54 @@ export class NoteEntityService implements OnModuleInit { this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.reactionService = this.moduleRef.get('ReactionService'); - this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService'); - this.idService = this.moduleRef.get('IdService'); } - + @bindThis - private treatVisibility(packedNote: Packed<'Note'>): Packed<'Note'>['visibility'] { - if (packedNote.visibility === 'public' || packedNote.visibility === 'home') { - const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore; - if ((followersOnlyBefore != null) - && ( - (followersOnlyBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (followersOnlyBefore * 1000))) - || (followersOnlyBefore > 0 && (new Date(packedNote.createdAt).getTime() < followersOnlyBefore * 1000)) - ) - ) { - packedNote.visibility = 'followers'; - } - } - return packedNote.visibility; - } - - @bindThis - private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise { - if (meId === packedNote.userId) return; - - // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) + private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) { + // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) let hide = false; - if (packedNote.user.requireSigninToViewContents && meId == null) { - hide = true; - } - - if (!hide) { - const hiddenBefore = packedNote.user.makeNotesHiddenBefore; - if ((hiddenBefore != null) - && ( - (hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000))) - || (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000)) - ) - ) { - hide = true; - } - } - // visibility が specified かつ自分が指定されていなかったら非表示 - if (!hide) { - if (packedNote.visibility === 'specified') { - if (meId == null) { - hide = true; - } else { - // 指定されているかどうか - const specified = packedNote.visibleUserIds!.some(id => meId === id); + if (packedNote.visibility === 'specified') { + if (meId == null) { + hide = true; + } else if (meId === packedNote.userId) { + hide = false; + } else { + // 指定されているかどうか + const specified = packedNote.visibleUserIds!.some((id: any) => meId === id); - if (!specified) { - hide = true; - } + if (specified) { + hide = false; + } else { + hide = true; } } } // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示 - if (!hide) { - if (packedNote.visibility === 'followers') { - if (meId == null) { - hide = true; - } else if (packedNote.reply && (meId === packedNote.reply.userId)) { - // 自分の投稿に対するリプライ - hide = false; - } else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) { - // 自分へのメンション - hide = false; - } else { - // フォロワーかどうか - // TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: packedNote.userId, - followerId: meId, - }, - }); + if (packedNote.visibility === 'followers') { + if (meId == null) { + hide = true; + } else if (meId === packedNote.userId) { + hide = false; + } else if (packedNote.reply && (meId === packedNote.reply.userId)) { + // 自分の投稿に対するリプライ + hide = false; + } else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) { + // 自分へのメンション + hide = false; + } else { + // フォロワーかどうか + const following = await this.followingsRepository.findOneBy({ + followeeId: packedNote.userId, + followerId: meId, + }); - hide = !isFollowing; + if (following == null) { + hide = true; + } else { + hide = false; } } } @@ -190,12 +127,11 @@ export class NoteEntityService implements OnModuleInit { packedNote.poll = undefined; packedNote.cw = null; packedNote.isHidden = true; - // TODO: hiddenReason みたいなのを提供しても良さそう } } @bindThis - private async populatePoll(note: MiNote, meId: MiUser['id'] | null) { + private async populatePoll(note: Note, meId: User['id'] | null) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id }); const choices = poll.choices.map(c => ({ text: c, @@ -228,37 +164,27 @@ export class NoteEntityService implements OnModuleInit { return { multiple: poll.multiple, - expiresAt: poll.expiresAt?.toISOString() ?? null, + expiresAt: poll.expiresAt, choices, }; } @bindThis - public async populateMyReaction(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: { - myReactions: Map; + private async populateMyReaction(note: Note, meId: User['id'], _hint_?: { + myReactions: Map; }) { if (_hint_?.myReactions) { const reaction = _hint_.myReactions.get(note.id); if (reaction) { - return this.reactionService.convertLegacyReaction(reaction); - } else { + return this.reactionService.convertLegacyReaction(reaction.reaction); + } else if (reaction === null) { return undefined; } + // 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない } - const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0); - if (reactionsCount === 0) return undefined; - if (note.reactionAndUserPairCache && reactionsCount <= note.reactionAndUserPairCache.length) { - const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId)); - if (pair) { - return this.reactionService.convertLegacyReaction(pair.split('/')[1]); - } else { - return undefined; - } - } - - // パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない - if (this.idService.parse(note.id).date.getTime() + 2000 > Date.now()) { + // パフォーマンスのためノートが作成されてから1秒以上経っていない場合はリアクションを取得しない + if (note.createdAt.getTime() + 1000 > Date.now()) { return undefined; } @@ -275,7 +201,7 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null): Promise { + public async isVisibleForMe(note: Note, meId: User['id'] | null): Promise { // This code must always be synchronized with the checks in generateVisibilityQuery. // visibility が specified かつ自分が指定されていなかったら非表示 if (note.visibility === 'specified') { @@ -285,7 +211,7 @@ export class NoteEntityService implements OnModuleInit { return true; } else { // 指定されているかどうか - return note.visibleUserIds.some(id => meId === id); + return note.visibleUserIds.some((id: any) => meId === id); } } @@ -329,7 +255,7 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - public async packAttachedFiles(fileIds: MiNote['fileIds'], packedFiles: Map | null>): Promise[]> { + public async packAttachedFiles(fileIds: Note['fileIds'], packedFiles: Map | null>): Promise[]> { const missingIds = []; for (const id of fileIds) { if (!packedFiles.has(id)) missingIds.push(id); @@ -340,44 +266,31 @@ export class NoteEntityService implements OnModuleInit { packedFiles.set(k, v); } } - return fileIds.map(id => packedFiles.get(id)).filter(x => x != null); + return fileIds.map(id => packedFiles.get(id)).filter(isNotNull); } @bindThis public async pack( - src: MiNote['id'] | MiNote, - me?: { id: MiUser['id'] } | null | undefined, + src: Note['id'] | Note, + me?: { id: User['id'] } | null | undefined, options?: { detail?: boolean; skipHide?: boolean; - withReactionAndUserPairCache?: boolean; _hint_?: { - bufferedReactions: Map; pairs: ([MiUser['id'], string])[] }> | null; - myReactions: Map; - packedFiles: Map | null>; - packedUsers: Map> + myReactions: Map; + packedFiles: Map | null>; }; }, ): Promise> { const opts = Object.assign({ detail: true, skipHide: false, - withReactionAndUserPairCache: false, }, options); const meId = me ? me.id : null; - const note = typeof src === 'object' ? src : await this.noteLoader.load(src); + const note = typeof src === 'object' ? src : await this.notesRepository.findOneOrFail({ where: { id: src }, relations: ['user'] }); const host = note.userHost; - const bufferedReactions = opts._hint_?.bufferedReactions != null - ? (opts._hint_.bufferedReactions.get(note.id) ?? { deltas: {}, pairs: [] }) - : this.meta.enableReactionsBuffering - ? await this.reactionsBufferingService.get(note.id) - : { deltas: {}, pairs: [] }; - const reactions = this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions.deltas ?? {})); - - const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/'))); - let text = note.text; if (note.name && (note.url ?? note.uri)) { @@ -390,29 +303,28 @@ export class NoteEntityService implements OnModuleInit { : await this.channelsRepository.findOneBy({ id: note.channelId }) : null; - const reactionEmojiNames = Object.keys(reactions) + const reactionEmojiNames = Object.keys(note.reactions) .filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ .map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', '')); const packedFiles = options?._hint_?.packedFiles; - const packedUsers = options?._hint_?.packedUsers; const packed: Packed<'Note'> = await awaitAll({ id: note.id, - createdAt: this.idService.parse(note.id).date.toISOString(), + createdAt: note.createdAt.toISOString(), userId: note.userId, - user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me), + user: this.userEntityService.pack(note.user ?? note.userId, me, { + detail: false, + }), text: text, cw: note.cw, visibility: note.visibility, - localOnly: note.localOnly, + localOnly: note.localOnly ?? undefined, reactionAcceptance: note.reactionAcceptance, visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined, renoteCount: note.renoteCount, repliesCount: note.repliesCount, - reactionCount: Object.values(reactions).reduce((a, b) => a + b, 0), - reactions: reactions, + reactions: this.reactionService.convertLegacyReactions(note.reactions), reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host), - reactionAndUserPairCache: opts.withReactionAndUserPairCache ? reactionAndUserPairCache : undefined, emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined, tags: note.tags.length > 0 ? note.tags : undefined, fileIds: note.fileIds, @@ -424,45 +336,48 @@ export class NoteEntityService implements OnModuleInit { id: channel.id, name: channel.name, color: channel.color, - isSensitive: channel.isSensitive, - allowRenoteToExternal: channel.allowRenoteToExternal, - userId: channel.userId, } : undefined, mentions: note.mentions.length > 0 ? note.mentions : undefined, - hasPoll: note.hasPoll || undefined, uri: note.uri ?? undefined, url: note.url ?? undefined, ...(opts.detail ? { - clippedCount: note.clippedCount, - reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, { detail: false, - skipHide: opts.skipHide, - withReactionAndUserPairCache: opts.withReactionAndUserPairCache, _hint_: options?._hint_, }) : undefined, renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, { detail: true, - skipHide: opts.skipHide, - withReactionAndUserPairCache: opts.withReactionAndUserPairCache, _hint_: options?._hint_, }) : undefined, poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, - ...(meId && Object.keys(reactions).length > 0 ? { - myReaction: this.populateMyReaction({ - id: note.id, - reactions: reactions, - reactionAndUserPairCache: reactionAndUserPairCache, - }, meId, options?._hint_), + ...(meId ? { + myReaction: this.populateMyReaction(note, meId, options?._hint_), } : {}), } : {}), }); - this.treatVisibility(packed); + if (packed.user.isCat && packed.text) { + const tokens = packed.text ? mfm.parse(packed.text) : []; + function nyaizeNode(node: mfm.MfmNode) { + if (node.type === 'quote') return; + if (node.type === 'text') { + node.props.text = nyaize(node.props.text); + } + if (node.children) { + for (const child of node.children) { + nyaizeNode(child); + } + } + } + for (const node of tokens) { + nyaizeNode(node); + } + packed.text = mfm.toString(tokens); + } if (!opts.skipHide) { await this.hideNote(packed, meId); @@ -473,8 +388,8 @@ export class NoteEntityService implements OnModuleInit { @bindThis public async packMany( - notes: MiNote[], - me?: { id: MiUser['id'] } | null | undefined, + notes: Note[], + me?: { id: User['id'] } | null | undefined, options?: { detail?: boolean; skipHide?: boolean; @@ -482,89 +397,38 @@ export class NoteEntityService implements OnModuleInit { ) { if (notes.length === 0) return []; - const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany([...getAppearNoteIds(notes)]) : null; - const meId = me ? me.id : null; - const myReactionsMap = new Map(); + const myReactionsMap = new Map(); if (meId) { - const idsNeedFetchMyReaction = new Set(); - - // パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない - const oldId = this.idService.gen(Date.now() - 2000); - - for (const note of notes) { - if (isPureRenote(note)) { - const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0); - if (reactionsCount === 0) { - myReactionsMap.set(note.renote.id, null); - } else if (reactionsCount <= note.renote.reactionAndUserPairCache.length + (bufferedReactions?.get(note.renote.id)?.pairs.length ?? 0)) { - const pairInBuffer = bufferedReactions?.get(note.renote.id)?.pairs.find(p => p[0] === meId); - if (pairInBuffer) { - myReactionsMap.set(note.renote.id, pairInBuffer[1]); - } else { - const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId)); - myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null); - } - } else { - idsNeedFetchMyReaction.add(note.renote.id); - } - } else { - if (note.id < oldId) { - const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0); - if (reactionsCount === 0) { - myReactionsMap.set(note.id, null); - } else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) { - const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId); - if (pairInBuffer) { - myReactionsMap.set(note.id, pairInBuffer[1]); - } else { - const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId)); - myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null); - } - } else { - idsNeedFetchMyReaction.add(note.id); - } - } else { - myReactionsMap.set(note.id, null); - } - } - } - - const myReactions = idsNeedFetchMyReaction.size > 0 ? await this.noteReactionsRepository.findBy({ + const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!); + // パフォーマンスのためノートが作成されてから1秒以上経っていない場合はリアクションを取得しない + const targets = [...notes.filter(n => n.createdAt.getTime() + 1000 < Date.now()).map(n => n.id), ...renoteIds]; + const myReactions = await this.noteReactionsRepository.findBy({ userId: meId, - noteId: In(Array.from(idsNeedFetchMyReaction)), - }) : []; + noteId: In(targets), + }); - for (const id of idsNeedFetchMyReaction) { - myReactionsMap.set(id, myReactions.find(reaction => reaction.noteId === id)?.reaction ?? null); + for (const target of targets) { + myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null); } } await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes)); // TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく - const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null); + const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull); const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map(); - const users = [ - ...notes.map(({ user, userId }) => user ?? userId), - ...notes.map(({ replyUserId }) => replyUserId).filter(x => x != null), - ...notes.map(({ renoteUserId }) => renoteUserId).filter(x => x != null), - ]; - const packedUsers = await this.userEntityService.packMany(users, me) - .then(users => new Map(users.map(u => [u.id, u]))); return await Promise.all(notes.map(n => this.pack(n, me, { ...options, _hint_: { - bufferedReactions, myReactions: myReactionsMap, packedFiles, - packedUsers, }, }))); } @bindThis - public aggregateNoteEmojis(notes: MiNote[]) { + public aggregateNoteEmojis(notes: Note[]) { let emojis: { name: string | null; host: string | null; }[] = []; for (const note of notes) { emojis = emojis.concat(note.emojis @@ -588,48 +452,17 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - private findNoteOrFail(id: string): Promise { - return this.notesRepository.findOneOrFail({ - where: { id }, - relations: ['user'], - }); - } - - @bindThis - public async fetchDiffs(noteIds: MiNote['id'][]) { - if (noteIds.length === 0) return []; - - const notes = await this.notesRepository.find({ - where: { - id: In(noteIds), - }, - select: { - id: true, - userHost: true, - reactions: true, - reactionAndUserPairCache: true, - }, - }); - - const bufferedReactionsMap = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(noteIds) : null; - - const packings = notes.map(note => { - const bufferedReactions = bufferedReactionsMap?.get(note.id); - //const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/'))); - - const reactions = this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.deltas ?? {})); - - const reactionEmojiNames = Object.keys(reactions) - .filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ - .map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', '')); - - return this.customEmojiService.populateEmojis(reactionEmojiNames, note.userHost).then(reactionEmojis => ({ - id: note.id, - reactions, - reactionEmojis, - })); - }); - - return await Promise.all(packings); - } + public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise { + // 指定したユーザーの指定したノートのリノートがいくつあるか数える + const query = this.notesRepository.createQueryBuilder('note') + .where('note.userId = :userId', { userId }) + .andWhere('note.renoteId = :renoteId', { renoteId }); + + // 指定した投稿を除く + if (excludeNoteId) { + query.andWhere('note.id != :excludeNoteId', { excludeNoteId }); + } + + return await query.getCount(); + } } diff --git a/packages/backend/src/core/entities/NoteFavoriteEntityService.ts b/packages/backend/src/core/entities/NoteFavoriteEntityService.ts index 3cdafe48ad..8a7727b4cd 100644 --- a/packages/backend/src/core/entities/NoteFavoriteEntityService.ts +++ b/packages/backend/src/core/entities/NoteFavoriteEntityService.ts @@ -1,17 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NoteFavoritesRepository } from '@/models/_.js'; -import type { } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiNoteFavorite } from '@/models/NoteFavorite.js'; -import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; +import type { NoteFavoritesRepository } from '@/models/index.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { NoteFavorite } from '@/models/entities/NoteFavorite.js'; import { NoteEntityService } from './NoteEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class NoteFavoriteEntityService { @@ -20,20 +14,19 @@ export class NoteFavoriteEntityService { private noteFavoritesRepository: NoteFavoritesRepository, private noteEntityService: NoteEntityService, - private idService: IdService, ) { } @bindThis public async pack( - src: MiNoteFavorite['id'] | MiNoteFavorite, - me?: { id: MiUser['id'] } | null | undefined, + src: NoteFavorite['id'] | NoteFavorite, + me?: { id: User['id'] } | null | undefined, ) { const favorite = typeof src === 'object' ? src : await this.noteFavoritesRepository.findOneByOrFail({ id: src }); return { id: favorite.id, - createdAt: this.idService.parse(favorite.id).date.toISOString(), + createdAt: favorite.createdAt.toISOString(), noteId: favorite.noteId, note: await this.noteEntityService.pack(favorite.note ?? favorite.noteId, me), }; @@ -42,7 +35,7 @@ export class NoteFavoriteEntityService { @bindThis public packMany( favorites: any[], - me: { id: MiUser['id'] }, + me: { id: User['id'] }, ) { return Promise.all(favorites.map(x => this.pack(x, me))); } diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts index 46ec13704c..8f943ba24c 100644 --- a/packages/backend/src/core/entities/NoteReactionEntityService.ts +++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts @@ -1,18 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NoteReactionsRepository } from '@/models/_.js'; +import type { NoteReactionsRepository } from '@/models/index.js'; import type { Packed } from '@/misc/json-schema.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; import type { OnModuleInit } from '@nestjs/common'; -import type { } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiNoteReaction } from '@/models/NoteReaction.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { NoteReaction } from '@/models/entities/NoteReaction.js'; import type { ReactionService } from '../ReactionService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; @@ -23,8 +17,7 @@ export class NoteReactionEntityService implements OnModuleInit { private userEntityService: UserEntityService; private noteEntityService: NoteEntityService; private reactionService: ReactionService; - private idService: IdService; - + constructor( private moduleRef: ModuleRef, @@ -34,7 +27,6 @@ export class NoteReactionEntityService implements OnModuleInit { //private userEntityService: UserEntityService, //private noteEntityService: NoteEntityService, //private reactionService: ReactionService, - //private idService: IdService, ) { } @@ -42,19 +34,15 @@ export class NoteReactionEntityService implements OnModuleInit { this.userEntityService = this.moduleRef.get('UserEntityService'); this.noteEntityService = this.moduleRef.get('NoteEntityService'); this.reactionService = this.moduleRef.get('ReactionService'); - this.idService = this.moduleRef.get('IdService'); } @bindThis public async pack( - src: MiNoteReaction['id'] | MiNoteReaction, - me?: { id: MiUser['id'] } | null | undefined, + src: NoteReaction['id'] | NoteReaction, + me?: { id: User['id'] } | null | undefined, options?: { withNote: boolean; }, - hints?: { - packedUser?: Packed<'UserLite'> - }, ): Promise> { const opts = Object.assign({ withNote: false, @@ -64,29 +52,12 @@ export class NoteReactionEntityService implements OnModuleInit { return { id: reaction.id, - createdAt: this.idService.parse(reaction.id).date.toISOString(), - user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me), + createdAt: reaction.createdAt.toISOString(), + user: await this.userEntityService.pack(reaction.user ?? reaction.userId, me), type: this.reactionService.convertLegacyReaction(reaction.reaction), ...(opts.withNote ? { note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me), } : {}), }; } - - @bindThis - public async packMany( - reactions: MiNoteReaction[], - me?: { id: MiUser['id'] } | null | undefined, - options?: { - withNote: boolean; - }, - ): Promise[]> { - const opts = Object.assign({ - withNote: false, - }, options); - const _users = reactions.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, me) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) }))); - } } diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index e91fb9eb51..d76b863957 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -1,34 +1,27 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { FollowRequestsRepository, NotesRepository, MiUser, UsersRepository } from '@/models/_.js'; +import type { AccessTokensRepository, FollowRequestsRepository, NoteReactionsRepository, NotesRepository, User, UsersRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { MiGroupedNotification, MiNotification } from '@/models/Notification.js'; -import type { MiNote } from '@/models/Note.js'; +import type { Notification } from '@/models/entities/Notification.js'; +import type { Note } from '@/models/entities/Note.js'; import type { Packed } from '@/misc/json-schema.js'; import { bindThis } from '@/decorators.js'; -import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js'; -import { CacheService } from '@/core/CacheService.js'; -import { RoleEntityService } from './RoleEntityService.js'; -import { ChatEntityService } from './ChatEntityService.js'; +import { isNotNull } from '@/misc/is-not-null.js'; +import { notificationTypes } from '@/types.js'; import type { OnModuleInit } from '@nestjs/common'; +import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; -const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded'] as (typeof groupedNotificationTypes[number])[]); +const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]); @Injectable() export class NotificationEntityService implements OnModuleInit { private userEntityService: UserEntityService; private noteEntityService: NoteEntityService; - private roleEntityService: RoleEntityService; - private chatEntityService: ChatEntityService; + private customEmojiService: CustomEmojiService; constructor( private moduleRef: ModuleRef, @@ -39,164 +32,88 @@ export class NotificationEntityService implements OnModuleInit { @Inject(DI.usersRepository) private usersRepository: UsersRepository, + @Inject(DI.noteReactionsRepository) + private noteReactionsRepository: NoteReactionsRepository, + @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, - private cacheService: CacheService, + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, + + //private userEntityService: UserEntityService, + //private noteEntityService: NoteEntityService, + //private customEmojiService: CustomEmojiService, ) { } onModuleInit() { this.userEntityService = this.moduleRef.get('UserEntityService'); this.noteEntityService = this.moduleRef.get('NoteEntityService'); - this.roleEntityService = this.moduleRef.get('RoleEntityService'); - this.chatEntityService = this.moduleRef.get('ChatEntityService'); + this.customEmojiService = this.moduleRef.get('CustomEmojiService'); } - /** - * 通知をパックする共通処理 - */ - async #packInternal ( - src: T, - meId: MiUser['id'], + @bindThis + public async pack( + src: Notification, + meId: User['id'], + // eslint-disable-next-line @typescript-eslint/ban-types options: { - checkValidNotifier?: boolean; + }, hint?: { - packedNotes: Map>; - packedUsers: Map>; + packedNotes: Map>; + packedUsers: Map>; }, - ): Promise | null> { + ): Promise> { const notification = src; - - if (options.checkValidNotifier !== false && !(await this.#isValidNotifier(notification, meId))) return null; - - const needsNote = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification; - const noteIfNeed = needsNote ? ( + const token = notification.appAccessTokenId ? await this.accessTokensRepository.findOneByOrFail({ id: notification.appAccessTokenId }) : null; + const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && notification.noteId != null ? ( hint?.packedNotes != null ? hint.packedNotes.get(notification.noteId) - : this.noteEntityService.pack(notification.noteId, { id: meId }, { + : this.noteEntityService.pack(notification.noteId!, { id: meId }, { detail: true, }) ) : undefined; - // if the note has been deleted, don't show this notification - if (needsNote && !noteIfNeed) return null; - - const needsUser = 'notifierId' in notification; - const userIfNeed = needsUser ? ( + const userIfNeed = notification.notifierId != null ? ( hint?.packedUsers != null ? hint.packedUsers.get(notification.notifierId) - : this.userEntityService.pack(notification.notifierId, { id: meId }) + : this.userEntityService.pack(notification.notifierId!, { id: meId }, { + detail: false, + }) ) : undefined; - // if the user has been deleted, don't show this notification - if (needsUser && !userIfNeed) return null; - - //#region Grouped notifications - if (notification.type === 'reaction:grouped') { - const reactions = (await Promise.all(notification.reactions.map(async reaction => { - const user = hint?.packedUsers != null - ? hint.packedUsers.get(reaction.userId)! - : await this.userEntityService.pack(reaction.userId, { id: meId }); - return { - user, - reaction: reaction.reaction, - }; - }))).filter(r => r.user != null); - // if all users have been deleted, don't show this notification - if (reactions.length === 0) { - return null; - } - - return await awaitAll({ - id: notification.id, - createdAt: new Date(notification.createdAt).toISOString(), - type: notification.type, - note: noteIfNeed, - reactions, - }); - } else if (notification.type === 'renote:grouped') { - const users = (await Promise.all(notification.userIds.map(userId => { - const packedUser = hint?.packedUsers != null ? hint.packedUsers.get(userId) : null; - if (packedUser) { - return packedUser; - } - - return this.userEntityService.pack(userId, { id: meId }); - }))).filter(x => x != null); - // if all users have been deleted, don't show this notification - if (users.length === 0) { - return null; - } - - return await awaitAll({ - id: notification.id, - createdAt: new Date(notification.createdAt).toISOString(), - type: notification.type, - note: noteIfNeed, - users, - }); - } - //#endregion - - const needsRole = notification.type === 'roleAssigned'; - const role = needsRole ? await this.roleEntityService.pack(notification.roleId) : undefined; - // if the role has been deleted, don't show this notification - if (needsRole && !role) { - return null; - } - - const needsChatRoomInvitation = notification.type === 'chatRoomInvitationReceived'; - const chatRoomInvitation = needsChatRoomInvitation ? await this.chatEntityService.packRoomInvitation(notification.invitationId, { id: meId }).catch(() => null) : undefined; - // if the invitation has been deleted, don't show this notification - if (needsChatRoomInvitation && !chatRoomInvitation) { - return null; - } return await awaitAll({ id: notification.id, createdAt: new Date(notification.createdAt).toISOString(), type: notification.type, - userId: 'notifierId' in notification ? notification.notifierId : undefined, + userId: notification.notifierId, ...(userIfNeed != null ? { user: userIfNeed } : {}), ...(noteIfNeed != null ? { note: noteIfNeed } : {}), ...(notification.type === 'reaction' ? { reaction: notification.reaction, } : {}), - ...(notification.type === 'roleAssigned' ? { - role: role, - } : {}), - ...(notification.type === 'chatRoomInvitationReceived' ? { - invitation: chatRoomInvitation, - } : {}), - ...(notification.type === 'followRequestAccepted' ? { - message: notification.message, - } : {}), ...(notification.type === 'achievementEarned' ? { achievement: notification.achievement, } : {}), - ...(notification.type === 'exportCompleted' ? { - exportedEntity: notification.exportedEntity, - fileId: notification.fileId, - } : {}), ...(notification.type === 'app' ? { body: notification.customBody, - header: notification.customHeader, - icon: notification.customIcon, + header: notification.customHeader ?? token?.name, + icon: notification.customIcon ?? token?.iconUrl, } : {}), }); } - async #packManyInternal ( - notifications: T[], - meId: MiUser['id'], - ): Promise { + @bindThis + public async packMany( + notifications: Notification[], + meId: User['id'], + ) { if (notifications.length === 0) return []; let validNotifications = notifications; - validNotifications = await this.#filterValidNotifier(validNotifications, meId); - - const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(x => x != null); + const noteIds = validNotifications.map(x => x.noteId).filter(isNotNull); const notes = noteIds.length > 0 ? await this.notesRepository.find({ where: { id: In(noteIds) }, relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'], @@ -206,130 +123,29 @@ export class NotificationEntityService implements OnModuleInit { }); const packedNotes = new Map(packedNotesArray.map(p => [p.id, p])); - validNotifications = validNotifications.filter(x => !('noteId' in x) || packedNotes.has(x.noteId)); + validNotifications = validNotifications.filter(x => x.noteId == null || packedNotes.has(x.noteId)); - const userIds = []; - for (const notification of validNotifications) { - if ('notifierId' in notification) userIds.push(notification.notifierId); - if (notification.type === 'reaction:grouped') userIds.push(...notification.reactions.map(x => x.userId)); - if (notification.type === 'renote:grouped') userIds.push(...notification.userIds); - } + const userIds = validNotifications.map(x => x.notifierId).filter(isNotNull); const users = userIds.length > 0 ? await this.usersRepository.find({ where: { id: In(userIds) }, }) : []; - const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }); + const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, { + detail: false, + }); const packedUsers = new Map(packedUsersArray.map(p => [p.id, p])); // 既に解決されたフォローリクエストの通知を除外 - const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty => x.type === 'receiveFollowRequest'); + const followRequestNotifications = validNotifications.filter(x => x.type === 'receiveFollowRequest'); if (followRequestNotifications.length > 0) { const reqs = await this.followRequestsRepository.find({ - where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) }, + where: { followerId: In(followRequestNotifications.map(x => x.notifierId!)) }, }); validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId)); } - const packPromises = validNotifications.map(x => { - return this.pack( - x, - meId, - { checkValidNotifier: false }, - { packedNotes, packedUsers }, - ); - }); - - return (await Promise.all(packPromises)).filter(x => x != null); - } - - @bindThis - public async pack( - src: MiNotification | MiGroupedNotification, - meId: MiUser['id'], - - options: { - checkValidNotifier?: boolean; - }, - hint?: { - packedNotes: Map>; - packedUsers: Map>; - }, - ): Promise | null> { - return await this.#packInternal(src, meId, options, hint); - } - - @bindThis - public async packMany( - notifications: MiNotification[], - meId: MiUser['id'], - ): Promise { - return await this.#packManyInternal(notifications, meId); - } - - @bindThis - public async packGroupedMany( - notifications: MiGroupedNotification[], - meId: MiUser['id'], - ): Promise { - return await this.#packManyInternal(notifications, meId); - } - - /** - * notifierが存在するか、ミュートされていないか、サスペンドされていないかを確認するvalidator - */ - #validateNotifier ( - notification: T, - userIdsWhoMeMuting: Set, - userMutedInstances: Set, - notifiers: MiUser[], - ): boolean { - if (!('notifierId' in notification)) return true; - if (userIdsWhoMeMuting.has(notification.notifierId)) return false; - - const notifier = notifiers.find(x => x.id === notification.notifierId) ?? null; - - if (notifier == null) return false; - if (notifier.host && userMutedInstances.has(notifier.host)) return false; - - if (notifier.isSuspended) return false; - - return true; - } - - /** - * notifierが存在するか、ミュートされていないか、サスペンドされていないかを実際に確認する - */ - async #isValidNotifier( - notification: MiNotification | MiGroupedNotification, - meId: MiUser['id'], - ): Promise { - return (await this.#filterValidNotifier([notification], meId)).length === 1; - } - - /** - * notifierが存在するか、ミュートされていないか、サスペンドされていないかを実際に複数確認する - */ - async #filterValidNotifier ( - notifications: T[], - meId: MiUser['id'], - ): Promise { - const [ - userIdsWhoMeMuting, - userMutedInstances, - ] = await Promise.all([ - this.cacheService.userMutingsCache.fetch(meId), - this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)), - ]); - - const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(x => x != null); - const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({ - where: { id: In(notifierIds) }, - }) : []; - - const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => { - const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers); - return isValid ? notification : null; - }))) as [T | null] ).filter(x => x != null); - - return filteredNotifications; + return await Promise.all(validNotifications.map(x => this.pack(x, meId, {}, { + packedNotes, + packedUsers, + }))); } } diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts index 46bf51bb6d..d6da856637 100644 --- a/packages/backend/src/core/entities/PageEntityService.ts +++ b/packages/backend/src/core/entities/PageEntityService.ts @@ -1,19 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, PagesRepository, PageLikesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, PagesRepository, PageLikesRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiPage } from '@/models/Page.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { Page } from '@/models/entities/Page.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js'; @@ -31,22 +25,18 @@ export class PageEntityService { private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, - private idService: IdService, ) { } @bindThis public async pack( - src: MiPage['id'] | MiPage, - me?: { id: MiUser['id'] } | null | undefined, - hint?: { - packedUser?: Packed<'UserLite'> - }, + src: Page['id'] | Page, + me?: { id: User['id'] } | null | undefined, ): Promise> { const meId = me ? me.id : null; const page = typeof src === 'object' ? src : await this.pagesRepository.findOneByOrFail({ id: src }); - const attachedFiles: Promise[] = []; + const attachedFiles: Promise[] = []; const collectFile = (xs: any[]) => { for (const x of xs) { if (x.type === 'image') { @@ -90,10 +80,10 @@ export class PageEntityService { return await awaitAll({ id: page.id, - createdAt: this.idService.parse(page.id).date.toISOString(), + createdAt: page.createdAt.toISOString(), updatedAt: page.updatedAt.toISOString(), userId: page.userId, - user: hint?.packedUser ?? this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意 + user: this.userEntityService.pack(page.user ?? page.userId, me), // { detail: true } すると無限ループするので注意 content: page.content, variables: page.variables, title: page.title, @@ -105,21 +95,18 @@ export class PageEntityService { script: page.script, eyeCatchingImageId: page.eyeCatchingImageId, eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null, - attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(x => x != null)), + attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)), likedCount: page.likedCount, - isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined, + isLiked: meId ? await this.pageLikesRepository.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined, }); } @bindThis - public async packMany( - pages: MiPage[], - me?: { id: MiUser['id'] } | null | undefined, + public packMany( + pages: Page[], + me?: { id: User['id'] } | null | undefined, ) { - const _users = pages.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, me) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(pages.map(page => this.pack(page, me, { packedUser: _userMap.get(page.userId) }))); + return Promise.all(pages.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/PageLikeEntityService.ts b/packages/backend/src/core/entities/PageLikeEntityService.ts index cfccbcb660..3460c1e422 100644 --- a/packages/backend/src/core/entities/PageLikeEntityService.ts +++ b/packages/backend/src/core/entities/PageLikeEntityService.ts @@ -1,16 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { PageLikesRepository } from '@/models/_.js'; -import type { } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiPageLike } from '@/models/PageLike.js'; -import { bindThis } from '@/decorators.js'; +import type { PageLikesRepository } from '@/models/index.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { PageLike } from '@/models/entities/PageLike.js'; import { PageEntityService } from './PageEntityService.js'; +import { bindThis } from '@/decorators.js'; @Injectable() export class PageLikeEntityService { @@ -24,8 +19,8 @@ export class PageLikeEntityService { @bindThis public async pack( - src: MiPageLike['id'] | MiPageLike, - me?: { id: MiUser['id'] } | null | undefined, + src: PageLike['id'] | PageLike, + me?: { id: User['id'] } | null | undefined, ) { const like = typeof src === 'object' ? src : await this.pageLikesRepository.findOneByOrFail({ id: src }); @@ -38,7 +33,7 @@ export class PageLikeEntityService { @bindThis public packMany( likes: any[], - me: { id: MiUser['id'] }, + me: { id: User['id'] }, ) { return Promise.all(likes.map(x => this.pack(x, me))); } diff --git a/packages/backend/src/core/entities/RenoteMutingEntityService.ts b/packages/backend/src/core/entities/RenoteMutingEntityService.ts index e4e154109a..f8871e0495 100644 --- a/packages/backend/src/core/entities/RenoteMutingEntityService.ts +++ b/packages/backend/src/core/entities/RenoteMutingEntityService.ts @@ -1,18 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { RenoteMutingsRepository } from '@/models/_.js'; +import type { RenoteMutingsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiRenoteMuting } from '@/models/RenoteMuting.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { RenoteMuting } from '@/models/entities/RenoteMuting.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; import { UserEntityService } from './UserEntityService.js'; @Injectable() @@ -22,39 +16,32 @@ export class RenoteMutingEntityService { private renoteMutingsRepository: RenoteMutingsRepository, private userEntityService: UserEntityService, - private idService: IdService, ) { } @bindThis public async pack( - src: MiRenoteMuting['id'] | MiRenoteMuting, - me?: { id: MiUser['id'] } | null | undefined, - hints?: { - packedMutee?: Packed<'UserDetailedNotMe'> - }, + src: RenoteMuting['id'] | RenoteMuting, + me?: { id: User['id'] } | null | undefined, ): Promise> { const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src }); return await awaitAll({ id: muting.id, - createdAt: this.idService.parse(muting.id).date.toISOString(), + createdAt: muting.createdAt.toISOString(), muteeId: muting.muteeId, - mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, { - schema: 'UserDetailedNotMe', + mutee: this.userEntityService.pack(muting.muteeId, me, { + detail: true, }), }); } @bindThis - public async packMany( - mutings: MiRenoteMuting[], - me: { id: MiUser['id'] }, + public packMany( + mutings: any[], + me: { id: User['id'] }, ) { - const _users = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId); - const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailedNotMe' }) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) }))); + return Promise.all(mutings.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts deleted file mode 100644 index df042e75c1..0000000000 --- a/packages/backend/src/core/entities/ReversiGameEntityService.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { ReversiGamesRepository } from '@/models/_.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; -import type { MiReversiGame } from '@/models/ReversiGame.js'; -import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; -import { UserEntityService } from './UserEntityService.js'; - -@Injectable() -export class ReversiGameEntityService { - constructor( - @Inject(DI.reversiGamesRepository) - private reversiGamesRepository: ReversiGamesRepository, - - private userEntityService: UserEntityService, - private idService: IdService, - ) { - } - - @bindThis - public async packDetail( - src: MiReversiGame['id'] | MiReversiGame, - hint?: { - packedUser1?: Packed<'UserLite'>, - packedUser2?: Packed<'UserLite'>, - }, - ): Promise> { - const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); - - const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id); - const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id); - - return await awaitAll({ - id: game.id, - createdAt: this.idService.parse(game.id).date.toISOString(), - startedAt: game.startedAt && game.startedAt.toISOString(), - endedAt: game.endedAt && game.endedAt.toISOString(), - isStarted: game.isStarted, - isEnded: game.isEnded, - form1: game.form1, - form2: game.form2, - user1Ready: game.user1Ready, - user2Ready: game.user2Ready, - user1Id: game.user1Id, - user2Id: game.user2Id, - user1, - user2, - winnerId: game.winnerId, - winner: game.winnerId ? [user1, user2].find(u => u.id === game.winnerId)! : null, - surrenderedUserId: game.surrenderedUserId, - timeoutUserId: game.timeoutUserId, - black: game.black, - bw: game.bw, - isLlotheo: game.isLlotheo, - canPutEverywhere: game.canPutEverywhere, - loopedBoard: game.loopedBoard, - timeLimitForEachTurn: game.timeLimitForEachTurn, - noIrregularRules: game.noIrregularRules, - logs: game.logs, - map: game.map, - }); - } - - @bindThis - public async packDetailMany( - games: MiReversiGame[], - ) { - const _user1s = games.map(({ user1, user1Id }) => user1 ?? user1Id); - const _user2s = games.map(({ user2, user2Id }) => user2 ?? user2Id); - const _userMap = await this.userEntityService.packMany([..._user1s, ..._user2s]) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all( - games.map(game => { - return this.packDetail(game, { - packedUser1: _userMap.get(game.user1Id), - packedUser2: _userMap.get(game.user2Id), - }); - }), - ); - } - - @bindThis - public async packLite( - src: MiReversiGame['id'] | MiReversiGame, - hint?: { - packedUser1?: Packed<'UserLite'>, - packedUser2?: Packed<'UserLite'>, - }, - ): Promise> { - const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); - - const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id); - const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id); - - return await awaitAll({ - id: game.id, - createdAt: this.idService.parse(game.id).date.toISOString(), - startedAt: game.startedAt && game.startedAt.toISOString(), - endedAt: game.endedAt && game.endedAt.toISOString(), - isStarted: game.isStarted, - isEnded: game.isEnded, - user1Id: game.user1Id, - user2Id: game.user2Id, - user1, - user2, - winnerId: game.winnerId, - winner: game.winnerId ? [user1, user2].find(u => u.id === game.winnerId)! : null, - surrenderedUserId: game.surrenderedUserId, - timeoutUserId: game.timeoutUserId, - black: game.black, - bw: game.bw, - isLlotheo: game.isLlotheo, - canPutEverywhere: game.canPutEverywhere, - loopedBoard: game.loopedBoard, - timeLimitForEachTurn: game.timeLimitForEachTurn, - noIrregularRules: game.noIrregularRules, - }); - } - - @bindThis - public async packLiteMany( - games: MiReversiGame[], - ) { - const _user1s = games.map(({ user1, user1Id }) => user1 ?? user1Id); - const _user2s = games.map(({ user2, user2Id }) => user2 ?? user2Id); - const _userMap = await this.userEntityService.packMany([..._user1s, ..._user2s]) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all( - games.map(game => { - return this.packLite(game, { - packedUser1: _userMap.get(game.user1Id), - packedUser2: _userMap.get(game.user2Id), - }); - }), - ); - } -} - diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts index 3fa38c9521..54818782dd 100644 --- a/packages/backend/src/core/entities/RoleEntityService.ts +++ b/packages/backend/src/core/entities/RoleEntityService.ts @@ -1,19 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { RoleAssignmentsRepository, RolesRepository } from '@/models/_.js'; +import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiRole } from '@/models/Role.js'; +import type { User } from '@/models/entities/User.js'; +import type { Role } from '@/models/entities/Role.js'; import { bindThis } from '@/decorators.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; -import { IdService } from '@/core/IdService.js'; -import { Packed } from '@/misc/json-schema.js'; +import { UserEntityService } from './UserEntityService.js'; @Injectable() export class RoleEntityService { @@ -24,23 +18,22 @@ export class RoleEntityService { @Inject(DI.roleAssignmentsRepository) private roleAssignmentsRepository: RoleAssignmentsRepository, - private idService: IdService, + private userEntityService: UserEntityService, ) { } @bindThis public async pack( - src: MiRole['id'] | MiRole, - me?: { id: MiUser['id'] } | null | undefined, - ): Promise> { + src: Role['id'] | Role, + me?: { id: User['id'] } | null | undefined, + ) { const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src }); const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign') .where('assign.roleId = :roleId', { roleId: role.id }) - .andWhere(new Brackets(qb => { - qb - .where('assign.expiresAt IS NULL') - .orWhere('assign.expiresAt > :now', { now: new Date() }); + .andWhere(new Brackets(qb => { qb + .where('assign.expiresAt IS NULL') + .orWhere('assign.expiresAt > :now', { now: new Date() }); })) .getCount(); @@ -55,7 +48,7 @@ export class RoleEntityService { return await awaitAll({ id: role.id, - createdAt: this.idService.parse(role.id).date.toISOString(), + createdAt: role.createdAt.toISOString(), updatedAt: role.updatedAt.toISOString(), name: role.name, description: role.description, @@ -68,7 +61,6 @@ export class RoleEntityService { isModerator: role.isModerator, isExplorable: role.isExplorable, asBadge: role.asBadge, - preserveAssignmentOnMoveAccount: role.preserveAssignmentOnMoveAccount, canEditMembersByModerator: role.canEditMembersByModerator, displayOrder: role.displayOrder, policies: policies, @@ -79,7 +71,7 @@ export class RoleEntityService { @bindThis public packMany( roles: any[], - me: { id: MiUser['id'] }, + me: { id: User['id'] }, ) { return Promise.all(roles.map(x => this.pack(x, me))); } diff --git a/packages/backend/src/core/entities/SigninEntityService.ts b/packages/backend/src/core/entities/SigninEntityService.ts index 00b124d594..51fa7543d9 100644 --- a/packages/backend/src/core/entities/SigninEntityService.ts +++ b/packages/backend/src/core/entities/SigninEntityService.ts @@ -1,32 +1,26 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import type { } from '@/models/Blocking.js'; -import type { MiSignin } from '@/models/Signin.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { SigninsRepository } from '@/models/index.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { Signin } from '@/models/entities/Signin.js'; +import { UserEntityService } from './UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; @Injectable() export class SigninEntityService { constructor( - private idService: IdService, + @Inject(DI.signinsRepository) + private signinsRepository: SigninsRepository, + + private userEntityService: UserEntityService, ) { } @bindThis public async pack( - src: MiSignin, + src: Signin, ) { - return { - id: src.id, - createdAt: this.idService.parse(src.id).date.toISOString(), - ip: src.ip, - headers: src.headers, - success: src.success, - }; + return src; } } diff --git a/packages/backend/src/core/entities/SystemWebhookEntityService.ts b/packages/backend/src/core/entities/SystemWebhookEntityService.ts deleted file mode 100644 index e18734091c..0000000000 --- a/packages/backend/src/core/entities/SystemWebhookEntityService.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import type { MiSystemWebhook, SystemWebhooksRepository } from '@/models/_.js'; -import { bindThis } from '@/decorators.js'; -import { Packed } from '@/misc/json-schema.js'; - -@Injectable() -export class SystemWebhookEntityService { - constructor( - @Inject(DI.systemWebhooksRepository) - private systemWebhooksRepository: SystemWebhooksRepository, - ) { - } - - @bindThis - public async pack( - src: MiSystemWebhook['id'] | MiSystemWebhook, - opts?: { - webhooks: Map - }, - ): Promise> { - const webhook = typeof src === 'object' - ? src - : opts?.webhooks.get(src) ?? await this.systemWebhooksRepository.findOneByOrFail({ id: src }); - - return { - id: webhook.id, - isActive: webhook.isActive, - updatedAt: webhook.updatedAt.toISOString(), - latestSentAt: webhook.latestSentAt?.toISOString() ?? null, - latestStatus: webhook.latestStatus, - name: webhook.name, - on: webhook.on, - url: webhook.url, - secret: webhook.secret, - }; - } - - @bindThis - public async packMany(src: MiSystemWebhook['id'][] | MiSystemWebhook[]): Promise[]> { - if (src.length === 0) { - return []; - } - - const webhooks = Array.of(); - webhooks.push( - ...src.filter((it): it is MiSystemWebhook => typeof it === 'object'), - ); - - const ids = src.filter((it): it is MiSystemWebhook['id'] => typeof it === 'string'); - if (ids.length > 0) { - webhooks.push( - ...await this.systemWebhooksRepository.findBy({ id: In(ids) }), - ); - } - - return Promise - .all( - webhooks.map(x => - this.pack(x, { - webhooks: new Map(webhooks.map(x => [x.id, x])), - }), - ), - ) - .then(it => it.sort((a, b) => a.id.localeCompare(b.id))); - } -} - diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index d4769d24d4..f1a4e56c02 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -1,99 +1,61 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; +import { In, Not } from 'typeorm'; import * as Redis from 'ioredis'; import _Ajv from 'ajv'; import { ModuleRef } from '@nestjs/core'; -import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { Packed } from '@/misc/json-schema.js'; import type { Promiseable } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; -import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import { - birthdaySchema, - descriptionSchema, - localUsernameSchema, - locationSchema, - nameSchema, - passwordSchema, -} from '@/models/User.js'; -import type { - BlockingsRepository, - FollowingsRepository, - FollowRequestsRepository, - MiFollowing, - MiMeta, - MiUserNotePining, - MiUserProfile, - MutingsRepository, - RenoteMutingsRepository, - UserMemoRepository, - UserNotePiningsRepository, - UserProfilesRepository, - UserSecurityKeysRepository, - UsersRepository, -} from '@/models/_.js'; +import type { LocalUser, PartialLocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js'; +import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; +import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; -import { IdService } from '@/core/IdService.js'; -import type { AnnouncementService } from '@/core/AnnouncementService.js'; -import type { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; -import { ChatService } from '@/core/ChatService.js'; import type { OnModuleInit } from '@nestjs/common'; +import type { AntennaService } from '../AntennaService.js'; +import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { NoteEntityService } from './NoteEntityService.js'; +import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { PageEntityService } from './PageEntityService.js'; +type IsUserDetailed = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>; +type IsMeAndIsUserDetailed = + Detailed extends true ? + ExpectsMe extends true ? Packed<'MeDetailed'> : + ExpectsMe extends false ? Packed<'UserDetailedNotMe'> : + Packed<'UserDetailed'> : + Packed<'UserLite'>; + const Ajv = _Ajv.default; const ajv = new Ajv(); -function isLocalUser(user: MiUser): user is MiLocalUser; -function isLocalUser(user: T): user is (T & { host: null; }); - -function isLocalUser(user: MiUser | { host: MiUser['host'] }): boolean { +function isLocalUser(user: User): user is LocalUser; +function isLocalUser(user: T): user is (T & { host: null; }); +function isLocalUser(user: User | { host: User['host'] }): boolean { return user.host == null; } -function isRemoteUser(user: MiUser): user is MiRemoteUser; -function isRemoteUser(user: T): user is (T & { host: string; }); - -function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean { +function isRemoteUser(user: User): user is RemoteUser; +function isRemoteUser(user: T): user is (T & { host: string; }); +function isRemoteUser(user: User | { host: User['host'] }): boolean { return !isLocalUser(user); } -export type UserRelation = { - id: MiUser['id'] - following: MiFollowing | null, - isFollowing: boolean - isFollowed: boolean - hasPendingFollowRequestFromYou: boolean - hasPendingFollowRequestToYou: boolean - isBlocking: boolean - isBlocked: boolean - isMuted: boolean - isRenoteMuted: boolean -}; - @Injectable() export class UserEntityService implements OnModuleInit { private apPersonService: ApPersonService; private noteEntityService: NoteEntityService; + private driveFileEntityService: DriveFileEntityService; private pageEntityService: PageEntityService; private customEmojiService: CustomEmojiService; - private announcementService: AnnouncementService; + private antennaService: AntennaService; private roleService: RoleService; private federatedInstanceService: FederatedInstanceService; - private idService: IdService; - private avatarDecorationService: AvatarDecorationService; - private chatService: ChatService; constructor( private moduleRef: ModuleRef, @@ -101,9 +63,6 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.redis) private redisClient: Redis.Redis, @@ -128,28 +87,54 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.noteUnreadsRepository) + private noteUnreadsRepository: NoteUnreadsRepository, + + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + @Inject(DI.userNotePiningsRepository) private userNotePiningsRepository: UserNotePiningsRepository, @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + @Inject(DI.announcementReadsRepository) + private announcementReadsRepository: AnnouncementReadsRepository, + + @Inject(DI.announcementsRepository) + private announcementsRepository: AnnouncementsRepository, + + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, + @Inject(DI.userMemosRepository) private userMemosRepository: UserMemoRepository, + + //private noteEntityService: NoteEntityService, + //private driveFileEntityService: DriveFileEntityService, + //private pageEntityService: PageEntityService, + //private customEmojiService: CustomEmojiService, + //private antennaService: AntennaService, + //private roleService: RoleService, ) { } onModuleInit() { this.apPersonService = this.moduleRef.get('ApPersonService'); this.noteEntityService = this.moduleRef.get('NoteEntityService'); + this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.pageEntityService = this.moduleRef.get('PageEntityService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService'); - this.announcementService = this.moduleRef.get('AnnouncementService'); + this.antennaService = this.moduleRef.get('AntennaService'); this.roleService = this.moduleRef.get('RoleService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); - this.idService = this.moduleRef.get('IdService'); - this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService'); - this.chatService = this.moduleRef.get('ChatService'); } //#region Validators @@ -165,203 +150,112 @@ export class UserEntityService implements OnModuleInit { public isRemoteUser = isRemoteUser; @bindThis - public async getRelation(me: MiUser['id'], target: MiUser['id']): Promise { - const [ - following, - isFollowed, - hasPendingFollowRequestFromYou, - hasPendingFollowRequestToYou, - isBlocking, - isBlocked, - isMuted, - isRenoteMuted, - ] = await Promise.all([ - this.followingsRepository.findOneBy({ - followerId: me, - followeeId: target, - }), - this.followingsRepository.exists({ - where: { - followerId: target, - followeeId: me, - }, - }), - this.followRequestsRepository.exists({ + public async getRelation(me: User['id'], target: User['id']) { + return awaitAll({ + id: target, + isFollowing: this.followingsRepository.count({ where: { followerId: me, followeeId: target, }, - }), - this.followRequestsRepository.exists({ + take: 1, + }).then(n => n > 0), + isFollowed: this.followingsRepository.count({ where: { followerId: target, followeeId: me, }, - }), - this.blockingsRepository.exists({ + take: 1, + }).then(n => n > 0), + hasPendingFollowRequestFromYou: this.followRequestsRepository.count({ + where: { + followerId: me, + followeeId: target, + }, + take: 1, + }).then(n => n > 0), + hasPendingFollowRequestToYou: this.followRequestsRepository.count({ + where: { + followerId: target, + followeeId: me, + }, + take: 1, + }).then(n => n > 0), + isBlocking: this.blockingsRepository.count({ where: { blockerId: me, blockeeId: target, }, - }), - this.blockingsRepository.exists({ + take: 1, + }).then(n => n > 0), + isBlocked: this.blockingsRepository.count({ where: { blockerId: target, blockeeId: me, }, - }), - this.mutingsRepository.exists({ + take: 1, + }).then(n => n > 0), + isMuted: this.mutingsRepository.count({ where: { muterId: me, muteeId: target, }, - }), - this.renoteMutingsRepository.exists({ + take: 1, + }).then(n => n > 0), + isRenoteMuted: this.renoteMutingsRepository.count({ where: { muterId: me, muteeId: target, }, - }), - ]); - - return { - id: target, - following, - isFollowing: following != null, - isFollowed, - hasPendingFollowRequestFromYou, - hasPendingFollowRequestToYou, - isBlocking, - isBlocked, - isMuted, - isRenoteMuted, - }; + take: 1, + }).then(n => n > 0), + }); } @bindThis - public async getRelations(me: MiUser['id'], targets: MiUser['id'][]): Promise> { - const [ - followers, - followees, - followersRequests, - followeesRequests, - blockers, - blockees, - muters, - renoteMuters, - ] = await Promise.all([ - this.followingsRepository.findBy({ followerId: me }) - .then(f => new Map(f.map(it => [it.followeeId, it]))), - this.followingsRepository.createQueryBuilder('f') - .select('f.followerId') - .where('f.followeeId = :me', { me }) - .getRawMany<{ f_followerId: string }>() - .then(it => it.map(it => it.f_followerId)), - this.followRequestsRepository.createQueryBuilder('f') - .select('f.followeeId') - .where('f.followerId = :me', { me }) - .getRawMany<{ f_followeeId: string }>() - .then(it => it.map(it => it.f_followeeId)), - this.followRequestsRepository.createQueryBuilder('f') - .select('f.followerId') - .where('f.followeeId = :me', { me }) - .getRawMany<{ f_followerId: string }>() - .then(it => it.map(it => it.f_followerId)), - this.blockingsRepository.createQueryBuilder('b') - .select('b.blockeeId') - .where('b.blockerId = :me', { me }) - .getRawMany<{ b_blockeeId: string }>() - .then(it => it.map(it => it.b_blockeeId)), - this.blockingsRepository.createQueryBuilder('b') - .select('b.blockerId') - .where('b.blockeeId = :me', { me }) - .getRawMany<{ b_blockerId: string }>() - .then(it => it.map(it => it.b_blockerId)), - this.mutingsRepository.createQueryBuilder('m') - .select('m.muteeId') - .where('m.muterId = :me', { me }) - .getRawMany<{ m_muteeId: string }>() - .then(it => it.map(it => it.m_muteeId)), - this.renoteMutingsRepository.createQueryBuilder('m') - .select('m.muteeId') - .where('m.muterId = :me', { me }) - .getRawMany<{ m_muteeId: string }>() - .then(it => it.map(it => it.m_muteeId)), - ]); + public async getHasUnreadAnnouncement(userId: User['id']): Promise { + const reads = await this.announcementReadsRepository.findBy({ + userId: userId, + }); - return new Map( - targets.map(target => { - const following = followers.get(target) ?? null; + const count = await this.announcementsRepository.countBy(reads.length > 0 ? { + id: Not(In(reads.map(read => read.announcementId))), + } : {}); - return [ - target, - { - id: target, - following: following, - isFollowing: following != null, - isFollowed: followees.includes(target), - hasPendingFollowRequestFromYou: followersRequests.includes(target), - hasPendingFollowRequestToYou: followeesRequests.includes(target), - isBlocking: blockers.includes(target), - isBlocked: blockees.includes(target), - isMuted: muters.includes(target), - isRenoteMuted: renoteMuters.includes(target), - }, - ]; - }), - ); + return count > 0; } @bindThis - public async getHasUnreadAntenna(userId: MiUser['id']): Promise { + public async getHasUnreadAntenna(userId: User['id']): Promise { /* const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId); - const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exists({ - where: { - antennaId: In(myAntennas.map(x => x.id)), - read: false, - }, - }) : false); + const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({ + antennaId: In(myAntennas.map(x => x.id)), + read: false, + }) : null; - return isUnread; + return unread != null; */ return false; // TODO } @bindThis - public async getNotificationsInfo(userId: MiUser['id']): Promise<{ - hasUnread: boolean; - unreadCount: number; - }> { - const response = { - hasUnread: false, - unreadCount: 0, - }; - + public async getHasUnreadNotification(userId: User['id']): Promise { const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`); - if (!latestReadNotificationId) { - response.unreadCount = await this.redisClient.xlen(`notificationTimeline:${userId}`); - } else { - const latestNotificationIdsRes = await this.redisClient.xrevrange( - `notificationTimeline:${userId}`, - '+', - latestReadNotificationId, - ); + const latestNotificationIdsRes = await this.redisClient.xrevrange( + `notificationTimeline:${userId}`, + '+', + '-', + 'COUNT', 1); + const latestNotificationId = latestNotificationIdsRes[0]?.[0]; - response.unreadCount = (latestNotificationIdsRes.length - 1 >= 0) ? latestNotificationIdsRes.length - 1 : 0; - } - - if (response.unreadCount > 0) { - response.hasUnread = true; - } - - return response; + return latestNotificationId != null && (latestReadNotificationId == null || latestReadNotificationId < latestNotificationId); } @bindThis - public async getHasPendingReceivedFollowRequest(userId: MiUser['id']): Promise { + public async getHasPendingReceivedFollowRequest(userId: User['id']): Promise { const count = await this.followRequestsRepository.countBy({ followeeId: userId, }); @@ -370,7 +264,7 @@ export class UserEntityService implements OnModuleInit { } @bindThis - public getOnlineStatus(user: MiUser): 'unknown' | 'online' | 'active' | 'offline' { + public getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' { if (user.hideOnlineStatus) return 'unknown'; if (user.lastActiveDate == null) return 'unknown'; const elapsed = Date.now() - user.lastActiveDate.getTime(); @@ -382,16 +276,12 @@ export class UserEntityService implements OnModuleInit { } @bindThis - public getIdenticonUrl(user: MiUser): string { - if ((user.host == null || user.host === this.config.host) && user.username.includes('.') && this.meta.iconUrl) { // ローカルのシステムアカウントの場合 - return this.meta.iconUrl; - } else { - return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.host}`; - } + public getIdenticonUrl(user: User): string { + return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.host}`; } @bindThis - public getUserUri(user: MiLocalUser | MiPartialLocalUser | MiRemoteUser | MiPartialRemoteUser): string { + public getUserUri(user: LocalUser | PartialLocalUser | RemoteUser | PartialRemoteUser): string { return this.isRemoteUser(user) ? user.uri : this.genLocalUserUri(user.id); } @@ -401,106 +291,76 @@ export class UserEntityService implements OnModuleInit { return `${this.config.url}/users/${userId}`; } - public async pack( - src: MiUser['id'] | MiUser, - me?: { id: MiUser['id']; } | null | undefined, + public async pack( + src: User['id'] | User, + me?: { id: User['id']; } | null | undefined, options?: { - schema?: S, + detail?: D, includeSecrets?: boolean, - userProfile?: MiUserProfile, - userRelations?: Map, - userMemos?: Map, - pinNotes?: Map, + userProfile?: UserProfile, }, - ): Promise> { + ): Promise> { const opts = Object.assign({ - schema: 'UserLite', + detail: false, includeSecrets: false, }, options); const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src }); - const isDetailed = opts.schema !== 'UserLite'; + // migration + if (user.avatarId != null && user.avatarUrl === null) { + const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId }); + user.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar'); + this.usersRepository.update(user.id, { + avatarUrl: user.avatarUrl, + avatarBlurhash: avatar.blurhash, + }); + } + if (user.bannerId != null && user.bannerUrl === null) { + const banner = await this.driveFilesRepository.findOneByOrFail({ id: user.bannerId }); + user.bannerUrl = this.driveFileEntityService.getPublicUrl(banner); + this.usersRepository.update(user.id, { + bannerUrl: user.bannerUrl, + bannerBlurhash: banner.blurhash, + }); + } + const meId = me ? me.id : null; const isMe = meId === user.id; - const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; + const iAmModerator = me ? await this.roleService.isModerator(me as User) : false; - const profile = isDetailed - ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) - : null; - - let relation: UserRelation | null = null; - if (meId && !isMe && isDetailed) { - if (opts.userRelations) { - relation = opts.userRelations.get(user.id) ?? null; - } else { - relation = await this.getRelation(meId, user.id); - } - } - - let memo: string | null = null; - if (isDetailed && meId) { - if (opts.userMemos) { - memo = opts.userMemos.get(user.id) ?? null; - } else { - memo = await this.userMemosRepository.findOneBy({ userId: meId, targetUserId: user.id }) - .then(row => row?.memo ?? null); - } - } - - let pins: MiUserNotePining[] = []; - if (isDetailed) { - if (opts.pinNotes) { - pins = opts.pinNotes.get(user.id) ?? []; - } else { - pins = await this.userNotePiningsRepository.createQueryBuilder('pin') - .where('pin.userId = :userId', { userId: user.id }) - .innerJoinAndSelect('pin.note', 'note') - .orderBy('pin.id', 'DESC') - .getMany(); - } - } + const relation = meId && !isMe && opts.detail ? await this.getRelation(meId, user.id) : null; + const pins = opts.detail ? await this.userNotePiningsRepository.createQueryBuilder('pin') + .where('pin.userId = :userId', { userId: user.id }) + .innerJoinAndSelect('pin.note', 'note') + .orderBy('pin.id', 'DESC') + .getMany() : []; + const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null; const followingCount = profile == null ? null : - (profile.followingVisibility === 'public') || isMe || iAmModerator ? user.followingCount : - (profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount : + (profile.ffVisibility === 'public') || isMe ? user.followingCount : + (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount : null; const followersCount = profile == null ? null : - (profile.followersVisibility === 'public') || isMe || iAmModerator ? user.followersCount : - (profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : + (profile.ffVisibility === 'public') || isMe ? user.followersCount : + (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : null; - const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : null; - const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : null; - const unreadAnnouncements = isMe && isDetailed ? - (await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({ - createdAt: this.idService.parse(announcement.id).date.toISOString(), - ...announcement, - })) : null; + const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null; + const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null; - const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null; + const falsy = opts.detail ? false : undefined; const packed = { id: user.id, name: user.name, username: user.username, host: user.host, - avatarUrl: (user.avatarId == null ? null : user.avatarUrl) ?? this.getIdenticonUrl(user), - avatarBlurhash: (user.avatarId == null ? null : user.avatarBlurhash), - avatarDecorations: user.avatarDecorations.length > 0 ? this.avatarDecorationService.getAll().then(decorations => user.avatarDecorations.filter(ud => decorations.some(d => d.id === ud.id)).map(ud => ({ - id: ud.id, - angle: ud.angle || undefined, - flipH: ud.flipH || undefined, - offsetX: ud.offsetX || undefined, - offsetY: ud.offsetY || undefined, - url: decorations.find(d => d.id === ud.id)!.url, - }))) : [], - isBot: user.isBot, - isCat: user.isCat, - requireSigninToViewContents: user.requireSigninToViewContents === false ? undefined : true, - makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore ?? undefined, - makeNotesHiddenBefore: user.makeNotesHiddenBefore ?? undefined, + avatarUrl: user.avatarUrl ?? this.getIdenticonUrl(user), + avatarBlurhash: user.avatarBlurhash, + isBot: user.isBot ?? falsy, + isCat: user.isCat ?? falsy, instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? { name: instance.name, softwareName: instance.softwareName, @@ -512,38 +372,33 @@ export class UserEntityService implements OnModuleInit { emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), onlineStatus: this.getOnlineStatus(user), // パフォーマンス上の理由でローカルユーザーのみ - badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs - .filter((r) => r.isPublic || iAmModerator) - .sort((a, b) => b.displayOrder - a.displayOrder) - .map((r) => ({ - name: r.name, - iconUrl: r.iconUrl, - displayOrder: r.displayOrder, - })), - ) : undefined, + badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.sort((a, b) => b.displayOrder - a.displayOrder).map(r => ({ + name: r.name, + iconUrl: r.iconUrl, + displayOrder: r.displayOrder, + }))) : undefined, - ...(isDetailed ? { + ...(opts.detail ? { url: profile!.url, uri: user.uri, movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null, alsoKnownAs: user.alsoKnownAs ? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null))) - .then(xs => xs.length === 0 ? null : xs.filter(x => x != null)) + .then(xs => xs.length === 0 ? null : xs.filter(x => x != null) as string[]) : null, - createdAt: this.idService.parse(user.id).date.toISOString(), + createdAt: user.createdAt.toISOString(), updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null, - bannerUrl: user.bannerId == null ? null : user.bannerUrl, - bannerBlurhash: user.bannerId == null ? null : user.bannerBlurhash, + bannerUrl: user.bannerUrl, + bannerBlurhash: user.bannerBlurhash, isLocked: user.isLocked, isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote), - isSuspended: user.isSuspended, + isSuspended: user.isSuspended ?? falsy, description: profile!.description, location: profile!.location, birthday: profile!.birthday, lang: profile!.lang, fields: profile!.fields, - verifiedLinks: profile!.verifiedLinks, followersCount: followersCount ?? 0, followingCount: followingCount ?? 0, notesCount: user.notesCount, @@ -553,11 +408,15 @@ export class UserEntityService implements OnModuleInit { }), pinnedPageId: profile!.pinnedPageId, pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null, - publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964 - followersVisibility: profile!.followersVisibility, - followingVisibility: profile!.followingVisibility, - chatScope: user.chatScope, - canChat: this.roleService.getUserPolicies(user.id).then(r => r.chatAvailability === 'available'), + publicReactions: profile!.publicReactions, + ffVisibility: profile!.ffVisibility, + twoFactorEnabled: profile!.twoFactorEnabled, + usePasswordLessLogin: profile!.usePasswordLessLogin, + securityKeys: profile!.twoFactorEnabled + ? this.userSecurityKeysRepository.countBy({ + userId: user.id, + }).then(result => result >= 1) + : false, roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({ id: role.id, name: role.name, @@ -568,22 +427,16 @@ export class UserEntityService implements OnModuleInit { isAdministrator: role.isAdministrator, displayOrder: role.displayOrder, }))), - memo: memo, + memo: meId == null ? null : await this.userMemosRepository.findOneBy({ + userId: meId, + targetUserId: user.id, + }).then(row => row?.memo ?? null), moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined, } : {}), - ...(isDetailed && (isMe || iAmModerator) ? { - twoFactorEnabled: profile!.twoFactorEnabled, - usePasswordLessLogin: profile!.usePasswordLessLogin, - securityKeys: profile!.twoFactorEnabled - ? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1) - : false, - } : {}), - - ...(isDetailed && isMe ? { + ...(opts.detail && isMe ? { avatarId: user.avatarId, bannerId: user.bannerId, - followedMessage: profile!.followedMessage, isModerator: isModerator, isAdmin: isAdmin, injectFeaturedNote: profile!.injectFeaturedNote, @@ -596,23 +449,23 @@ export class UserEntityService implements OnModuleInit { preventAiLearning: profile!.preventAiLearning, isExplorable: user.isExplorable, isDeleted: user.isDeleted, - twoFactorBackupCodesStock: profile?.twoFactorBackupSecret?.length === 5 ? 'full' : (profile?.twoFactorBackupSecret?.length ?? 0) > 0 ? 'partial' : 'none', hideOnlineStatus: user.hideOnlineStatus, - hasUnreadSpecifiedNotes: false, // 後方互換性のため - hasUnreadMentions: false, // 後方互換性のため - hasUnreadChatMessages: this.chatService.hasUnreadMessages(user.id), - hasUnreadAnnouncement: unreadAnnouncements!.length > 0, - unreadAnnouncements, + hasUnreadSpecifiedNotes: this.noteUnreadsRepository.count({ + where: { userId: user.id, isSpecified: true }, + take: 1, + }).then(count => count > 0), + hasUnreadMentions: this.noteUnreadsRepository.count({ + where: { userId: user.id, isMentioned: true }, + take: 1, + }).then(count => count > 0), + hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id), hasUnreadAntenna: this.getHasUnreadAntenna(user.id), hasUnreadChannel: false, // 後方互換性のため - hasUnreadNotification: notificationsInfo?.hasUnread, // 後方互換性のため + hasUnreadNotification: this.getHasUnreadNotification(user.id), hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), - unreadNotificationsCount: notificationsInfo?.unreadCount, mutedWords: profile!.mutedWords, - hardMutedWords: profile!.hardMutedWords, mutedInstances: profile!.mutedInstances, - mutingNotificationTypes: [], // 後方互換性のため - notificationRecieveConfig: profile!.notificationRecieveConfig, + mutingNotificationTypes: profile!.mutingNotificationTypes, emailNotificationTypes: profile!.emailNotificationTypes, achievements: profile!.achievements, loggedInDays: profile!.loggedInDates.length, @@ -645,86 +498,20 @@ export class UserEntityService implements OnModuleInit { isBlocked: relation.isBlocked, isMuted: relation.isMuted, isRenoteMuted: relation.isRenoteMuted, - notify: relation.following?.notify ?? 'none', - withReplies: relation.following?.withReplies ?? false, - followedMessage: relation.isFollowing ? profile!.followedMessage : undefined, } : {}), - } as Promiseable>; + } as Promiseable> as Promiseable>; return await awaitAll(packed); } - public async packMany( - users: (MiUser['id'] | MiUser)[], - me?: { id: MiUser['id'] } | null | undefined, + public packMany( + users: (User['id'] | User)[], + me?: { id: User['id'] } | null | undefined, options?: { - schema?: S, + detail?: D, includeSecrets?: boolean, }, - ): Promise[]> { - // -- IDのみの要素を補完して完全なエンティティ一覧を作る - - const _users = users.filter((user): user is MiUser => typeof user !== 'string'); - if (_users.length !== users.length) { - _users.push( - ...await this.usersRepository.findBy({ - id: In(users.filter((user): user is string => typeof user === 'string')), - }), - ); - } - const _userIds = _users.map(u => u.id); - - // -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得 - - let profilesMap: Map = new Map(); - let userRelations: Map = new Map(); - let userMemos: Map = new Map(); - let pinNotes: Map = new Map(); - - if (options?.schema !== 'UserLite') { - profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) }) - .then(profiles => new Map(profiles.map(p => [p.userId, p]))); - - const meId = me ? me.id : null; - if (meId) { - userMemos = await this.userMemosRepository.findBy({ userId: meId }) - .then(memos => new Map(memos.map(memo => [memo.targetUserId, memo.memo]))); - - if (_userIds.length > 0) { - userRelations = await this.getRelations(meId, _userIds); - pinNotes = await this.userNotePiningsRepository.createQueryBuilder('pin') - .where('pin.userId IN (:...userIds)', { userIds: _userIds }) - .innerJoinAndSelect('pin.note', 'note') - .getMany() - .then(pinsNotes => { - const map = new Map(); - for (const note of pinsNotes) { - const notes = map.get(note.userId) ?? []; - notes.push(note); - map.set(note.userId, notes); - } - for (const [, notes] of map.entries()) { - // pack側ではDESCで取得しているので、それに合わせて降順に並び替えておく - notes.sort((a, b) => b.id.localeCompare(a.id)); - } - return map; - }); - } - } - } - - return Promise.all( - _users.map(u => this.pack( - u, - me, - { - ...options, - userProfile: profilesMap.get(u.id), - userRelations: userRelations, - userMemos: userMemos, - pinNotes: pinNotes, - }, - )), - ); + ): Promise[]> { + return Promise.all(users.map(u => this.pack(u, me, options))); } } diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts index b77249c5cb..8628819278 100644 --- a/packages/backend/src/core/entities/UserListEntityService.ts +++ b/packages/backend/src/core/entities/UserListEntityService.ts @@ -1,16 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; +import type { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; -import type { MiUserList } from '@/models/UserList.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { UserList } from '@/models/entities/UserList.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; import { UserEntityService } from './UserEntityService.js'; @Injectable() @@ -19,47 +13,30 @@ export class UserListEntityService { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, private userEntityService: UserEntityService, - private idService: IdService, ) { } @bindThis public async pack( - src: MiUserList['id'] | MiUserList, + src: UserList['id'] | UserList, ): Promise> { const userList = typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src }); - const users = await this.userListMembershipsRepository.findBy({ + const users = await this.userListJoiningsRepository.findBy({ userListId: userList.id, }); return { id: userList.id, - createdAt: this.idService.parse(userList.id).date.toISOString(), + createdAt: userList.createdAt.toISOString(), name: userList.name, userIds: users.map(x => x.userId), isPublic: userList.isPublic, }; } - - @bindThis - public async packMembershipsMany( - memberships: MiUserListMembership[], - ) { - const _users = memberships.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(memberships.map(async x => ({ - id: x.id, - createdAt: this.idService.parse(x.id).date.toISOString(), - userId: x.userId, - user: _userMap.get(x.userId) ?? await this.userEntityService.pack(x.userId), - withReplies: x.withReplies, - }))); - } } diff --git a/packages/backend/src/daemons/DaemonModule.ts b/packages/backend/src/daemons/DaemonModule.ts index a67907e6dd..683f9cbfe3 100644 --- a/packages/backend/src/daemons/DaemonModule.ts +++ b/packages/backend/src/daemons/DaemonModule.ts @@ -1,11 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; import { GlobalModule } from '@/GlobalModule.js'; +import { JanitorService } from './JanitorService.js'; import { QueueStatsService } from './QueueStatsService.js'; import { ServerStatsService } from './ServerStatsService.js'; @@ -15,10 +11,12 @@ import { ServerStatsService } from './ServerStatsService.js'; CoreModule, ], providers: [ + JanitorService, QueueStatsService, ServerStatsService, ], exports: [ + JanitorService, QueueStatsService, ServerStatsService, ], diff --git a/packages/backend/src/daemons/JanitorService.ts b/packages/backend/src/daemons/JanitorService.ts new file mode 100644 index 0000000000..f826d50625 --- /dev/null +++ b/packages/backend/src/daemons/JanitorService.ts @@ -0,0 +1,45 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { LessThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { AttestationChallengesRepository } from '@/models/index.js'; +import { bindThis } from '@/decorators.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +const interval = 30 * 60 * 1000; + +@Injectable() +export class JanitorService implements OnApplicationShutdown { + private intervalId: NodeJS.Timer; + + constructor( + @Inject(DI.attestationChallengesRepository) + private attestationChallengesRepository: AttestationChallengesRepository, + ) { + } + + /** + * Clean up database occasionally + */ + @bindThis + public start(): void { + const tick = async () => { + await this.attestationChallengesRepository.delete({ + createdAt: LessThan(new Date(new Date().getTime() - 5 * 60 * 1000)), + }); + }; + + tick(); + + this.intervalId = setInterval(tick, interval); + } + + @bindThis + public dispose(): void { + clearInterval(this.intervalId); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } +} diff --git a/packages/backend/src/daemons/QueueStatsService.ts b/packages/backend/src/daemons/QueueStatsService.ts index ede104b9fe..53a0d14cd7 100644 --- a/packages/backend/src/daemons/QueueStatsService.ts +++ b/packages/backend/src/daemons/QueueStatsService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import Xev from 'xev'; import * as Bull from 'bullmq'; @@ -19,7 +14,7 @@ const interval = 10000; @Injectable() export class QueueStatsService implements OnApplicationShutdown { - private intervalId: NodeJS.Timeout; + private intervalId: NodeJS.Timer; constructor( @Inject(DI.config) @@ -86,7 +81,7 @@ export class QueueStatsService implements OnApplicationShutdown { this.intervalId = setInterval(tick, interval); } - + @bindThis public dispose(): void { clearInterval(this.intervalId); diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index d229efb123..6cd71c0e2a 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -1,16 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import si from 'systeminformation'; import Xev from 'xev'; import * as osUtils from 'os-utils'; import { bindThis } from '@/decorators.js'; import type { OnApplicationShutdown } from '@nestjs/common'; -import { MiMeta } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; const ev = new Xev(); @@ -21,11 +14,9 @@ const round = (num: number) => Math.round(num * 10) / 10; @Injectable() export class ServerStatsService implements OnApplicationShutdown { - private intervalId: NodeJS.Timeout | null = null; + private intervalId: NodeJS.Timer; constructor( - @Inject(DI.meta) - private meta: MiMeta, ) { } @@ -33,13 +24,11 @@ export class ServerStatsService implements OnApplicationShutdown { * Report server stats regularly */ @bindThis - public async start(): Promise { - if (!this.meta.enableServerMachineStats) return; - + public start(): void { const log = [] as any[]; ev.on('requestServerStatsLog', x => { - ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length)); + ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length ?? 50)); }); const tick = async () => { @@ -75,9 +64,7 @@ export class ServerStatsService implements OnApplicationShutdown { @bindThis public dispose(): void { - if (this.intervalId) { - clearInterval(this.intervalId); - } + clearInterval(this.intervalId); } @bindThis @@ -110,5 +97,6 @@ async function net() { // FS STAT async function fs() { - return await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 })); + const data = await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 })); + return data ?? { rIO_sec: 0, wIO_sec: 0 }; } diff --git a/packages/backend/src/decorators.ts b/packages/backend/src/decorators.ts index 42f925e125..db23317eef 100644 --- a/packages/backend/src/decorators.ts +++ b/packages/backend/src/decorators.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - // https://github.com/andreypopp/autobind-decorator /** @@ -10,9 +5,8 @@ * The getter will return a .bind version of the function * and memoize the result against a symbol on the instance */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function bindThis(target: any, key: string, descriptor: any) { - const fn = descriptor.value; + let fn = descriptor.value; if (typeof fn !== 'function') { throw new TypeError(`@bindThis decorator can only be applied to methods not: ${typeof fn}`); @@ -22,18 +16,26 @@ export function bindThis(target: any, key: string, descriptor: any) { configurable: true, get() { // eslint-disable-next-line no-prototype-builtins - if (this === target.prototype || this.hasOwnProperty(key)) { + if (this === target.prototype || this.hasOwnProperty(key) || + typeof fn !== 'function') { return fn; } const boundFn = fn.bind(this); - Reflect.defineProperty(this, key, { - value: boundFn, + Object.defineProperty(this, key, { configurable: true, - writable: true, + get() { + return boundFn; + }, + set(value) { + fn = value; + delete this[key]; + }, }); - return boundFn; }, + set(value: any) { + fn = value; + }, }; } diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 77d2838e09..4a073f102f 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -1,18 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const DI = { config: Symbol('config'), db: Symbol('db'), - meta: Symbol('meta'), meilisearch: Symbol('meilisearch'), redis: Symbol('redis'), redisForPub: Symbol('redisForPub'), redisForSub: Symbol('redisForSub'), - redisForTimelines: Symbol('redisForTimelines'), - redisForReactions: Symbol('redisForReactions'), //#region Repositories usersRepository: Symbol('usersRepository'), @@ -20,20 +12,21 @@ export const DI = { announcementsRepository: Symbol('announcementsRepository'), announcementReadsRepository: Symbol('announcementReadsRepository'), appsRepository: Symbol('appsRepository'), - avatarDecorationsRepository: Symbol('avatarDecorationsRepository'), noteFavoritesRepository: Symbol('noteFavoritesRepository'), noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'), noteReactionsRepository: Symbol('noteReactionsRepository'), + noteUnreadsRepository: Symbol('noteUnreadsRepository'), pollsRepository: Symbol('pollsRepository'), pollVotesRepository: Symbol('pollVotesRepository'), userProfilesRepository: Symbol('userProfilesRepository'), userKeypairsRepository: Symbol('userKeypairsRepository'), userPendingsRepository: Symbol('userPendingsRepository'), + attestationChallengesRepository: Symbol('attestationChallengesRepository'), userSecurityKeysRepository: Symbol('userSecurityKeysRepository'), userPublickeysRepository: Symbol('userPublickeysRepository'), userListsRepository: Symbol('userListsRepository'), userListFavoritesRepository: Symbol('userListFavoritesRepository'), - userListMembershipsRepository: Symbol('userListMembershipsRepository'), + userListJoiningsRepository: Symbol('userListJoiningsRepository'), userNotePiningsRepository: Symbol('userNotePiningsRepository'), userIpsRepository: Symbol('userIpsRepository'), usedUsernamesRepository: Symbol('usedUsernamesRepository'), @@ -50,7 +43,6 @@ export const DI = { swSubscriptionsRepository: Symbol('swSubscriptionsRepository'), hashtagsRepository: Symbol('hashtagsRepository'), abuseUserReportsRepository: Symbol('abuseUserReportsRepository'), - abuseReportNotificationRecipientRepository: Symbol('abuseReportNotificationRecipientRepository'), registrationTicketsRepository: Symbol('registrationTicketsRepository'), authSessionsRepository: Symbol('authSessionsRepository'), accessTokensRepository: Symbol('accessTokensRepository'), @@ -67,13 +59,12 @@ export const DI = { promoNotesRepository: Symbol('promoNotesRepository'), promoReadsRepository: Symbol('promoReadsRepository'), relaysRepository: Symbol('relaysRepository'), + mutedNotesRepository: Symbol('mutedNotesRepository'), channelsRepository: Symbol('channelsRepository'), channelFollowingsRepository: Symbol('channelFollowingsRepository'), channelFavoritesRepository: Symbol('channelFavoritesRepository'), registryItemsRepository: Symbol('registryItemsRepository'), webhooksRepository: Symbol('webhooksRepository'), - systemWebhooksRepository: Symbol('systemWebhooksRepository'), - systemAccountsRepository: Symbol('systemAccountsRepository'), adsRepository: Symbol('adsRepository'), passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'), retentionAggregationsRepository: Symbol('retentionAggregationsRepository'), @@ -82,12 +73,5 @@ export const DI = { flashsRepository: Symbol('flashsRepository'), flashLikesRepository: Symbol('flashLikesRepository'), userMemosRepository: Symbol('userMemosRepository'), - chatMessagesRepository: Symbol('chatMessagesRepository'), - chatApprovalsRepository: Symbol('chatApprovalsRepository'), - chatRoomsRepository: Symbol('chatRoomsRepository'), - chatRoomMembershipsRepository: Symbol('chatRoomMembershipsRepository'), - chatRoomInvitationsRepository: Symbol('chatRoomInvitationsRepository'), - bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), - reversiGamesRepository: Symbol('reversiGamesRepository'), //#endregion }; diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts index ba44cfa2e6..d7c8304b47 100644 --- a/packages/backend/src/env.ts +++ b/packages/backend/src/env.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - const envOption = { onlyQueue: false, onlyServer: false, diff --git a/packages/backend/src/global.d.ts b/packages/backend/src/global.d.ts index 2f19e85525..7343aa1994 100644 --- a/packages/backend/src/global.d.ts +++ b/packages/backend/src/global.d.ts @@ -1,6 +1 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - type FIXME = any; diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts index ff5363a425..465b557ce4 100644 --- a/packages/backend/src/logger.ts +++ b/packages/backend/src/logger.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import cluster from 'node:cluster'; import chalk from 'chalk'; import { default as convertColor } from 'color-convert'; @@ -18,31 +13,34 @@ type Context = { type Level = 'error' | 'success' | 'warning' | 'debug' | 'info'; -// eslint-disable-next-line import/no-default-export export default class Logger { private context: Context; private parentLogger: Logger | null = null; + private store: boolean; - constructor(context: string, color?: KEYWORD) { + constructor(context: string, color?: KEYWORD, store = true) { this.context = { name: context, color: color, }; + this.store = store; } @bindThis - public createSubLogger(context: string, color?: KEYWORD): Logger { - const logger = new Logger(context, color); + public createSubLogger(context: string, color?: KEYWORD, store = true): Logger { + const logger = new Logger(context, color, store); logger.parentLogger = this; return logger; } @bindThis - private log(level: Level, message: string, data?: Record | null, important = false, subContexts: Context[] = []): void { + private log(level: Level, message: string, data?: Record | null, important = false, subContexts: Context[] = [], store = true): void { if (envOption.quiet) return; + if (!this.store) store = false; + if (level === 'debug') store = false; if (this.parentLogger) { - this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts)); + this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts), store); return; } @@ -67,11 +65,8 @@ export default class Logger { let log = `${l} ${worker}\t[${contexts.join(' ')}]\t${m}`; if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log; - const args: unknown[] = [important ? chalk.bold(log) : log]; - if (data != null) { - args.push(data); - } - console.log(...args); + console.log(important ? chalk.bold(log) : log); + if (level === 'error' && data) console.log(data); } @bindThis diff --git a/packages/backend/src/misc/FileWriterStream.ts b/packages/backend/src/misc/FileWriterStream.ts deleted file mode 100644 index 27c67cb5df..0000000000 --- a/packages/backend/src/misc/FileWriterStream.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as fs from 'node:fs/promises'; -import { WritableStream } from 'node:stream/web'; -import type { PathLike } from 'node:fs'; - -/** - * `fs.createWriteStream()`相当のことを行う`WritableStream` (Web標準) - */ -export class FileWriterStream extends WritableStream { - constructor(path: PathLike) { - let file: fs.FileHandle | null = null; - - super({ - start: async () => { - file = await fs.open(path, 'a'); - }, - write: async (chunk, controller) => { - if (file === null) { - controller.error(); - throw new Error(); - } - - await file.write(chunk); - }, - close: async () => { - await file?.close(); - }, - abort: async () => { - await file?.close(); - }, - }); - } -} diff --git a/packages/backend/src/misc/JsonArrayStream.ts b/packages/backend/src/misc/JsonArrayStream.ts deleted file mode 100644 index 754938989d..0000000000 --- a/packages/backend/src/misc/JsonArrayStream.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { TransformStream } from 'node:stream/web'; - -/** - * ストリームに流れてきた各データについて`JSON.stringify()`した上で、それらを一つの配列にまとめる - */ -export class JsonArrayStream extends TransformStream { - constructor() { - /** 最初の要素かどうかを変数に記録 */ - let isFirst = true; - - super({ - start(controller) { - controller.enqueue('['); - }, - flush(controller) { - controller.enqueue(']'); - }, - transform(chunk, controller) { - if (isFirst) { - isFirst = false; - } else { - // 妥当なJSON配列にするためには最初以外の要素の前に`,`を挿入しなければならない - controller.enqueue(',\n'); - } - - controller.enqueue(JSON.stringify(chunk)); - }, - }); - } -} diff --git a/packages/backend/src/misc/acct.ts b/packages/backend/src/misc/acct.ts index 3d729b1151..d1a6852a95 100644 --- a/packages/backend/src/misc/acct.ts +++ b/packages/backend/src/misc/acct.ts @@ -1,15 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export type Acct = { username: string; host: string | null; }; export function parse(acct: string): Acct { - if (acct.startsWith('@')) acct = acct.substring(1); + if (acct.startsWith('@')) acct = acct.substr(1); const split = acct.split('@', 2); return { username: split[0], host: split[1] ?? null }; } diff --git a/packages/backend/src/misc/api-permissions.ts b/packages/backend/src/misc/api-permissions.ts new file mode 100644 index 0000000000..160cdf9fd6 --- /dev/null +++ b/packages/backend/src/misc/api-permissions.ts @@ -0,0 +1,35 @@ +export const kinds = [ + 'read:account', + 'write:account', + 'read:blocks', + 'write:blocks', + 'read:drive', + 'write:drive', + 'read:favorites', + 'write:favorites', + 'read:following', + 'write:following', + 'read:messaging', + 'write:messaging', + 'read:mutes', + 'write:mutes', + 'write:notes', + 'read:notifications', + 'write:notifications', + 'read:reactions', + 'write:reactions', + 'write:votes', + 'read:pages', + 'write:pages', + 'write:page-likes', + 'read:page-likes', + 'read:user-groups', + 'write:user-groups', + 'read:channels', + 'write:channels', + 'read:gallery', + 'write:gallery', + 'read:gallery-likes', + 'write:gallery-likes', +]; +// IF YOU ADD KINDS(PERMISSIONS), YOU MUST ADD TRANSLATIONS (under _permissions). diff --git a/packages/backend/src/misc/bigint.ts b/packages/backend/src/misc/bigint.ts deleted file mode 100644 index efa1527ec9..0000000000 --- a/packages/backend/src/misc/bigint.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -function parseBigIntChunked(str: string, base: number, chunkSize: number, powerOfChunkSize: bigint): bigint { - const chunks = []; - while (str.length > 0) { - chunks.unshift(str.slice(-chunkSize)); - str = str.slice(0, -chunkSize); - } - let result = 0n; - for (const chunk of chunks) { - result *= powerOfChunkSize; - const int = parseInt(chunk, base); - if (Number.isNaN(int)) { - throw new Error('Invalid base36 string'); - } - result += BigInt(int); - } - return result; -} - -export function parseBigInt36(str: string): bigint { - // log_36(Number.MAX_SAFE_INTEGER) => 10.251599391715352 - // so we process 10 chars at once - return parseBigIntChunked(str, 36, 10, 36n ** 10n); -} - -export function parseBigInt16(str: string): bigint { - // log_16(Number.MAX_SAFE_INTEGER) => 13.25 - // so we process 13 chars at once - return parseBigIntChunked(str, 16, 13, 16n ** 13n); -} - -export function parseBigInt32(str: string): bigint { - // log_32(Number.MAX_SAFE_INTEGER) => 10.6 - // so we process 10 chars at once - return parseBigIntChunked(str, 32, 10, 32n ** 10n); -} diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index f9692ce5d5..f130a7db8b 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,29 +1,24 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as Redis from 'ioredis'; import { bindThis } from '@/decorators.js'; export class RedisKVCache { - private readonly lifetime: number; - private readonly memoryCache: MemoryKVCache; - private readonly fetcher: (key: string) => Promise; - private readonly toRedisConverter: (value: T) => string; - private readonly fromRedisConverter: (value: string) => T | undefined; + private redisClient: Redis.Redis; + private name: string; + private lifetime: number; + private memoryCache: MemoryKVCache; + private fetcher: (key: string) => Promise; + private toRedisConverter: (value: T) => string; + private fromRedisConverter: (value: string) => T | undefined; - constructor( - private redisClient: Redis.Redis, - private name: string, - opts: { - lifetime: RedisKVCache['lifetime']; - memoryCacheLifetime: number; - fetcher: RedisKVCache['fetcher']; - toRedisConverter: RedisKVCache['toRedisConverter']; - fromRedisConverter: RedisKVCache['fromRedisConverter']; - }, - ) { + constructor(redisClient: RedisKVCache['redisClient'], name: RedisKVCache['name'], opts: { + lifetime: RedisKVCache['lifetime']; + memoryCacheLifetime: number; + fetcher: RedisKVCache['fetcher']; + toRedisConverter: RedisKVCache['toRedisConverter']; + fromRedisConverter: RedisKVCache['fromRedisConverter']; + }) { + this.redisClient = redisClient; + this.name = name; this.lifetime = opts.lifetime; this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime); this.fetcher = opts.fetcher; @@ -55,13 +50,7 @@ export class RedisKVCache { const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`); if (cached == null) return undefined; - - const value = this.fromRedisConverter(cached); - if (value !== undefined) { - this.memoryCache.set(key, value); - } - - return value; + return this.fromRedisConverter(cached); } @bindThis @@ -72,10 +61,6 @@ export class RedisKVCache { /** * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します - * This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons: - * * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster. - * * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value. - * * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses. */ @bindThis public async fetch(key: string): Promise { @@ -87,14 +72,14 @@ export class RedisKVCache { // Cache MISS const value = await this.fetcher(key); - await this.set(key, value); + this.set(key, value); return value; } @bindThis public async refresh(key: string) { const value = await this.fetcher(key); - await this.set(key, value); + this.set(key, value); // TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする } @@ -111,23 +96,23 @@ export class RedisKVCache { } export class RedisSingleCache { - private readonly lifetime: number; - private readonly memoryCache: MemorySingleCache; - private readonly fetcher: () => Promise; - private readonly toRedisConverter: (value: T) => string; - private readonly fromRedisConverter: (value: string) => T | undefined; + private redisClient: Redis.Redis; + private name: string; + private lifetime: number; + private memoryCache: MemorySingleCache; + private fetcher: () => Promise; + private toRedisConverter: (value: T) => string; + private fromRedisConverter: (value: string) => T | undefined; - constructor( - private redisClient: Redis.Redis, - private name: string, - opts: { - lifetime: number; - memoryCacheLifetime: number; - fetcher: RedisSingleCache['fetcher']; - toRedisConverter: RedisSingleCache['toRedisConverter']; - fromRedisConverter: RedisSingleCache['fromRedisConverter']; - }, - ) { + constructor(redisClient: RedisSingleCache['redisClient'], name: RedisSingleCache['name'], opts: { + lifetime: RedisSingleCache['lifetime']; + memoryCacheLifetime: number; + fetcher: RedisSingleCache['fetcher']; + toRedisConverter: RedisSingleCache['toRedisConverter']; + fromRedisConverter: RedisSingleCache['fromRedisConverter']; + }) { + this.redisClient = redisClient; + this.name = name; this.lifetime = opts.lifetime; this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime); this.fetcher = opts.fetcher; @@ -159,13 +144,7 @@ export class RedisSingleCache { const cached = await this.redisClient.get(`singlecache:${this.name}`); if (cached == null) return undefined; - - const value = this.fromRedisConverter(cached); - if (value !== undefined) { - this.memoryCache.set(value); - } - - return value; + return this.fromRedisConverter(cached); } @bindThis @@ -176,10 +155,6 @@ export class RedisSingleCache { /** * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します - * This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons: - * * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster. - * * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value. - * * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses. */ @bindThis public async fetch(): Promise { @@ -191,14 +166,14 @@ export class RedisSingleCache { // Cache MISS const value = await this.fetcher(); - await this.set(value); + this.set(value); return value; } @bindThis public async refresh() { const value = await this.fetcher(); - await this.set(value); + this.set(value); // TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする } @@ -207,18 +182,20 @@ export class RedisSingleCache { // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? export class MemoryKVCache { - private readonly cache = new Map(); - private readonly gcIntervalHandle = setInterval(() => this.gc(), 1000 * 60 * 3); // 3m + public cache: Map; + private lifetime: number; + private gcIntervalHandle: NodeJS.Timer; - constructor( - private readonly lifetime: number, - ) {} + constructor(lifetime: MemoryKVCache['lifetime']) { + this.cache = new Map(); + this.lifetime = lifetime; + + this.gcIntervalHandle = setInterval(() => { + this.gc(); + }, 1000 * 60 * 3); + } @bindThis - /** - * Mapにキャッシュをセットします - * @deprecated これを直接呼び出すべきではない。InternalEventなどで変更を全てのプロセス/マシンに通知するべき - */ public set(key: string, value: T): void { this.cache.set(key, { date: Date.now(), @@ -297,14 +274,10 @@ export class MemoryKVCache { @bindThis public gc(): void { const now = Date.now(); - for (const [key, { date }] of this.cache.entries()) { - // The map is ordered from oldest to youngest. - // We can stop once we find an entry that's still active, because all following entries must *also* be active. - const age = now - date; - if (age < this.lifetime) break; - - this.cache.delete(key); + if ((now - date) > this.lifetime) { + this.cache.delete(key); + } } } @@ -312,19 +285,16 @@ export class MemoryKVCache { public dispose(): void { clearInterval(this.gcIntervalHandle); } - - public get entries() { - return this.cache.entries(); - } } export class MemorySingleCache { private cachedAt: number | null = null; private value: T | undefined; + private lifetime: number; - constructor( - private lifetime: number, - ) {} + constructor(lifetime: MemorySingleCache['lifetime']) { + this.lifetime = lifetime; + } @bindThis public set(value: T): void { diff --git a/packages/backend/src/misc/check-https.ts b/packages/backend/src/misc/check-https.ts index 15a54f6ce7..b33f019973 100644 --- a/packages/backend/src/misc/check-https.ts +++ b/packages/backend/src/misc/check-https.ts @@ -1,9 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function checkHttps(url: string): boolean { - return url.startsWith('https://') || - (url.startsWith('http://') && process.env.NODE_ENV !== 'production'); +export function checkHttps(url: string) { + return url.startsWith('https://') || + (url.startsWith('http://') && process.env.NODE_ENV !== 'production'); } diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts index c50f2b723c..910bebfcfe 100644 --- a/packages/backend/src/misc/check-word-mute.ts +++ b/packages/backend/src/misc/check-word-mute.ts @@ -1,21 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { AhoCorasick } from 'slacc'; import RE2 from 're2'; -import type { MiNote } from '@/models/Note.js'; -import type { MiUser } from '@/models/User.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { User } from '@/models/entities/User.js'; type NoteLike = { - userId: MiNote['userId']; - text: MiNote['text']; - cw?: MiNote['cw']; + userId: Note['userId']; + text: Note['text']; + cw?: Note['cw']; }; type UserLike = { - id: MiUser['id']; + id: User['id']; }; const acCache = new Map(); diff --git a/packages/backend/src/misc/clone.ts b/packages/backend/src/misc/clone.ts index ed05485649..16fad24129 100644 --- a/packages/backend/src/misc/clone.ts +++ b/packages/backend/src/misc/clone.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - // structredCloneが遅いため // SEE: http://var.blog.jp/archives/86038606.html -type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | Cloneable[]; +type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[]; export function deepClone(x: T): T { if (typeof x === 'object') { @@ -14,7 +9,7 @@ export function deepClone(x: T): T { if (Array.isArray(x)) return x.map(deepClone) as T; const obj = {} as Record; for (const [k, v] of Object.entries(x)) { - obj[k] = v === undefined ? undefined : deepClone(v); + obj[k] = deepClone(v); } return obj as T; } else { diff --git a/packages/backend/src/misc/collapsed-queue.ts b/packages/backend/src/misc/collapsed-queue.ts deleted file mode 100644 index 5bc20a78ae..0000000000 --- a/packages/backend/src/misc/collapsed-queue.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -type Job = { - value: V; - timer: NodeJS.Timeout; -}; - -// TODO: redis使えるようにする -export class CollapsedQueue { - private jobs: Map> = new Map(); - - constructor( - private timeout: number, - private collapse: (oldValue: V, newValue: V) => V, - private perform: (key: K, value: V) => Promise, - ) {} - - enqueue(key: K, value: V) { - if (this.jobs.has(key)) { - const old = this.jobs.get(key)!; - const merged = this.collapse(old.value, value); - this.jobs.set(key, { ...old, value: merged }); - } else { - const timer = setTimeout(() => { - const job = this.jobs.get(key)!; - this.jobs.delete(key); - this.perform(key, job.value); - }, this.timeout); - this.jobs.set(key, { value, timer }); - } - } - - async performAllNow() { - const entries = [...this.jobs.entries()]; - this.jobs.clear(); - for (const [_key, job] of entries) { - clearTimeout(job.timer); - } - await Promise.allSettled(entries.map(([key, job]) => this.perform(key, job.value))); - } -} diff --git a/packages/backend/src/misc/content-disposition.ts b/packages/backend/src/misc/content-disposition.ts index 467b5057d6..b2aec471d5 100644 --- a/packages/backend/src/misc/content-disposition.ts +++ b/packages/backend/src/misc/content-disposition.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import cd from 'content-disposition'; export function contentDisposition(type: 'inline' | 'attachment', filename: string): string { diff --git a/packages/backend/src/misc/correct-filename.ts b/packages/backend/src/misc/correct-filename.ts index f7ee02781d..23a0699f39 100644 --- a/packages/backend/src/misc/correct-filename.ts +++ b/packages/backend/src/misc/correct-filename.ts @@ -1,58 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/** - * Array.includes()よりSet.has()の方が高速 - */ -const targetExtsToSkip = new Set([ - '.gz', - '.tar', - '.tgz', - '.bz2', - '.xz', - '.zip', - '.7z', -]); - -const extRegExp = /\.[0-9a-zA-Z]+$/i; - -/** - * 与えられた拡張子とファイル名が一致しているかどうかを確認し、 - * 一致していない場合は拡張子を付与して返す - * - * extはfile-typeのextを想定 - */ +// 与えられた拡張子とファイル名が一致しているかどうかを確認し、 +// 一致していない場合は拡張子を付与して返す export function correctFilename(filename: string, ext: string | null) { - const dotExt = ext ? ext[0] === '.' ? ext : `.${ext}` : '.unknown'; - - const match = extRegExp.exec(filename); - if (!match || !match[0]) { - // filenameが拡張子を持っていない場合は拡張子をつける - return `${filename}${dotExt}`; - } - - const filenameExt = match[0].toLowerCase(); - if ( - // 未知のファイル形式かつ拡張子がある場合は何もしない - ext === null || - // 拡張子が一致している場合は何もしない - filenameExt === dotExt || - - // jpeg, tiffを同一視 - dotExt === '.jpg' && filenameExt === '.jpeg' || - dotExt === '.tif' && filenameExt === '.tiff' || - // dllもexeもportable executableなので判定が正しく行われない - dotExt === '.exe' && filenameExt === '.dll' || - - // 圧縮形式っぽければ下手に拡張子を変えない - // https://github.com/misskey-dev/misskey/issues/11482 - targetExtsToSkip.has(dotExt) - ) { + const dotExt = ext ? ext.startsWith('.') ? ext : `.${ext}` : '.unknown'; + if (filename.endsWith(dotExt)) { + return filename; + } + if (ext === 'jpg' && filename.endsWith('.jpeg')) { + return filename; + } + if (ext === 'tif' && filename.endsWith('.tiff')) { return filename; } - - // 拡張子があるが一致していないなどの場合は拡張子を付け足す return `${filename}${dotExt}`; } diff --git a/packages/backend/src/misc/create-temp.ts b/packages/backend/src/misc/create-temp.ts index 9aaecf8263..7b8942e308 100644 --- a/packages/backend/src/misc/create-temp.ts +++ b/packages/backend/src/misc/create-temp.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as tmp from 'tmp'; export function createTemp(): Promise<[string, () => void]> { diff --git a/packages/backend/src/misc/dev-null.ts b/packages/backend/src/misc/dev-null.ts index 4d9806fbe8..38b9d82669 100644 --- a/packages/backend/src/misc/dev-null.ts +++ b/packages/backend/src/misc/dev-null.ts @@ -1,16 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Writable, WritableOptions } from 'node:stream'; +import { Writable, WritableOptions } from "node:stream"; export class DevNull extends Writable implements NodeJS.WritableStream { - constructor(opts?: WritableOptions) { - super(opts); - } + constructor(opts?: WritableOptions) { + super(opts); + } - _write (chunk: any, encoding: BufferEncoding, cb: (err?: Error | null) => void) { - setImmediate(cb); - } + _write (chunk: any, encoding: BufferEncoding, cb: (err?: Error | null) => void) { + setImmediate(cb); + } } diff --git a/packages/backend/src/misc/emoji-regex.ts b/packages/backend/src/misc/emoji-regex.ts index 6d03b433ba..1c6f5776db 100644 --- a/packages/backend/src/misc/emoji-regex.ts +++ b/packages/backend/src/misc/emoji-regex.ts @@ -1,9 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// taken from @twemoji/parser/dist/lib/regex.js -const twemojiRegex = /(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83e\udef1\ud83c\udffb\u200d\ud83e\udef2\ud83c[\udffc-\udfff]|\ud83e\udef1\ud83c\udffc\u200d\ud83e\udef2\ud83c[\udffb\udffd-\udfff]|\ud83e\udef1\ud83c\udffd\u200d\ud83e\udef2\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\udef1\ud83c\udffe\u200d\ud83e\udef2\ud83c[\udffb-\udffd\udfff]|\ud83e\udef1\ud83c\udfff\u200d\ud83e\udef2\ud83c[\udffb-\udffe]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d\udc8f\ud83c[\udffb-\udfff]|\ud83d\udc91\ud83c[\udffb-\udfff]|\ud83e\udd1d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d\udc8f\udc91]|\ud83e\udd1d)|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd4\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|\u2764\ufe0f\u200d\ud83d\udd25|\u2764\ufe0f\u200d\ud83e\ude79|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude35\u200d\ud83d\udcab|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b|\ud83d\udc26\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|\ud83e\udef0|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd\udec3-\udec5\udef1-\udef8]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udc8e\udc90\udc92-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udedc-\udedf\udeeb\udeec\udef4-\udefc\udfe0-\udfeb\udff0]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78-\uddb4\uddb7\uddba\uddbc-\uddcc\uddd0\uddde-\uddff\ude70-\ude7c\ude80-\ude88\ude90-\udebd\udebf-\udec2\udece-\udedb\udee0-\udee8]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g; +// taken from twemoji-parser/dist/lib/regex.js +const twemojiRegex = /(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83e\udef1\ud83c\udffb\u200d\ud83e\udef2\ud83c[\udffc-\udfff]|\ud83e\udef1\ud83c\udffc\u200d\ud83e\udef2\ud83c[\udffb\udffd-\udfff]|\ud83e\udef1\ud83c\udffd\u200d\ud83e\udef2\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\udef1\ud83c\udffe\u200d\ud83e\udef2\ud83c[\udffb-\udffd\udfff]|\ud83e\udef1\ud83c\udfff\u200d\ud83e\udef2\ud83c[\udffb-\udffe]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d\udc8f\ud83c[\udffb-\udfff]|\ud83d\udc91\ud83c[\udffb-\udfff]|\ud83e\udd1d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d\udc8f\udc91]|\ud83e\udd1d)|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd4\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|\u2764\ufe0f\u200d\ud83d\udd25|\u2764\ufe0f\u200d\ud83e\ude79|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude35\u200d\ud83d\udcab|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd\udec3-\udec5\udef0-\udef6]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udc8e\udc90\udc92-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udedd-\udedf\udeeb\udeec\udef4-\udefc\udfe0-\udfeb\udff0]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78-\uddb4\uddb7\uddba\uddbc-\uddcc\uddd0\uddde-\uddff\ude70-\ude74\ude78-\ude7c\ude80-\ude86\ude90-\udeac\udeb0-\udeba\udec0-\udec2\uded0-\uded9\udee0-\udee7]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g; export const emojiRegex = new RegExp(`(${twemojiRegex.source})`); diff --git a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts index 73ae9abb54..14c25922ad 100644 --- a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts +++ b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as mfm from 'mfm-js'; import { unique } from '@/misc/prelude/array.js'; diff --git a/packages/backend/src/misc/extract-hashtags.ts b/packages/backend/src/misc/extract-hashtags.ts index d3d245d414..d293fd7f52 100644 --- a/packages/backend/src/misc/extract-hashtags.ts +++ b/packages/backend/src/misc/extract-hashtags.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as mfm from 'mfm-js'; import { unique } from '@/misc/prelude/array.js'; diff --git a/packages/backend/src/misc/extract-mentions.ts b/packages/backend/src/misc/extract-mentions.ts index 2ec9349718..c8762e797b 100644 --- a/packages/backend/src/misc/extract-mentions.ts +++ b/packages/backend/src/misc/extract-mentions.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - // test is located in test/extract-mentions import * as mfm from 'mfm-js'; diff --git a/packages/backend/src/misc/fastify-hook-handlers.ts b/packages/backend/src/misc/fastify-hook-handlers.ts deleted file mode 100644 index fa3ef0a267..0000000000 --- a/packages/backend/src/misc/fastify-hook-handlers.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { onRequestHookHandler } from 'fastify'; - -export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => { - const index = request.url.indexOf('?'); - if (~index) { - reply.redirect(request.url.slice(0, index), 301); - } - done(); -}; diff --git a/packages/backend/src/misc/fastify-reply-error.ts b/packages/backend/src/misc/fastify-reply-error.ts index e6c4e78d2f..4e987175e2 100644 --- a/packages/backend/src/misc/fastify-reply-error.ts +++ b/packages/backend/src/misc/fastify-reply-error.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - // https://www.fastify.io/docs/latest/Reference/Reply/#async-await-and-promises export class FastifyReplyError extends Error { public message: string; diff --git a/packages/backend/src/misc/gen-identicon.ts b/packages/backend/src/misc/gen-identicon.ts index 342e0f8602..b40745973e 100644 --- a/packages/backend/src/misc/gen-identicon.ts +++ b/packages/backend/src/misc/gen-identicon.ts @@ -1,15 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - /** * Identicon generator * https://en.wikipedia.org/wiki/Identicon */ -import { createCanvas } from '@napi-rs/canvas'; +import * as p from 'pureimage'; import gen from 'random-seed'; +import type { WriteStream } from 'node:fs'; const size = 128; // px const n = 5; // resolution @@ -44,9 +40,9 @@ const sideN = Math.floor(n / 2); /** * Generate buffer of an identicon by seed */ -export async function genIdenticon(seed: string): Promise { +export function genIdenticon(seed: string, stream: WriteStream): Promise { const rand = gen.create(seed); - const canvas = createCanvas(size, size); + const canvas = p.make(size, size, undefined); const ctx = canvas.getContext('2d'); const bgColors = colors[rand(colors.length)]; @@ -100,5 +96,5 @@ export async function genIdenticon(seed: string): Promise { } } - return await canvas.encode('png'); + return p.encodePNGToStream(canvas, stream); } diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts index 02a303dc0a..e2ad598501 100644 --- a/packages/backend/src/misc/gen-key-pair.ts +++ b/packages/backend/src/misc/gen-key-pair.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as crypto from 'node:crypto'; import * as util from 'node:util'; diff --git a/packages/backend/src/misc/generate-invite-code.ts b/packages/backend/src/misc/generate-invite-code.ts deleted file mode 100644 index 006920cf0e..0000000000 --- a/packages/backend/src/misc/generate-invite-code.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { secureRndstr } from './secure-rndstr.js'; - -const CHARS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; // [0-9A-Z] w/o [01IO] (32 patterns) - -export function generateInviteCode(): string { - const code = secureRndstr(8, { - chars: CHARS, - }); - - const uniqueId = []; - let n = Math.floor(Date.now() / 1000 / 60); - while (true) { - uniqueId.push(CHARS[n % CHARS.length]); - const t = Math.floor(n / CHARS.length); - if (!t) break; - n = t; - } - - return code + uniqueId.reverse().join(''); -} diff --git a/packages/backend/src/misc/generate-native-user-token.ts b/packages/backend/src/misc/generate-native-user-token.ts new file mode 100644 index 0000000000..7292d765a8 --- /dev/null +++ b/packages/backend/src/misc/generate-native-user-token.ts @@ -0,0 +1,3 @@ +import { secureRndstr } from '@/misc/secure-rndstr.js'; + +export default () => secureRndstr(16); diff --git a/packages/backend/src/misc/get-ip-hash.ts b/packages/backend/src/misc/get-ip-hash.ts index e132fa8f31..1a86fb8814 100644 --- a/packages/backend/src/misc/get-ip-hash.ts +++ b/packages/backend/src/misc/get-ip-hash.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import IPCIDR from 'ip-cidr'; export function getIpHash(ip: string): string { diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts index 1a07139a50..964f20b25b 100644 --- a/packages/backend/src/misc/get-note-summary.ts +++ b/packages/backend/src/misc/get-note-summary.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import type { Packed } from './json-schema.js'; /** diff --git a/packages/backend/src/misc/get-reaction-emoji.ts b/packages/backend/src/misc/get-reaction-emoji.ts index 3f975853ed..c2e0b98582 100644 --- a/packages/backend/src/misc/get-reaction-emoji.ts +++ b/packages/backend/src/misc/get-reaction-emoji.ts @@ -1,9 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// eslint-disable-next-line import/no-default-export export default function(reaction: string): string { switch (reaction) { case 'like': return '👍'; diff --git a/packages/backend/src/misc/i18n.ts b/packages/backend/src/misc/i18n.ts index 6cbbdef74c..b1c727827d 100644 --- a/packages/backend/src/misc/i18n.ts +++ b/packages/backend/src/misc/i18n.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class I18n> { public locale: T; diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts index c0e8478db5..f0cbc9900d 100644 --- a/packages/backend/src/misc/id/aid.ts +++ b/packages/backend/src/misc/id/aid.ts @@ -1,13 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - // AID // 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ2の[ノイズ文字列] import * as crypto from 'node:crypto'; -import { parseBigInt36 } from '@/misc/bigint.js'; export const aidRegExp = /^[0-9a-z]{10}$/; @@ -25,7 +19,8 @@ function getNoise(): string { return counter.toString(36).padStart(2, '0').slice(-2); } -export function genAid(t: number): string { +export function genAid(date: Date): string { + const t = date.getTime(); if (isNaN(t)) throw new Error('Failed to create AID: Invalid Date'); counter++; return getTime(t) + getNoise(); @@ -35,13 +30,3 @@ export function parseAid(id: string): { date: Date; } { const time = parseInt(id.slice(0, 8), 36) + TIME2000; return { date: new Date(time) }; } - -export function parseAidFull(id: string): { date: number; additional: bigint; } { - const date = parseInt(id.slice(0, 8), 36) + TIME2000; - const additional = parseBigInt36(id.slice(8, 10)); - return { date, additional }; -} - -export function isSafeAidT(t: number): boolean { - return t > TIME2000; -} diff --git a/packages/backend/src/misc/id/aidx.ts b/packages/backend/src/misc/id/aidx.ts deleted file mode 100644 index 006673a6d0..0000000000 --- a/packages/backend/src/misc/id/aidx.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// AIDX -// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ4の[個体ID] + 長さ4の[カウンタ] -// (c) mei23 -// https://misskey.m544.net/notes/71899acdcc9859ec5708ac24 - -import { customAlphabet } from 'nanoid'; -import { parseBigInt36 } from '@/misc/bigint.js'; - -export const aidxRegExp = /^[0-9a-z]{16}$/; - -const TIME2000 = 946684800000; -const TIME_LENGTH = 8; -const NODE_LENGTH = 4; -const NOISE_LENGTH = 4; -const AIDX_LENGTH = TIME_LENGTH + NODE_LENGTH + NOISE_LENGTH; - -const nodeId = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', NODE_LENGTH)(); -let counter = 0; - -function getTime(time: number): string { - time = time - TIME2000; - if (time < 0) time = 0; - - return time.toString(36).padStart(TIME_LENGTH, '0').slice(-TIME_LENGTH); -} - -function getNoise(): string { - return counter.toString(36).padStart(NOISE_LENGTH, '0').slice(-NOISE_LENGTH); -} - -export function genAidx(t: number): string { - if (isNaN(t)) throw new Error('Failed to create AIDX: Invalid Date'); - counter++; - return getTime(t) + nodeId + getNoise(); -} - -export function parseAidx(id: string): { date: Date; } { - const time = parseInt(id.slice(0, TIME_LENGTH), 36) + TIME2000; - return { date: new Date(time) }; -} - -export function parseAidxFull(id: string): { date: number; additional: bigint; } { - const date = parseInt(id.slice(0, TIME_LENGTH), 36) + TIME2000; - const additional = parseBigInt36(id.slice(TIME_LENGTH, AIDX_LENGTH)); - return { date, additional }; -} - -export function isSafeAidxT(t: number): boolean { - return t > TIME2000; -} diff --git a/packages/backend/src/misc/id/meid.ts b/packages/backend/src/misc/id/meid.ts index 563e07ed8f..337416b059 100644 --- a/packages/backend/src/misc/id/meid.ts +++ b/packages/backend/src/misc/id/meid.ts @@ -1,10 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { parseBigInt16 } from '@/misc/bigint.js'; - const CHARS = '0123456789abcdef'; // same as object-id @@ -31,8 +24,8 @@ function getRandom() { return str; } -export function genMeid(t: number): string { - return getTime(t) + getRandom(); +export function genMeid(date: Date): string { + return getTime(date.getTime()) + getRandom(); } export function parseMeid(id: string): { date: Date; } { @@ -40,14 +33,3 @@ export function parseMeid(id: string): { date: Date; } { date: new Date(parseInt(id.slice(0, 12), 16) - 0x800000000000), }; } - -export function parseMeidFull(id: string): { date: number; additional: bigint; } { - return { - date: parseInt(id.slice(0, 12), 16) - 0x800000000000, - additional: parseBigInt16(id.slice(12, 24)), - }; -} - -export function isSafeMeidT(t: number): boolean { - return t > 0; -} diff --git a/packages/backend/src/misc/id/meidg.ts b/packages/backend/src/misc/id/meidg.ts index b825807114..19d0bc1fd2 100644 --- a/packages/backend/src/misc/id/meidg.ts +++ b/packages/backend/src/misc/id/meidg.ts @@ -1,10 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { parseBigInt16 } from '@/misc/bigint.js'; - const CHARS = '0123456789abcdef'; // 4bit Fixed hex value 'g' @@ -31,8 +24,8 @@ function getRandom() { return str; } -export function genMeidg(t: number): string { - return 'g' + getTime(t) + getRandom(); +export function genMeidg(date: Date): string { + return 'g' + getTime(date.getTime()) + getRandom(); } export function parseMeidg(id: string): { date: Date; } { @@ -40,14 +33,3 @@ export function parseMeidg(id: string): { date: Date; } { date: new Date(parseInt(id.slice(1, 12), 16)), }; } - -export function parseMeidgFull(id: string): { date: number; additional: bigint; } { - return { - date: parseInt(id.slice(1, 12), 16), - additional: parseBigInt16(id.slice(12, 24)), - }; -} - -export function isSafeMeidgT(t: number): boolean { - return t > 0; -} diff --git a/packages/backend/src/misc/id/object-id.ts b/packages/backend/src/misc/id/object-id.ts index 68409c7a61..aec3447bd7 100644 --- a/packages/backend/src/misc/id/object-id.ts +++ b/packages/backend/src/misc/id/object-id.ts @@ -1,10 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { parseBigInt16 } from '@/misc/bigint.js'; - const CHARS = '0123456789abcdef'; // same as meid @@ -31,8 +24,8 @@ function getRandom() { return str; } -export function genObjectId(t: number): string { - return getTime(t) + getRandom(); +export function genObjectId(date: Date): string { + return getTime(date.getTime()) + getRandom(); } export function parseObjectId(id: string): { date: Date; } { @@ -40,14 +33,3 @@ export function parseObjectId(id: string): { date: Date; } { date: new Date(parseInt(id.slice(0, 8), 16) * 1000), }; } - -export function parseObjectIdFull(id: string): { date: number; additional: bigint; } { - return { - date: parseInt(id.slice(0, 8), 16) * 1000, - additional: parseBigInt16(id.slice(8, 24)), - }; -} - -export function isSafeObjectIdT(t: number): boolean { - return t > 0; -} diff --git a/packages/backend/src/misc/id/ulid.ts b/packages/backend/src/misc/id/ulid.ts index 8b81702d19..e8aa752890 100644 --- a/packages/backend/src/misc/id/ulid.ts +++ b/packages/backend/src/misc/id/ulid.ts @@ -1,31 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - // Crockford's Base32 // https://github.com/ulid/spec#encoding -import { parseBigInt32 } from '@/misc/bigint.js'; - const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; export const ulidRegExp = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/; -function parseBase32(timestamp: string) { - let time = 0; - for (let i = 0; i < timestamp.length; i++) { - time = time * 32 + CHARS.indexOf(timestamp[i]); - } - return time; -} - export function parseUlid(id: string): { date: Date; } { - return { date: new Date(parseBase32(id.slice(0, 10))) }; -} - -export function parseUlidFull(id: string): { date: number; additional: bigint; } { - return { - date: parseBase32(id.slice(0, 10)), - additional: parseBigInt32(id.slice(10, 26)), - }; + const timestamp = id.slice(0, 10); + let time = 0; + for (let i = 0; i < 10; i++) { + time = time * 32 + CHARS.indexOf(timestamp[i]); + } + return { date: new Date(time) }; } diff --git a/packages/backend/src/misc/identifiable-error.ts b/packages/backend/src/misc/identifiable-error.ts index 13c41f1e3b..e394123f1b 100644 --- a/packages/backend/src/misc/identifiable-error.ts +++ b/packages/backend/src/misc/identifiable-error.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - /** * ID付きエラー */ diff --git a/packages/backend/src/misc/is-duplicate-key-value-error.ts b/packages/backend/src/misc/is-duplicate-key-value-error.ts index 8da0280f60..04ff191e41 100644 --- a/packages/backend/src/misc/is-duplicate-key-value-error.ts +++ b/packages/backend/src/misc/is-duplicate-key-value-error.ts @@ -1,10 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { QueryFailedError } from 'typeorm'; - export function isDuplicateKeyValueError(e: unknown | Error): boolean { - return e instanceof QueryFailedError && e.driverError.code === '23505'; + return (e as any).message && (e as Error).message.startsWith('duplicate key value'); } diff --git a/packages/backend/src/misc/is-instance-muted.ts b/packages/backend/src/misc/is-instance-muted.ts index 096a8b39c7..73ad0b3b82 100644 --- a/packages/backend/src/misc/is-instance-muted.ts +++ b/packages/backend/src/misc/is-instance-muted.ts @@ -1,15 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { MiNote } from '@/models/Note.js'; import type { Packed } from './json-schema.js'; -export function isInstanceMuted(note: Packed<'Note'> | MiNote, mutedInstances: Set): boolean { - if (mutedInstances.has(note.user?.host ?? '')) return true; - if (mutedInstances.has(note.reply?.user?.host ?? '')) return true; - if (mutedInstances.has(note.renote?.user?.host ?? '')) return true; +export function isInstanceMuted(note: Packed<'Note'>, mutedInstances: Set): boolean { + if (mutedInstances.has(note.user.host ?? '')) return true; + if (mutedInstances.has(note.reply?.user.host ?? '')) return true; + if (mutedInstances.has(note.renote?.user.host ?? '')) return true; return false; } diff --git a/packages/backend/src/misc/is-mime-image.ts b/packages/backend/src/misc/is-mime-image.ts index 8ffbc99230..46a66efc0f 100644 --- a/packages/backend/src/misc/is-mime-image.ts +++ b/packages/backend/src/misc/is-mime-image.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; const dictionary = { diff --git a/packages/backend/src/misc/is-native-token.ts b/packages/backend/src/misc/is-native-token.ts new file mode 100644 index 0000000000..2833c570c8 --- /dev/null +++ b/packages/backend/src/misc/is-native-token.ts @@ -0,0 +1 @@ +export default (token: string) => token.length === 16; diff --git a/packages/backend/src/misc/is-not-null.ts b/packages/backend/src/misc/is-not-null.ts new file mode 100644 index 0000000000..d89a1957be --- /dev/null +++ b/packages/backend/src/misc/is-not-null.ts @@ -0,0 +1,5 @@ +// we are using {} as "any non-nullish value" as expected +// eslint-disable-next-line @typescript-eslint/ban-types +export function isNotNull(input: T | undefined | null): input is T { + return input != null; +} diff --git a/packages/backend/src/misc/is-quote.ts b/packages/backend/src/misc/is-quote.ts new file mode 100644 index 0000000000..248b25a0bf --- /dev/null +++ b/packages/backend/src/misc/is-quote.ts @@ -0,0 +1,5 @@ +import type { Note } from '@/models/entities/Note.js'; + +export default function(note: Note): boolean { + return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0)); +} diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts deleted file mode 100644 index f4bb329d80..0000000000 --- a/packages/backend/src/misc/is-renote.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { MiNote } from '@/models/Note.js'; -import type { Packed } from '@/misc/json-schema.js'; - -// NoteEntityService.isPureRenote とよしなにリンク - -type Renote = - MiNote & { - renoteId: NonNullable - }; - -type Quote = - Renote & ({ - text: NonNullable - } | { - cw: NonNullable - } | { - replyId: NonNullable - reply: NonNullable - } | { - hasPoll: true - }); - -export function isRenote(note: MiNote): note is Renote { - return note.renoteId != null; -} - -export function isQuote(note: Renote): note is Quote { - // NOTE: SYNC WITH NoteCreateService.isQuote - return note.text != null || - note.cw != null || - note.replyId != null || - note.hasPoll || - note.fileIds.length > 0; -} - -type PackedRenote = - Packed<'Note'> & { - renoteId: NonNullable['renoteId']> - }; - -type PackedQuote = - PackedRenote & ({ - text: NonNullable['text']> - } | { - cw: NonNullable['cw']> - } | { - replyId: NonNullable['replyId']> - } | { - poll: NonNullable['poll']> - } | { - fileIds: NonNullable['fileIds']> - }); - -export function isRenotePacked(note: Packed<'Note'>): note is PackedRenote { - return note.renoteId != null; -} - -export function isQuotePacked(note: PackedRenote): note is PackedQuote { - return note.text != null || - note.cw != null || - note.replyId != null || - note.poll != null || - (note.fileIds != null && note.fileIds.length > 0); -} diff --git a/packages/backend/src/misc/is-reply.ts b/packages/backend/src/misc/is-reply.ts deleted file mode 100644 index 980eae11c9..0000000000 --- a/packages/backend/src/misc/is-reply.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { MiUser } from '@/models/User.js'; - -export function isReply(note: any, viewerId?: MiUser['id'] | undefined | null): boolean { - return note.replyId && note.replyUserId !== note.userId && note.replyUserId !== viewerId; -} diff --git a/packages/backend/src/misc/is-user-related.ts b/packages/backend/src/misc/is-user-related.ts index 6f72082733..e6bbdb5d35 100644 --- a/packages/backend/src/misc/is-user-related.ts +++ b/packages/backend/src/misc/is-user-related.ts @@ -1,37 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { MiUser } from '@/models/_.js'; - -interface NoteLike { - userId: MiUser['id']; - reply?: NoteLike | null; - renote?: NoteLike | null; - replyUserId?: MiUser['id'] | null; - renoteUserId?: MiUser['id'] | null; -} - -export function isUserRelated(note: NoteLike | null | undefined, userIds: Set, ignoreAuthor = false): boolean { - if (!note) { - return false; - } - - if (userIds.has(note.userId) && !ignoreAuthor) { +export function isUserRelated(note: any, userIds: Set): boolean { + if (userIds.has(note.userId)) { return true; } - const replyUserId = note.replyUserId ?? note.reply?.userId; - if (replyUserId != null && replyUserId !== note.userId && userIds.has(replyUserId)) { + if (note.reply != null && userIds.has(note.reply.userId)) { return true; } - const renoteUserId = note.renoteUserId ?? note.renote?.userId; - if (renoteUserId != null && renoteUserId !== note.userId && userIds.has(renoteUserId)) { + if (note.renote != null && userIds.has(note.renote.userId)) { return true; } return false; } - diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 5e5d7041b9..e748f93a26 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -1,15 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { - packedMeDetailedOnlySchema, - packedMeDetailedSchema, - packedUserDetailedNotMeOnlySchema, - packedUserDetailedNotMeSchema, - packedUserDetailedSchema, packedUserLiteSchema, + packedUserDetailedNotMeOnlySchema, + packedMeDetailedOnlySchema, + packedUserDetailedNotMeSchema, + packedMeDetailedSchema, + packedUserDetailedSchema, packedUserSchema, } from '@/models/json-schema/user.js'; import { packedNoteSchema } from '@/models/json-schema/note.js'; @@ -24,54 +19,16 @@ import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js' import { packedBlockingSchema } from '@/models/json-schema/blocking.js'; import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js'; import { packedHashtagSchema } from '@/models/json-schema/hashtag.js'; -import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js'; -import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js'; +import { packedPageSchema } from '@/models/json-schema/page.js'; import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js'; import { packedChannelSchema } from '@/models/json-schema/channel.js'; import { packedAntennaSchema } from '@/models/json-schema/antenna.js'; import { packedClipSchema } from '@/models/json-schema/clip.js'; import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js'; -import { - packedQueueCountSchema, - packedQueueMetricsSchema, - packedQueueJobSchema, -} from '@/models/json-schema/queue.js'; +import { packedQueueCountSchema } from '@/models/json-schema/queue.js'; import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js'; -import { - packedEmojiDetailedAdminSchema, - packedEmojiDetailedSchema, - packedEmojiSimpleSchema, -} from '@/models/json-schema/emoji.js'; +import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js'; import { packedFlashSchema } from '@/models/json-schema/flash.js'; -import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; -import { packedSigninSchema } from '@/models/json-schema/signin.js'; -import { - packedRoleCondFormulaFollowersOrFollowingOrNotesSchema, - packedRoleCondFormulaLogicsSchema, - packedRoleCondFormulaValueAssignedRoleSchema, - packedRoleCondFormulaValueCreatedSchema, - packedRoleCondFormulaValueIsLocalOrRemoteSchema, - packedRoleCondFormulaValueNot, - packedRoleCondFormulaValueSchema, - packedRoleCondFormulaValueUserSettingBooleanSchema, - packedRoleLiteSchema, - packedRolePoliciesSchema, - packedRoleSchema, -} from '@/models/json-schema/role.js'; -import { packedAdSchema } from '@/models/json-schema/ad.js'; -import { packedReversiGameDetailedSchema, packedReversiGameLiteSchema } from '@/models/json-schema/reversi-game.js'; -import { - packedMetaDetailedOnlySchema, - packedMetaDetailedSchema, - packedMetaLiteSchema, -} from '@/models/json-schema/meta.js'; -import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js'; -import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js'; -import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessageLiteForRoomSchema, packedChatMessageLiteFor1on1Schema } from '@/models/json-schema/chat-message.js'; -import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js'; -import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js'; -import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js'; -import { packedAchievementNameSchema, packedAchievementSchema } from '@/models/json-schema/achievement.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -83,10 +40,6 @@ export const refs = { User: packedUserSchema, UserList: packedUserListSchema, - Achievement: packedAchievementSchema, - AchievementName: packedAchievementNameSchema, - Ad: packedAdSchema, - Announcement: packedAnnouncementSchema, App: packedAppSchema, Note: packedNoteSchema, NoteReaction: packedNoteReactionSchema, @@ -99,54 +52,20 @@ export const refs = { RenoteMuting: packedRenoteMutingSchema, Blocking: packedBlockingSchema, Hashtag: packedHashtagSchema, - InviteCode: packedInviteCodeSchema, Page: packedPageSchema, - PageBlock: packedPageBlockSchema, Channel: packedChannelSchema, QueueCount: packedQueueCountSchema, - QueueMetrics: packedQueueMetricsSchema, - QueueJob: packedQueueJobSchema, Antenna: packedAntennaSchema, Clip: packedClipSchema, FederationInstance: packedFederationInstanceSchema, GalleryPost: packedGalleryPostSchema, EmojiSimple: packedEmojiSimpleSchema, EmojiDetailed: packedEmojiDetailedSchema, - EmojiDetailedAdmin: packedEmojiDetailedAdminSchema, Flash: packedFlashSchema, - Signin: packedSigninSchema, - RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema, - RoleCondFormulaValueNot: packedRoleCondFormulaValueNot, - RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema, - RoleCondFormulaValueUserSettingBooleanSchema: packedRoleCondFormulaValueUserSettingBooleanSchema, - RoleCondFormulaValueAssignedRole: packedRoleCondFormulaValueAssignedRoleSchema, - RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema, - RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema, - RoleCondFormulaValue: packedRoleCondFormulaValueSchema, - RoleLite: packedRoleLiteSchema, - Role: packedRoleSchema, - RolePolicies: packedRolePoliciesSchema, - ReversiGameLite: packedReversiGameLiteSchema, - ReversiGameDetailed: packedReversiGameDetailedSchema, - MetaLite: packedMetaLiteSchema, - MetaDetailedOnly: packedMetaDetailedOnlySchema, - MetaDetailed: packedMetaDetailedSchema, - SystemWebhook: packedSystemWebhookSchema, - AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema, - ChatMessage: packedChatMessageSchema, - ChatMessageLite: packedChatMessageLiteSchema, - ChatMessageLiteFor1on1: packedChatMessageLiteFor1on1Schema, - ChatMessageLiteForRoom: packedChatMessageLiteForRoomSchema, - ChatRoom: packedChatRoomSchema, - ChatRoomInvitation: packedChatRoomInvitationSchema, - ChatRoomMembership: packedChatRoomMembershipSchema, }; export type Packed = SchemaType; -export type KeyOf = PropertiesToUnion; -type PropertiesToUnion

= p['properties'] extends NonNullable ? keyof p['properties'] : never; - type TypeStringef = 'null' | 'boolean' | 'integer' | 'number' | 'string' | 'array' | 'object' | 'any'; type StringDefToType = T extends 'null' ? null : @@ -163,22 +82,19 @@ type OfSchema = { readonly anyOf?: ReadonlyArray; readonly oneOf?: ReadonlyArray; readonly allOf?: ReadonlyArray; -}; +} export interface Schema extends OfSchema { readonly type?: TypeStringef; readonly nullable?: boolean; readonly optional?: boolean; - readonly prefixItems?: ReadonlyArray; readonly items?: Schema; - readonly unevaluatedItems?: Schema | boolean; readonly properties?: Obj; readonly required?: ReadonlyArray, string>>; readonly description?: string; readonly example?: any; readonly format?: string; readonly ref?: keyof typeof refs; - readonly selfRef?: boolean; readonly enum?: ReadonlyArray; readonly default?: (this['type'] extends TypeStringef ? StringDefToType : any) | null; readonly maxLength?: number; @@ -186,16 +102,15 @@ export interface Schema extends OfSchema { readonly maximum?: number; readonly minimum?: number; readonly pattern?: string; - readonly additionalProperties?: Schema | boolean; } type RequiredPropertyNames = { [K in keyof s]: - // K is not optional - s[K]['optional'] extends false ? K : - // K has default value - s[K]['default'] extends null | string | number | boolean | Record ? K : - never + // K is not optional + s[K]['optional'] extends false ? K : + // K has default value + s[K]['default'] extends null | string | number | boolean | Record ? K : + never }[keyof s]; export type Obj = Record; @@ -216,19 +131,9 @@ type NullOrUndefined

= | T; // https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection -// Get intersection from union +// Get intersection from union type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; - -type ArrayToIntersection> = - T extends readonly [infer Head, ...infer Tail] - ? Head extends Schema - ? Tail extends ReadonlyArray - ? Tail extends [] - ? SchemaType - : SchemaType & ArrayToIntersection - : never - : never - : never; +type PartialIntersection = Partial>; // https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552 // To get union, we use `Foo extends any ? Hoge : never` @@ -236,7 +141,6 @@ type UnionSchemaType = X //type UnionObjectSchemaType = X extends any ? ObjectSchemaType : never; type UnionObjType = a[number]> = X extends any ? ObjType : never; type ArrayUnion = T extends any ? Array : never; -type ArrayToTuple> = { [K in keyof X]: SchemaType }; type ObjectSchemaTypeDef

= p['ref'] extends keyof typeof refs ? Packed : @@ -244,18 +148,11 @@ type ObjectSchemaTypeDef

= p['anyOf'] extends ReadonlyArray ? p['anyOf'][number]['required'] extends ReadonlyArray ? UnionObjType> & ObjType> : never - : ObjType> - : - p['anyOf'] extends ReadonlyArray ? UnionSchemaType : - p['allOf'] extends ReadonlyArray ? ArrayToIntersection : - p['additionalProperties'] extends true ? Record : - p['additionalProperties'] extends Schema ? - p['additionalProperties'] extends infer AdditionalProperties ? - AdditionalProperties extends Schema ? - Record> : - never : - never : - any; + : ObjType> + : + p['anyOf'] extends ReadonlyArray ? never : // see CONTRIBUTING.md + p['allOf'] extends ReadonlyArray ? UnionToIntersection> : + any type ObjectSchemaType

= NullOrUndefined>; @@ -265,31 +162,24 @@ export type SchemaTypeDef

= p['type'] extends 'number' ? number : p['type'] extends 'string' ? ( p['enum'] extends readonly (string | null)[] ? - p['enum'][number] : - p['format'] extends 'date-time' ? string : // Dateにする?? - string + p['enum'][number] : + p['format'] extends 'date-time' ? string : // Dateにする?? + string ) : - p['type'] extends 'boolean' ? boolean : - p['type'] extends 'object' ? ObjectSchemaTypeDef

: - p['type'] extends 'array' ? ( - p['items'] extends OfSchema ? ( - p['items']['anyOf'] extends ReadonlyArray ? UnionSchemaType>[] : - p['items']['oneOf'] extends ReadonlyArray ? ArrayUnion>> : - p['items']['allOf'] extends ReadonlyArray ? UnionToIntersection>>[] : - never - ) : - p['prefixItems'] extends ReadonlyArray ? ( - p['items'] extends NonNullable ? [...ArrayToTuple, ...SchemaType[]] : - p['items'] extends false ? ArrayToTuple : - p['unevaluatedItems'] extends false ? ArrayToTuple : - [...ArrayToTuple, ...unknown[]] - ) : - p['items'] extends NonNullable ? SchemaType[] : - any[] + p['type'] extends 'boolean' ? boolean : + p['type'] extends 'object' ? ObjectSchemaTypeDef

: + p['type'] extends 'array' ? ( + p['items'] extends OfSchema ? ( + p['items']['anyOf'] extends ReadonlyArray ? UnionSchemaType>[] : + p['items']['oneOf'] extends ReadonlyArray ? ArrayUnion>> : + p['items']['allOf'] extends ReadonlyArray ? UnionToIntersection>>[] : + never ) : - p['anyOf'] extends ReadonlyArray ? UnionSchemaType : - p['allOf'] extends ReadonlyArray ? ArrayToIntersection : - p['oneOf'] extends ReadonlyArray ? UnionSchemaType : - any; + p['items'] extends NonNullable ? SchemaTypeDef[] : + any[] + ) : + p['anyOf'] extends ReadonlyArray ? UnionSchemaType & PartialIntersection> : + p['oneOf'] extends ReadonlyArray ? UnionSchemaType : + any; export type SchemaType

= NullOrUndefined>; diff --git a/packages/backend/src/misc/json-value.ts b/packages/backend/src/misc/json-value.ts deleted file mode 100644 index 195f7c4d47..0000000000 --- a/packages/backend/src/misc/json-value.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export type JsonValue = JsonArray | JsonObject | string | number | boolean | null; -export type JsonObject = { [K in string]?: JsonValue }; -export type JsonArray = JsonValue[]; - -export function isJsonObject(value: JsonValue | undefined): value is JsonObject { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} diff --git a/packages/backend/src/misc/langmap.ts b/packages/backend/src/misc/langmap.ts index 5ff9338651..5ee85e6c09 100644 --- a/packages/backend/src/misc/langmap.ts +++ b/packages/backend/src/misc/langmap.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - // TODO: sharedに置いてフロントエンドのと統合したい export const langmap = { 'ach': { diff --git a/packages/backend/src/misc/loader.ts b/packages/backend/src/misc/loader.ts deleted file mode 100644 index 7f29b9db10..0000000000 --- a/packages/backend/src/misc/loader.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export type FetchFunction = (key: K) => Promise; - -type ResolveReject = Parameters>[0]>; - -type ResolverPair = { - resolve: ResolveReject[0]; - reject: ResolveReject[1]; -}; - -export class DebounceLoader { - private resolverMap = new Map>(); - private promiseMap = new Map>(); - private resolvedPromise = Promise.resolve(); - constructor(private loadFn: FetchFunction) {} - - public load(key: K): Promise { - const promise = this.promiseMap.get(key); - if (typeof promise !== 'undefined') { - return promise; - } - - const isFirst = this.promiseMap.size === 0; - const newPromise = new Promise((resolve, reject) => { - this.resolverMap.set(key, { resolve, reject }); - }); - this.promiseMap.set(key, newPromise); - - if (isFirst) { - this.enqueueDebouncedLoadJob(); - } - - return newPromise; - } - - private runDebouncedLoad(): void { - const resolvers = [...this.resolverMap]; - this.resolverMap.clear(); - this.promiseMap.clear(); - - for (const [key, { resolve, reject }] of resolvers) { - this.loadFn(key).then(resolve, reject); - } - } - - private enqueueDebouncedLoadJob(): void { - this.resolvedPromise.then(() => { - process.nextTick(() => { - this.runDebouncedLoad(); - }); - }); - } -} diff --git a/packages/backend/src/misc/normalize-for-search.ts b/packages/backend/src/misc/normalize-for-search.ts index 3f19617e14..200540566e 100644 --- a/packages/backend/src/misc/normalize-for-search.ts +++ b/packages/backend/src/misc/normalize-for-search.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export function normalizeForSearch(tag: string): string { // ref. // - https://analytics-note.xyz/programming/unicode-normalization-forms/ diff --git a/packages/backend/src/misc/nyaize.ts b/packages/backend/src/misc/nyaize.ts new file mode 100644 index 0000000000..350f8d2172 --- /dev/null +++ b/packages/backend/src/misc/nyaize.ts @@ -0,0 +1,15 @@ +export function nyaize(text: string): string { + return text + // ja-JP + .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), + )) + .replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥') + .replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥'); +} diff --git a/packages/backend/src/misc/prelude/array.ts b/packages/backend/src/misc/prelude/array.ts index f741a0c913..0b2830cb7b 100644 --- a/packages/backend/src/misc/prelude/array.ts +++ b/packages/backend/src/misc/prelude/array.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { EndoRelation, Predicate } from './relation.js'; /** @@ -65,6 +60,43 @@ export function maximum(xs: number[]): number { return Math.max(...xs); } +/** + * Splits an array based on the equivalence relation. + * The concatenation of the result is equal to the argument. + */ +export function groupBy(f: EndoRelation, xs: T[]): T[][] { + const groups = [] as T[][]; + for (const x of xs) { + if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) { + groups[groups.length - 1].push(x); + } else { + groups.push([x]); + } + } + return groups; +} + +/** + * Splits an array based on the equivalence relation induced by the function. + * The concatenation of the result is equal to the argument. + */ +export function groupOn(f: (x: T) => S, xs: T[]): T[][] { + return groupBy((a, b) => f(a) === f(b), xs); +} + +export function groupByX(collections: T[], keySelector: (x: T) => string) { + return collections.reduce((obj: Record, item: T) => { + const key = keySelector(item); + if (!Object.prototype.hasOwnProperty.call(obj, key)) { + obj[key] = []; + } + + obj[key].push(item); + + return obj; + }, {}); +} + /** * Compare two arrays by lexicographical order */ diff --git a/packages/backend/src/misc/prelude/await-all.ts b/packages/backend/src/misc/prelude/await-all.ts index 48249fe1ae..b955c3a5d8 100644 --- a/packages/backend/src/misc/prelude/await-all.ts +++ b/packages/backend/src/misc/prelude/await-all.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export type Promiseable = { [K in keyof T]: Promise | T[K]; }; @@ -15,7 +10,7 @@ export async function awaitAll(obj: Promiseable): Promise { const resolvedValues = await Promise.all(values.map(value => (!value || !value.constructor || value.constructor.name !== 'Object') ? value - : awaitAll(value), + : awaitAll(value) )); for (let i = 0; i < keys.length; i++) { diff --git a/packages/backend/src/misc/prelude/math.ts b/packages/backend/src/misc/prelude/math.ts new file mode 100644 index 0000000000..07b94bec30 --- /dev/null +++ b/packages/backend/src/misc/prelude/math.ts @@ -0,0 +1,3 @@ +export function gcd(a: number, b: number): number { + return b === 0 ? a : gcd(b, a % b); +} diff --git a/packages/backend/src/misc/prelude/maybe.ts b/packages/backend/src/misc/prelude/maybe.ts new file mode 100644 index 0000000000..df7c4ed52a --- /dev/null +++ b/packages/backend/src/misc/prelude/maybe.ts @@ -0,0 +1,20 @@ +export interface IMaybe { + isJust(): this is IJust; +} + +export interface IJust extends IMaybe { + get(): T; +} + +export function just(value: T): IJust { + return { + isJust: () => true, + get: () => value, + }; +} + +export function nothing(): IMaybe { + return { + isJust: () => false, + }; +} diff --git a/packages/backend/src/misc/prelude/relation.ts b/packages/backend/src/misc/prelude/relation.ts index 7dcd4c700a..1f4703f52f 100644 --- a/packages/backend/src/misc/prelude/relation.ts +++ b/packages/backend/src/misc/prelude/relation.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export type Predicate = (a: T) => boolean; export type Relation = (a: T, b: U) => boolean; diff --git a/packages/backend/src/misc/prelude/string.ts b/packages/backend/src/misc/prelude/string.ts new file mode 100644 index 0000000000..b907e0a2e1 --- /dev/null +++ b/packages/backend/src/misc/prelude/string.ts @@ -0,0 +1,15 @@ +export function concat(xs: string[]): string { + return xs.join(''); +} + +export function capitalize(s: string): string { + return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1)); +} + +export function toUpperCase(s: string): string { + return s.toUpperCase(); +} + +export function toLowerCase(s: string): string { + return s.toLowerCase(); +} diff --git a/packages/backend/src/misc/prelude/symbol.ts b/packages/backend/src/misc/prelude/symbol.ts new file mode 100644 index 0000000000..51e12f7450 --- /dev/null +++ b/packages/backend/src/misc/prelude/symbol.ts @@ -0,0 +1 @@ +export const fallback = Symbol('fallback'); diff --git a/packages/backend/src/misc/prelude/time.ts b/packages/backend/src/misc/prelude/time.ts index 275b67ed00..b21978b186 100644 --- a/packages/backend/src/misc/prelude/time.ts +++ b/packages/backend/src/misc/prelude/time.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - const dateTimeIntervals = { 'day': 86400000, 'hour': 3600000, diff --git a/packages/backend/src/misc/prelude/url.ts b/packages/backend/src/misc/prelude/url.ts index 270a075075..9b1dabc789 100644 --- a/packages/backend/src/misc/prelude/url.ts +++ b/packages/backend/src/misc/prelude/url.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - /* objを検査して * 1. 配列に何も入っていない時はクエリを付けない * 2. プロパティがundefinedの時はクエリを付けない * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) - */ + */ export function query(obj: Record): string { const params = Object.entries(obj) .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) diff --git a/packages/backend/src/misc/prelude/xml.ts b/packages/backend/src/misc/prelude/xml.ts index 61c166cee5..b4469a1d8d 100644 --- a/packages/backend/src/misc/prelude/xml.ts +++ b/packages/backend/src/misc/prelude/xml.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - const map: Record = { '&': '&', '<': '<', diff --git a/packages/backend/src/misc/promise-tracker.ts b/packages/backend/src/misc/promise-tracker.ts deleted file mode 100644 index 8a52ca703e..0000000000 --- a/packages/backend/src/misc/promise-tracker.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -const promiseRefs: Set>> = new Set(); - -/** - * This tracks promises that other modules decided not to wait for, - * and makes sure they are all settled before fully closing down the server. - */ -export function trackPromise(promise: Promise) { - if (process.env.NODE_ENV !== 'test') { - return; - } - const ref = new WeakRef(promise); - promiseRefs.add(ref); - promise.finally(() => promiseRefs.delete(ref)); -} - -export async function allSettled(): Promise { - await Promise.allSettled([...promiseRefs].map(r => r.deref())); -} diff --git a/packages/backend/src/misc/reset-db.ts b/packages/backend/src/misc/reset-db.ts index 75fb4c3e7b..835cd2ba28 100644 --- a/packages/backend/src/misc/reset-db.ts +++ b/packages/backend/src/misc/reset-db.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import type { DataSource } from 'typeorm'; export async function resetDb(db: DataSource) { diff --git a/packages/backend/src/misc/safe-for-sql.ts b/packages/backend/src/misc/safe-for-sql.ts index ac4b8e2e2e..02eb7f0a26 100644 --- a/packages/backend/src/misc/safe-for-sql.ts +++ b/packages/backend/src/misc/safe-for-sql.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export function safeForSql(text: string): boolean { return !/[\0\x08\x09\x1a\n\r"'\\\%]/g.test(text); } diff --git a/packages/backend/src/misc/secure-rndstr.ts b/packages/backend/src/misc/secure-rndstr.ts index 7853100d89..cde64c8142 100644 --- a/packages/backend/src/misc/secure-rndstr.ts +++ b/packages/backend/src/misc/secure-rndstr.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as crypto from 'node:crypto'; export const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'; diff --git a/packages/backend/src/misc/show-machine-info.ts b/packages/backend/src/misc/show-machine-info.ts index 8ddec35f23..fa5a53e313 100644 --- a/packages/backend/src/misc/show-machine-info.ts +++ b/packages/backend/src/misc/show-machine-info.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as os from 'node:os'; import sysUtils from 'systeminformation'; import type Logger from '@/logger.js'; diff --git a/packages/backend/src/misc/sql-like-escape.ts b/packages/backend/src/misc/sql-like-escape.ts index 6b4f51b00e..8470dca3de 100644 --- a/packages/backend/src/misc/sql-like-escape.ts +++ b/packages/backend/src/misc/sql-like-escape.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export function sqlLikeEscape(s: string) { - return s.replace(/([\\%_])/g, '\\$1'); + return s.replace(/([%_])/g, '\\$1'); } diff --git a/packages/backend/src/misc/status-error.ts b/packages/backend/src/misc/status-error.ts index c3533db607..0a33f8acaf 100644 --- a/packages/backend/src/misc/status-error.ts +++ b/packages/backend/src/misc/status-error.ts @@ -1,13 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class StatusError extends Error { public statusCode: number; public statusMessage?: string; public isClientError: boolean; - public isRetryable: boolean; constructor(message: string, statusCode: number, statusMessage?: string) { super(message); @@ -15,6 +9,5 @@ export class StatusError extends Error { this.statusCode = statusCode; this.statusMessage = statusMessage; this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500; - this.isRetryable = !this.isClientError || this.statusCode === 429; } } diff --git a/packages/backend/src/misc/token.ts b/packages/backend/src/misc/token.ts deleted file mode 100644 index 5d37cba26d..0000000000 --- a/packages/backend/src/misc/token.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { secureRndstr } from '@/misc/secure-rndstr.js'; - -export const generateNativeUserToken = () => secureRndstr(16); - -export const isNativeUserToken = (token: string) => token.length === 16; diff --git a/packages/backend/src/misc/truncate.ts b/packages/backend/src/misc/truncate.ts index 1c8a274609..cb120331a1 100644 --- a/packages/backend/src/misc/truncate.ts +++ b/packages/backend/src/misc/truncate.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { substring } from 'stringz'; export function truncate(input: string, size: number): string; diff --git a/packages/backend/src/models/AbuseReportNotificationRecipient.ts b/packages/backend/src/models/AbuseReportNotificationRecipient.ts deleted file mode 100644 index fbff880afc..0000000000 --- a/packages/backend/src/models/AbuseReportNotificationRecipient.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; -import { MiSystemWebhook } from '@/models/SystemWebhook.js'; -import { MiUserProfile } from '@/models/UserProfile.js'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -/** - * 通報受信時に通知を送信する方法. - */ -export type RecipientMethod = 'email' | 'webhook'; - -@Entity('abuse_report_notification_recipient') -export class MiAbuseReportNotificationRecipient { - @PrimaryColumn(id()) - public id: string; - - /** - * 有効かどうか. - */ - @Index() - @Column('boolean', { - default: true, - }) - public isActive: boolean; - - /** - * 更新日時. - */ - @Column('timestamp with time zone', { - default: () => 'CURRENT_TIMESTAMP', - }) - public updatedAt: Date; - - /** - * 通知設定名. - */ - @Column('varchar', { - length: 255, - }) - public name: string; - - /** - * 通知方法. - */ - @Index() - @Column('varchar', { - length: 64, - }) - public method: RecipientMethod; - - /** - * 通知先のユーザID. - */ - @Index() - @Column({ - ...id(), - nullable: true, - }) - public userId: MiUser['id'] | null; - - /** - * 通知先のユーザ. - */ - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn({ name: 'userId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId1' }) - public user: MiUser | null; - - /** - * 通知先のユーザプロフィール. - */ - @ManyToOne(type => MiUserProfile, {}) - @JoinColumn({ name: 'userId', referencedColumnName: 'userId', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId2' }) - public userProfile: MiUserProfile | null; - - /** - * 通知先のシステムWebhookId. - */ - @Index() - @Column({ - ...id(), - nullable: true, - }) - public systemWebhookId: string | null; - - /** - * 通知先のシステムWebhook. - */ - @ManyToOne(type => MiSystemWebhook, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public systemWebhook: MiSystemWebhook | null; -} diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts deleted file mode 100644 index d43ebf9342..0000000000 --- a/packages/backend/src/models/AbuseUserReport.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -export type AbuseReportResolveType = 'accept' | 'reject'; - -@Entity('abuse_user_report') -export class MiAbuseUserReport { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public targetUserId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public targetUser: MiUser | null; - - @Index() - @Column(id()) - public reporterId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public reporter: MiUser | null; - - @Column({ - ...id(), - nullable: true, - }) - public assigneeId: MiUser['id'] | null; - - @ManyToOne(type => MiUser, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public assignee: MiUser | null; - - @Index() - @Column('boolean', { - default: false, - }) - public resolved: boolean; - - /** - * リモートサーバーに転送したかどうか - */ - @Column('boolean', { - default: false, - }) - public forwarded: boolean; - - @Column('varchar', { - length: 2048, - }) - public comment: string; - - @Column('varchar', { - length: 8192, default: '', - }) - public moderationNote: string; - - /** - * accept 是認 ... 通報内容が正当であり、肯定的に対応された - * reject 否認 ... 通報内容が正当でなく、否定的に対応された - * null ... その他 - */ - @Column('varchar', { - length: 128, nullable: true, - }) - public resolvedAs: AbuseReportResolveType | null; - - //#region Denormalized fields - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public targetUserHost: string | null; - - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public reporterHost: string | null; - //#endregion -} diff --git a/packages/backend/src/models/Announcement.ts b/packages/backend/src/models/Announcement.ts deleted file mode 100644 index d0c59fff50..0000000000 --- a/packages/backend/src/models/Announcement.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Entity, Index, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -@Entity('announcement') -export class MiAnnouncement { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The updated date of the Announcement.', - nullable: true, - }) - public updatedAt: Date | null; - - @Column('varchar', { - length: 8192, nullable: false, - }) - public text: string; - - @Column('varchar', { - length: 256, nullable: false, - }) - public title: string; - - @Column('varchar', { - length: 1024, nullable: true, - }) - public imageUrl: string | null; - - // info, warning, error, success - @Column('varchar', { - length: 256, nullable: false, - default: 'info', - }) - public icon: 'info' | 'warning' | 'error' | 'success'; - - // normal ... お知らせページ掲載 - // banner ... お知らせページ掲載 + バナー表示 - // dialog ... お知らせページ掲載 + ダイアログ表示 - @Column('varchar', { - length: 256, nullable: false, - default: 'normal', - }) - public display: 'normal' | 'banner' | 'dialog'; - - @Column('boolean', { - default: false, - }) - public needConfirmationToRead: boolean; - - @Index() - @Column('boolean', { - default: true, - }) - public isActive: boolean; - - @Index() - @Column('boolean', { - default: false, - }) - public forExistingUsers: boolean; - - @Index() - @Column('boolean', { - default: false, - }) - public silence: boolean; - - @Index() - @Column({ - ...id(), - nullable: true, - }) - public userId: MiUser['id'] | null; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/AnnouncementRead.ts b/packages/backend/src/models/AnnouncementRead.ts deleted file mode 100644 index 47de8dd180..0000000000 --- a/packages/backend/src/models/AnnouncementRead.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiAnnouncement } from './Announcement.js'; - -@Entity('announcement_read') -@Index(['userId', 'announcementId'], { unique: true }) -export class MiAnnouncementRead { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Index() - @Column(id()) - public announcementId: MiAnnouncement['id']; - - @ManyToOne(type => MiAnnouncement, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public announcement: MiAnnouncement | null; -} diff --git a/packages/backend/src/models/AuthSession.ts b/packages/backend/src/models/AuthSession.ts deleted file mode 100644 index 03050ba955..0000000000 --- a/packages/backend/src/models/AuthSession.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiApp } from './App.js'; - -@Entity('auth_session') -export class MiAuthSession { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('varchar', { - length: 128, - }) - public token: string; - - @Column({ - ...id(), - nullable: true, - }) - public userId: MiUser['id'] | null; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - nullable: true, - }) - @JoinColumn() - public user: MiUser | null; - - @Column(id()) - public appId: MiApp['id']; - - @ManyToOne(type => MiApp, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public app: MiApp | null; -} diff --git a/packages/backend/src/models/AvatarDecoration.ts b/packages/backend/src/models/AvatarDecoration.ts deleted file mode 100644 index 13f0b05667..0000000000 --- a/packages/backend/src/models/AvatarDecoration.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from './util/id.js'; - -@Entity('avatar_decoration') -export class MiAvatarDecoration { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - nullable: true, - }) - public updatedAt: Date | null; - - @Column('varchar', { - length: 1024, - }) - public url: string; - - @Column('varchar', { - length: 256, - }) - public name: string; - - @Column('varchar', { - length: 2048, - }) - public description: string; - - // TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする - @Column('varchar', { - array: true, length: 128, default: '{}', - }) - public roleIdsThatCanBeUsedThisDecoration: string[]; -} diff --git a/packages/backend/src/models/BubbleGameRecord.ts b/packages/backend/src/models/BubbleGameRecord.ts deleted file mode 100644 index 686e39c118..0000000000 --- a/packages/backend/src/models/BubbleGameRecord.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -@Entity('bubble_game_record') -export class MiBubbleGameRecord { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - }) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Index() - @Column('timestamp with time zone') - public seededAt: Date; - - @Column('varchar', { - length: 1024, - }) - public seed: string; - - @Column('integer') - public gameVersion: number; - - @Column('varchar', { - length: 128, - }) - public gameMode: string; - - @Index() - @Column('integer') - public score: number; - - @Column('jsonb', { - default: [], - }) - public logs: number[][]; - - @Column('boolean', { - default: false, - }) - public isVerified: boolean; -} diff --git a/packages/backend/src/models/ChannelFavorite.ts b/packages/backend/src/models/ChannelFavorite.ts deleted file mode 100644 index 167f41cf16..0000000000 --- a/packages/backend/src/models/ChannelFavorite.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiChannel } from './Channel.js'; - -@Entity('channel_favorite') -@Index(['userId', 'channelId'], { unique: true }) -export class MiChannelFavorite { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - }) - public channelId: MiChannel['id']; - - @ManyToOne(type => MiChannel, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public channel: MiChannel | null; - - @Index() - @Column({ - ...id(), - }) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; -} diff --git a/packages/backend/src/models/ChannelFollowing.ts b/packages/backend/src/models/ChannelFollowing.ts deleted file mode 100644 index c7afdd05b0..0000000000 --- a/packages/backend/src/models/ChannelFollowing.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiChannel } from './Channel.js'; - -@Entity('channel_following') -@Index(['followerId', 'followeeId'], { unique: true }) -export class MiChannelFollowing { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: 'The followee channel ID.', - }) - public followeeId: MiChannel['id']; - - @ManyToOne(type => MiChannel, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public followee: MiChannel | null; - - @Index() - @Column({ - ...id(), - comment: 'The follower user ID.', - }) - public followerId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public follower: MiUser | null; -} diff --git a/packages/backend/src/models/ChatApproval.ts b/packages/backend/src/models/ChatApproval.ts deleted file mode 100644 index 55c9f07e9a..0000000000 --- a/packages/backend/src/models/ChatApproval.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -@Entity('chat_approval') -@Index(['userId', 'otherId'], { unique: true }) -export class MiChatApproval { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - }) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Index() - @Column({ - ...id(), - }) - public otherId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public other: MiUser | null; -} diff --git a/packages/backend/src/models/ChatMessage.ts b/packages/backend/src/models/ChatMessage.ts deleted file mode 100644 index 3d2b64268e..0000000000 --- a/packages/backend/src/models/ChatMessage.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiDriveFile } from './DriveFile.js'; -import { MiChatRoom } from './ChatRoom.js'; - -@Entity('chat_message') -export class MiChatMessage { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - }) - public fromUserId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public fromUser: MiUser | null; - - @Index() - @Column({ - ...id(), nullable: true, - }) - public toUserId: MiUser['id'] | null; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public toUser: MiUser | null; - - @Index() - @Column({ - ...id(), nullable: true, - }) - public toRoomId: MiChatRoom['id'] | null; - - @ManyToOne(type => MiChatRoom, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public toRoom: MiChatRoom | null; - - @Column('varchar', { - length: 4096, nullable: true, - }) - public text: string | null; - - @Column('varchar', { - length: 512, nullable: true, - }) - public uri: string | null; - - @Column({ - ...id(), - array: true, default: '{}', - }) - public reads: MiUser['id'][]; - - @Column({ - ...id(), - nullable: true, - }) - public fileId: MiDriveFile['id'] | null; - - @ManyToOne(type => MiDriveFile, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public file: MiDriveFile | null; - - @Column('varchar', { - length: 1024, array: true, default: '{}', - }) - public reactions: string[]; -} diff --git a/packages/backend/src/models/ChatRoom.ts b/packages/backend/src/models/ChatRoom.ts deleted file mode 100644 index ad2a910b78..0000000000 --- a/packages/backend/src/models/ChatRoom.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -@Entity('chat_room') -export class MiChatRoom { - @PrimaryColumn(id()) - public id: string; - - @Column('varchar', { - length: 256, - }) - public name: string; - - @Index() - @Column({ - ...id(), - }) - public ownerId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public owner: MiUser | null; - - @Column('varchar', { - length: 2048, default: '', - }) - public description: string; - - @Column('boolean', { - default: false, - }) - public isArchived: boolean; -} diff --git a/packages/backend/src/models/ChatRoomInvitation.ts b/packages/backend/src/models/ChatRoomInvitation.ts deleted file mode 100644 index 36ce12bc92..0000000000 --- a/packages/backend/src/models/ChatRoomInvitation.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiChatRoom } from './ChatRoom.js'; - -@Entity('chat_room_invitation') -@Index(['userId', 'roomId'], { unique: true }) -export class MiChatRoomInvitation { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - }) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Index() - @Column({ - ...id(), - }) - public roomId: MiChatRoom['id']; - - @ManyToOne(type => MiChatRoom, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public room: MiChatRoom | null; - - @Column('boolean', { - default: false, - }) - public ignored: boolean; -} diff --git a/packages/backend/src/models/ChatRoomMembership.ts b/packages/backend/src/models/ChatRoomMembership.ts deleted file mode 100644 index 3cb5524859..0000000000 --- a/packages/backend/src/models/ChatRoomMembership.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiChatRoom } from './ChatRoom.js'; - -@Entity('chat_room_membership') -@Index(['userId', 'roomId'], { unique: true }) -export class MiChatRoomMembership { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - }) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Index() - @Column({ - ...id(), - }) - public roomId: MiChatRoom['id']; - - @ManyToOne(type => MiChatRoom, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public room: MiChatRoom | null; - - @Column('boolean', { - default: false, - }) - public isMuted: boolean; -} diff --git a/packages/backend/src/models/ClipFavorite.ts b/packages/backend/src/models/ClipFavorite.ts deleted file mode 100644 index 40bdb9f4aa..0000000000 --- a/packages/backend/src/models/ClipFavorite.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiClip } from './Clip.js'; - -@Entity('clip_favorite') -@Index(['userId', 'clipId'], { unique: true }) -export class MiClipFavorite { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Column(id()) - public clipId: MiClip['id']; - - @ManyToOne(type => MiClip, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public clip: MiClip | null; -} diff --git a/packages/backend/src/models/ClipNote.ts b/packages/backend/src/models/ClipNote.ts deleted file mode 100644 index 6e1d2bec4c..0000000000 --- a/packages/backend/src/models/ClipNote.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; -import { id } from './util/id.js'; -import { MiNote } from './Note.js'; -import { MiClip } from './Clip.js'; - -@Entity('clip_note') -@Index(['noteId', 'clipId'], { unique: true }) -export class MiClipNote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: 'The note ID.', - }) - public noteId: MiNote['id']; - - @ManyToOne(type => MiNote, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: MiNote | null; - - @Index() - @Column({ - ...id(), - comment: 'The clip ID.', - }) - public clipId: MiClip['id']; - - @ManyToOne(type => MiClip, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public clip: MiClip | null; -} diff --git a/packages/backend/src/models/FlashLike.ts b/packages/backend/src/models/FlashLike.ts deleted file mode 100644 index a9fb48123e..0000000000 --- a/packages/backend/src/models/FlashLike.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiFlash } from './Flash.js'; - -@Entity('flash_like') -@Index(['userId', 'flashId'], { unique: true }) -export class MiFlashLike { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Column(id()) - public flashId: MiFlash['id']; - - @ManyToOne(type => MiFlash, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public flash: MiFlash | null; -} diff --git a/packages/backend/src/models/GalleryLike.ts b/packages/backend/src/models/GalleryLike.ts deleted file mode 100644 index ed0963122d..0000000000 --- a/packages/backend/src/models/GalleryLike.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiGalleryPost } from './GalleryPost.js'; - -@Entity('gallery_like') -@Index(['userId', 'postId'], { unique: true }) -export class MiGalleryLike { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Column(id()) - public postId: MiGalleryPost['id']; - - @ManyToOne(type => MiGalleryPost, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public post: MiGalleryPost | null; -} diff --git a/packages/backend/src/models/NoteFavorite.ts b/packages/backend/src/models/NoteFavorite.ts deleted file mode 100644 index cf76c767b0..0000000000 --- a/packages/backend/src/models/NoteFavorite.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiNote } from './Note.js'; -import { MiUser } from './User.js'; - -@Entity('note_favorite') -@Index(['userId', 'noteId'], { unique: true }) -export class MiNoteFavorite { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Column(id()) - public noteId: MiNote['id']; - - @ManyToOne(type => MiNote, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: MiNote | null; -} diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts deleted file mode 100644 index 5764a307b0..0000000000 --- a/packages/backend/src/models/Notification.ts +++ /dev/null @@ -1,150 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { userExportableEntities } from '@/types.js'; -import { MiUser } from './User.js'; -import { MiNote } from './Note.js'; -import { MiAccessToken } from './AccessToken.js'; -import { MiRole } from './Role.js'; -import { MiDriveFile } from './DriveFile.js'; - -export type MiNotification = { - type: 'note'; - id: string; - createdAt: string; - notifierId: MiUser['id']; - noteId: MiNote['id']; -} | { - type: 'follow'; - id: string; - createdAt: string; - notifierId: MiUser['id']; -} | { - type: 'mention'; - id: string; - createdAt: string; - notifierId: MiUser['id']; - noteId: MiNote['id']; -} | { - type: 'reply'; - id: string; - createdAt: string; - notifierId: MiUser['id']; - noteId: MiNote['id']; -} | { - type: 'renote'; - id: string; - createdAt: string; - notifierId: MiUser['id']; - noteId: MiNote['id']; - targetNoteId: MiNote['id']; -} | { - type: 'quote'; - id: string; - createdAt: string; - notifierId: MiUser['id']; - noteId: MiNote['id']; -} | { - type: 'reaction'; - id: string; - createdAt: string; - notifierId: MiUser['id']; - noteId: MiNote['id']; - reaction: string; -} | { - type: 'pollEnded'; - id: string; - createdAt: string; - notifierId: MiUser['id']; - noteId: MiNote['id']; -} | { - type: 'receiveFollowRequest'; - id: string; - createdAt: string; - notifierId: MiUser['id']; -} | { - type: 'followRequestAccepted'; - id: string; - createdAt: string; - notifierId: MiUser['id']; - message: string | null; -} | { - type: 'roleAssigned'; - id: string; - createdAt: string; - roleId: MiRole['id']; -} | { - type: 'chatRoomInvitationReceived'; - id: string; - createdAt: string; - notifierId: MiUser['id']; - invitationId: string; -} | { - type: 'achievementEarned'; - id: string; - createdAt: string; - achievement: string; -} | { - type: 'exportCompleted'; - id: string; - createdAt: string; - exportedEntity: typeof userExportableEntities[number]; - fileId: MiDriveFile['id']; -} | { - type: 'login'; - id: string; - createdAt: string; -} | { - type: 'createToken'; - id: string; - createdAt: string; -} | { - type: 'app'; - id: string; - createdAt: string; - - /** - * アプリ通知のbody - */ - customBody: string; - - /** - * アプリ通知のheader - * (省略時はアプリ名で表示されることを期待) - */ - customHeader: string | null; - - /** - * アプリ通知のicon(URL) - * (省略時はアプリアイコンで表示されることを期待) - */ - customIcon: string | null; - - /** - * アプリ通知のアプリ(のトークン) - */ - appAccessTokenId: MiAccessToken['id'] | null; -} | { - type: 'test'; - id: string; - createdAt: string; -}; - -export type MiGroupedNotification = MiNotification | { - type: 'reaction:grouped'; - id: string; - createdAt: string; - noteId: MiNote['id']; - reactions: { - userId: string; - reaction: string; - }[]; -} | { - type: 'renote:grouped'; - id: string; - createdAt: string; - noteId: MiNote['id']; - userIds: string[]; -}; diff --git a/packages/backend/src/models/PageLike.ts b/packages/backend/src/models/PageLike.ts deleted file mode 100644 index 05ca22cf2c..0000000000 --- a/packages/backend/src/models/PageLike.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiPage } from './Page.js'; - -@Entity('page_like') -@Index(['userId', 'pageId'], { unique: true }) -export class MiPageLike { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Column(id()) - public pageId: MiPage['id']; - - @ManyToOne(type => MiPage, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public page: MiPage | null; -} diff --git a/packages/backend/src/models/PasswordResetRequest.ts b/packages/backend/src/models/PasswordResetRequest.ts deleted file mode 100644 index fdaf21056b..0000000000 --- a/packages/backend/src/models/PasswordResetRequest.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -@Entity('password_reset_request') -export class MiPasswordResetRequest { - @PrimaryColumn(id()) - public id: string; - - @Index({ unique: true }) - @Column('varchar', { - length: 256, - }) - public token: string; - - @Index() - @Column({ - ...id(), - }) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; -} diff --git a/packages/backend/src/models/PollVote.ts b/packages/backend/src/models/PollVote.ts deleted file mode 100644 index b5c780293c..0000000000 --- a/packages/backend/src/models/PollVote.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiNote } from './Note.js'; - -@Entity('poll_vote') -@Index(['userId', 'noteId', 'choice'], { unique: true }) -export class MiPollVote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Index() - @Column(id()) - public noteId: MiNote['id']; - - @ManyToOne(type => MiNote, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: MiNote | null; - - @Column('integer') - public choice: number; -} diff --git a/packages/backend/src/models/PromoNote.ts b/packages/backend/src/models/PromoNote.ts deleted file mode 100644 index ae27adec9e..0000000000 --- a/packages/backend/src/models/PromoNote.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiNote } from './Note.js'; -import type { MiUser } from './User.js'; - -@Entity('promo_note') -export class MiPromoNote { - @PrimaryColumn(id()) - public noteId: MiNote['id']; - - @OneToOne(type => MiNote, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: MiNote | null; - - @Column('timestamp with time zone') - public expiresAt: Date; - - //#region Denormalized fields - @Index() - @Column({ - ...id(), - comment: '[Denormalized]', - }) - public userId: MiUser['id']; - //#endregion -} diff --git a/packages/backend/src/models/PromoRead.ts b/packages/backend/src/models/PromoRead.ts deleted file mode 100644 index b2a698cc7b..0000000000 --- a/packages/backend/src/models/PromoRead.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiNote } from './Note.js'; -import { MiUser } from './User.js'; - -@Entity('promo_read') -@Index(['userId', 'noteId'], { unique: true }) -export class MiPromoRead { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Column(id()) - public noteId: MiNote['id']; - - @ManyToOne(type => MiNote, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: MiNote | null; -} diff --git a/packages/backend/src/models/RegistrationTicket.ts b/packages/backend/src/models/RegistrationTicket.ts deleted file mode 100644 index 0a4e4b9189..0000000000 --- a/packages/backend/src/models/RegistrationTicket.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn, OneToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -@Entity('registration_ticket') -export class MiRegistrationTicket { - @PrimaryColumn(id()) - public id: string; - - @Index({ unique: true }) - @Column('varchar', { - length: 64, - }) - public code: string; - - @Column('timestamp with time zone', { - nullable: true, - }) - public expiresAt: Date | null; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public createdBy: MiUser | null; - - @Index() - @Column({ - ...id(), - nullable: true, - }) - public createdById: MiUser['id'] | null; - - @OneToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public usedBy: MiUser | null; - - @Index() - @Column({ - ...id(), - nullable: true, - }) - public usedById: MiUser['id'] | null; - - @Column('timestamp with time zone', { - nullable: true, - }) - public usedAt: Date | null; - - @Column('varchar', { - length: 32, - nullable: true, - }) - public pendingUserId: string | null; -} diff --git a/packages/backend/src/models/RenoteMuting.ts b/packages/backend/src/models/RenoteMuting.ts deleted file mode 100644 index 448a0b7663..0000000000 --- a/packages/backend/src/models/RenoteMuting.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -@Entity('renote_muting') -@Index(['muterId', 'muteeId'], { unique: true }) -export class MiRenoteMuting { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: 'The mutee user ID.', - }) - public muteeId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public mutee: MiUser | null; - - @Index() - @Column({ - ...id(), - comment: 'The muter user ID.', - }) - public muterId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public muter: MiUser | null; -} diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index b7142d91bf..4231acc046 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -1,557 +1,429 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { - MiAbuseReportNotificationRecipient, - MiAbuseUserReport, - MiAccessToken, - MiAd, - MiAnnouncement, - MiAnnouncementRead, - MiAntenna, - MiApp, - MiAuthSession, - MiAvatarDecoration, - MiBlocking, - MiBubbleGameRecord, - MiChannel, - MiChannelFavorite, - MiChannelFollowing, - MiClip, - MiClipFavorite, - MiClipNote, - MiDriveFile, - MiDriveFolder, - MiEmoji, - MiFlash, - MiFlashLike, - MiFollowing, - MiFollowRequest, - MiGalleryLike, - MiGalleryPost, - MiHashtag, - MiInstance, - MiMeta, - MiModerationLog, - MiMuting, - MiNote, - MiNoteFavorite, - MiNoteReaction, - MiNoteThreadMuting, - MiPage, - MiPageLike, - MiPasswordResetRequest, - MiPoll, - MiPollVote, - MiPromoNote, - MiPromoRead, - MiRegistrationTicket, - MiRegistryItem, - MiRelay, - MiRenoteMuting, - MiRepository, - miRepository, - MiRetentionAggregation, - MiReversiGame, - MiRole, - MiRoleAssignment, - MiSignin, - MiSwSubscription, - MiSystemAccount, - MiSystemWebhook, - MiUsedUsername, - MiUser, - MiUserIp, - MiUserKeypair, - MiUserList, - MiUserListFavorite, - MiUserListMembership, - MiUserMemo, - MiUserNotePining, - MiUserPending, - MiUserProfile, - MiUserPublickey, - MiUserSecurityKey, - MiWebhook, - MiChatMessage, - MiChatRoom, - MiChatRoomMembership, - MiChatRoomInvitation, - MiChatApproval, -} from './_.js'; -import type { Provider } from '@nestjs/common'; +import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo, UserListFavorite } from './index.js'; import type { DataSource } from 'typeorm'; +import type { Provider } from '@nestjs/common'; const $usersRepository: Provider = { provide: DI.usersRepository, - useFactory: (db: DataSource) => db.getRepository(MiUser).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(User), inject: [DI.db], }; const $notesRepository: Provider = { provide: DI.notesRepository, - useFactory: (db: DataSource) => db.getRepository(MiNote).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Note), inject: [DI.db], }; const $announcementsRepository: Provider = { provide: DI.announcementsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAnnouncement).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Announcement), inject: [DI.db], }; const $announcementReadsRepository: Provider = { provide: DI.announcementReadsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(AnnouncementRead), inject: [DI.db], }; const $appsRepository: Provider = { provide: DI.appsRepository, - useFactory: (db: DataSource) => db.getRepository(MiApp).extend(miRepository as MiRepository), - inject: [DI.db], -}; - -const $avatarDecorationsRepository: Provider = { - provide: DI.avatarDecorationsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(App), inject: [DI.db], }; const $noteFavoritesRepository: Provider = { provide: DI.noteFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(NoteFavorite), inject: [DI.db], }; const $noteThreadMutingsRepository: Provider = { provide: DI.noteThreadMutingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(NoteThreadMuting), inject: [DI.db], }; const $noteReactionsRepository: Provider = { provide: DI.noteReactionsRepository, - useFactory: (db: DataSource) => db.getRepository(MiNoteReaction).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(NoteReaction), + inject: [DI.db], +}; + +const $noteUnreadsRepository: Provider = { + provide: DI.noteUnreadsRepository, + useFactory: (db: DataSource) => db.getRepository(NoteUnread), inject: [DI.db], }; const $pollsRepository: Provider = { provide: DI.pollsRepository, - useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Poll), inject: [DI.db], }; const $pollVotesRepository: Provider = { provide: DI.pollVotesRepository, - useFactory: (db: DataSource) => db.getRepository(MiPollVote).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(PollVote), inject: [DI.db], }; const $userProfilesRepository: Provider = { provide: DI.userProfilesRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserProfile).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(UserProfile), inject: [DI.db], }; const $userKeypairsRepository: Provider = { provide: DI.userKeypairsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserKeypair).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(UserKeypair), inject: [DI.db], }; const $userPendingsRepository: Provider = { provide: DI.userPendingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserPending).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(UserPending), + inject: [DI.db], +}; + +const $attestationChallengesRepository: Provider = { + provide: DI.attestationChallengesRepository, + useFactory: (db: DataSource) => db.getRepository(AttestationChallenge), inject: [DI.db], }; const $userSecurityKeysRepository: Provider = { provide: DI.userSecurityKeysRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(UserSecurityKey), inject: [DI.db], }; const $userPublickeysRepository: Provider = { provide: DI.userPublickeysRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserPublickey).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(UserPublickey), inject: [DI.db], }; const $userListsRepository: Provider = { provide: DI.userListsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserList).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(UserList), inject: [DI.db], }; const $userListFavoritesRepository: Provider = { provide: DI.userListFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(UserListFavorite), inject: [DI.db], }; -const $userListMembershipsRepository: Provider = { - provide: DI.userListMembershipsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserListMembership).extend(miRepository as MiRepository), +const $userListJoiningsRepository: Provider = { + provide: DI.userListJoiningsRepository, + useFactory: (db: DataSource) => db.getRepository(UserListJoining), inject: [DI.db], }; const $userNotePiningsRepository: Provider = { provide: DI.userNotePiningsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserNotePining).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(UserNotePining), inject: [DI.db], }; const $userIpsRepository: Provider = { provide: DI.userIpsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserIp).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(UserIp), inject: [DI.db], }; const $usedUsernamesRepository: Provider = { provide: DI.usedUsernamesRepository, - useFactory: (db: DataSource) => db.getRepository(MiUsedUsername).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(UsedUsername), inject: [DI.db], }; const $followingsRepository: Provider = { provide: DI.followingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiFollowing).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Following), inject: [DI.db], }; const $followRequestsRepository: Provider = { provide: DI.followRequestsRepository, - useFactory: (db: DataSource) => db.getRepository(MiFollowRequest).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(FollowRequest), inject: [DI.db], }; const $instancesRepository: Provider = { provide: DI.instancesRepository, - useFactory: (db: DataSource) => db.getRepository(MiInstance).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Instance), inject: [DI.db], }; const $emojisRepository: Provider = { provide: DI.emojisRepository, - useFactory: (db: DataSource) => db.getRepository(MiEmoji).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Emoji), inject: [DI.db], }; const $driveFilesRepository: Provider = { provide: DI.driveFilesRepository, - useFactory: (db: DataSource) => db.getRepository(MiDriveFile).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(DriveFile), inject: [DI.db], }; const $driveFoldersRepository: Provider = { provide: DI.driveFoldersRepository, - useFactory: (db: DataSource) => db.getRepository(MiDriveFolder).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(DriveFolder), inject: [DI.db], }; const $metasRepository: Provider = { provide: DI.metasRepository, - useFactory: (db: DataSource) => db.getRepository(MiMeta).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Meta), inject: [DI.db], }; const $mutingsRepository: Provider = { provide: DI.mutingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiMuting).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Muting), inject: [DI.db], }; const $renoteMutingsRepository: Provider = { provide: DI.renoteMutingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(RenoteMuting), inject: [DI.db], }; const $blockingsRepository: Provider = { provide: DI.blockingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiBlocking).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Blocking), inject: [DI.db], }; const $swSubscriptionsRepository: Provider = { provide: DI.swSubscriptionsRepository, - useFactory: (db: DataSource) => db.getRepository(MiSwSubscription).extend(miRepository as MiRepository), - inject: [DI.db], -}; - -const $systemAccountsRepository: Provider = { - provide: DI.systemAccountsRepository, - useFactory: (db: DataSource) => db.getRepository(MiSystemAccount).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(SwSubscription), inject: [DI.db], }; const $hashtagsRepository: Provider = { provide: DI.hashtagsRepository, - useFactory: (db: DataSource) => db.getRepository(MiHashtag).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Hashtag), inject: [DI.db], }; const $abuseUserReportsRepository: Provider = { provide: DI.abuseUserReportsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport).extend(miRepository as MiRepository), - inject: [DI.db], -}; - -const $abuseReportNotificationRecipientRepository: Provider = { - provide: DI.abuseReportNotificationRecipientRepository, - useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(AbuseUserReport), inject: [DI.db], }; const $registrationTicketsRepository: Provider = { provide: DI.registrationTicketsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(RegistrationTicket), inject: [DI.db], }; const $authSessionsRepository: Provider = { provide: DI.authSessionsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAuthSession).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(AuthSession), inject: [DI.db], }; const $accessTokensRepository: Provider = { provide: DI.accessTokensRepository, - useFactory: (db: DataSource) => db.getRepository(MiAccessToken).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(AccessToken), inject: [DI.db], }; const $signinsRepository: Provider = { provide: DI.signinsRepository, - useFactory: (db: DataSource) => db.getRepository(MiSignin).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Signin), inject: [DI.db], }; const $pagesRepository: Provider = { provide: DI.pagesRepository, - useFactory: (db: DataSource) => db.getRepository(MiPage).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Page), inject: [DI.db], }; const $pageLikesRepository: Provider = { provide: DI.pageLikesRepository, - useFactory: (db: DataSource) => db.getRepository(MiPageLike).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(PageLike), inject: [DI.db], }; const $galleryPostsRepository: Provider = { provide: DI.galleryPostsRepository, - useFactory: (db: DataSource) => db.getRepository(MiGalleryPost).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(GalleryPost), inject: [DI.db], }; const $galleryLikesRepository: Provider = { provide: DI.galleryLikesRepository, - useFactory: (db: DataSource) => db.getRepository(MiGalleryLike).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(GalleryLike), inject: [DI.db], }; const $moderationLogsRepository: Provider = { provide: DI.moderationLogsRepository, - useFactory: (db: DataSource) => db.getRepository(MiModerationLog).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(ModerationLog), inject: [DI.db], }; const $clipsRepository: Provider = { provide: DI.clipsRepository, - useFactory: (db: DataSource) => db.getRepository(MiClip).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Clip), inject: [DI.db], }; const $clipNotesRepository: Provider = { provide: DI.clipNotesRepository, - useFactory: (db: DataSource) => db.getRepository(MiClipNote).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(ClipNote), inject: [DI.db], }; const $clipFavoritesRepository: Provider = { provide: DI.clipFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(MiClipFavorite).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(ClipFavorite), inject: [DI.db], }; const $antennasRepository: Provider = { provide: DI.antennasRepository, - useFactory: (db: DataSource) => db.getRepository(MiAntenna).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Antenna), inject: [DI.db], }; const $promoNotesRepository: Provider = { provide: DI.promoNotesRepository, - useFactory: (db: DataSource) => db.getRepository(MiPromoNote).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(PromoNote), inject: [DI.db], }; const $promoReadsRepository: Provider = { provide: DI.promoReadsRepository, - useFactory: (db: DataSource) => db.getRepository(MiPromoRead).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(PromoRead), inject: [DI.db], }; const $relaysRepository: Provider = { provide: DI.relaysRepository, - useFactory: (db: DataSource) => db.getRepository(MiRelay).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Relay), + inject: [DI.db], +}; + +const $mutedNotesRepository: Provider = { + provide: DI.mutedNotesRepository, + useFactory: (db: DataSource) => db.getRepository(MutedNote), inject: [DI.db], }; const $channelsRepository: Provider = { provide: DI.channelsRepository, - useFactory: (db: DataSource) => db.getRepository(MiChannel).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Channel), inject: [DI.db], }; const $channelFollowingsRepository: Provider = { provide: DI.channelFollowingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(ChannelFollowing), inject: [DI.db], }; const $channelFavoritesRepository: Provider = { provide: DI.channelFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(ChannelFavorite), inject: [DI.db], }; const $registryItemsRepository: Provider = { provide: DI.registryItemsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRegistryItem).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(RegistryItem), inject: [DI.db], }; const $webhooksRepository: Provider = { provide: DI.webhooksRepository, - useFactory: (db: DataSource) => db.getRepository(MiWebhook).extend(miRepository as MiRepository), - inject: [DI.db], -}; - -const $systemWebhooksRepository: Provider = { - provide: DI.systemWebhooksRepository, - useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Webhook), inject: [DI.db], }; const $adsRepository: Provider = { provide: DI.adsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAd).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Ad), inject: [DI.db], }; const $passwordResetRequestsRepository: Provider = { provide: DI.passwordResetRequestsRepository, - useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(PasswordResetRequest), inject: [DI.db], }; const $retentionAggregationsRepository: Provider = { provide: DI.retentionAggregationsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(RetentionAggregation), inject: [DI.db], }; const $flashsRepository: Provider = { provide: DI.flashsRepository, - useFactory: (db: DataSource) => db.getRepository(MiFlash).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Flash), inject: [DI.db], }; const $flashLikesRepository: Provider = { provide: DI.flashLikesRepository, - useFactory: (db: DataSource) => db.getRepository(MiFlashLike).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(FlashLike), inject: [DI.db], }; const $rolesRepository: Provider = { provide: DI.rolesRepository, - useFactory: (db: DataSource) => db.getRepository(MiRole).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(Role), inject: [DI.db], }; const $roleAssignmentsRepository: Provider = { provide: DI.roleAssignmentsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(RoleAssignment), inject: [DI.db], }; const $userMemosRepository: Provider = { provide: DI.userMemosRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserMemo).extend(miRepository as MiRepository), - inject: [DI.db], -}; - -const $chatMessagesRepository: Provider = { - provide: DI.chatMessagesRepository, - useFactory: (db: DataSource) => db.getRepository(MiChatMessage).extend(miRepository as MiRepository), - inject: [DI.db], -}; - -const $chatRoomsRepository: Provider = { - provide: DI.chatRoomsRepository, - useFactory: (db: DataSource) => db.getRepository(MiChatRoom).extend(miRepository as MiRepository), - inject: [DI.db], -}; - -const $chatRoomMembershipsRepository: Provider = { - provide: DI.chatRoomMembershipsRepository, - useFactory: (db: DataSource) => db.getRepository(MiChatRoomMembership).extend(miRepository as MiRepository), - inject: [DI.db], -}; - -const $chatRoomInvitationsRepository: Provider = { - provide: DI.chatRoomInvitationsRepository, - useFactory: (db: DataSource) => db.getRepository(MiChatRoomInvitation).extend(miRepository as MiRepository), - inject: [DI.db], -}; - -const $chatApprovalsRepository: Provider = { - provide: DI.chatApprovalsRepository, - useFactory: (db: DataSource) => db.getRepository(MiChatApproval).extend(miRepository as MiRepository), - inject: [DI.db], -}; - -const $bubbleGameRecordsRepository: Provider = { - provide: DI.bubbleGameRecordsRepository, - useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository), - inject: [DI.db], -}; - -const $reversiGamesRepository: Provider = { - provide: DI.reversiGamesRepository, - useFactory: (db: DataSource) => db.getRepository(MiReversiGame).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(UserMemo), inject: [DI.db], }; @Module({ - imports: [], + imports: [ + ], providers: [ $usersRepository, $notesRepository, $announcementsRepository, $announcementReadsRepository, $appsRepository, - $avatarDecorationsRepository, $noteFavoritesRepository, $noteThreadMutingsRepository, $noteReactionsRepository, + $noteUnreadsRepository, $pollsRepository, $pollVotesRepository, $userProfilesRepository, $userKeypairsRepository, $userPendingsRepository, + $attestationChallengesRepository, $userSecurityKeysRepository, $userPublickeysRepository, $userListsRepository, $userListFavoritesRepository, - $userListMembershipsRepository, + $userListJoiningsRepository, $userNotePiningsRepository, $userIpsRepository, $usedUsernamesRepository, @@ -566,10 +438,8 @@ const $reversiGamesRepository: Provider = { $renoteMutingsRepository, $blockingsRepository, $swSubscriptionsRepository, - $systemAccountsRepository, $hashtagsRepository, $abuseUserReportsRepository, - $abuseReportNotificationRecipientRepository, $registrationTicketsRepository, $authSessionsRepository, $accessTokensRepository, @@ -586,12 +456,12 @@ const $reversiGamesRepository: Provider = { $promoNotesRepository, $promoReadsRepository, $relaysRepository, + $mutedNotesRepository, $channelsRepository, $channelFollowingsRepository, $channelFavoritesRepository, $registryItemsRepository, $webhooksRepository, - $systemWebhooksRepository, $adsRepository, $passwordResetRequestsRepository, $retentionAggregationsRepository, @@ -600,13 +470,6 @@ const $reversiGamesRepository: Provider = { $flashsRepository, $flashLikesRepository, $userMemosRepository, - $chatMessagesRepository, - $chatRoomsRepository, - $chatRoomMembershipsRepository, - $chatRoomInvitationsRepository, - $chatApprovalsRepository, - $bubbleGameRecordsRepository, - $reversiGamesRepository, ], exports: [ $usersRepository, @@ -614,20 +477,21 @@ const $reversiGamesRepository: Provider = { $announcementsRepository, $announcementReadsRepository, $appsRepository, - $avatarDecorationsRepository, $noteFavoritesRepository, $noteThreadMutingsRepository, $noteReactionsRepository, + $noteUnreadsRepository, $pollsRepository, $pollVotesRepository, $userProfilesRepository, $userKeypairsRepository, $userPendingsRepository, + $attestationChallengesRepository, $userSecurityKeysRepository, $userPublickeysRepository, $userListsRepository, $userListFavoritesRepository, - $userListMembershipsRepository, + $userListJoiningsRepository, $userNotePiningsRepository, $userIpsRepository, $usedUsernamesRepository, @@ -642,10 +506,8 @@ const $reversiGamesRepository: Provider = { $renoteMutingsRepository, $blockingsRepository, $swSubscriptionsRepository, - $systemAccountsRepository, $hashtagsRepository, $abuseUserReportsRepository, - $abuseReportNotificationRecipientRepository, $registrationTicketsRepository, $authSessionsRepository, $accessTokensRepository, @@ -662,12 +524,12 @@ const $reversiGamesRepository: Provider = { $promoNotesRepository, $promoReadsRepository, $relaysRepository, + $mutedNotesRepository, $channelsRepository, $channelFollowingsRepository, $channelFavoritesRepository, $registryItemsRepository, $webhooksRepository, - $systemWebhooksRepository, $adsRepository, $passwordResetRequestsRepository, $retentionAggregationsRepository, @@ -676,14 +538,6 @@ const $reversiGamesRepository: Provider = { $flashsRepository, $flashLikesRepository, $userMemosRepository, - $chatMessagesRepository, - $chatRoomsRepository, - $chatRoomMembershipsRepository, - $chatRoomInvitationsRepository, - $chatApprovalsRepository, - $bubbleGameRecordsRepository, - $reversiGamesRepository, ], }) -export class RepositoryModule { -} +export class RepositoryModule {} diff --git a/packages/backend/src/models/ReversiGame.ts b/packages/backend/src/models/ReversiGame.ts deleted file mode 100644 index 6b29a0ce8c..0000000000 --- a/packages/backend/src/models/ReversiGame.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -@Entity('reversi_game') -export class MiReversiGame { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - nullable: true, - comment: 'The started date of the ReversiGame.', - }) - public startedAt: Date | null; - - @Column('timestamp with time zone', { - nullable: true, - comment: 'The ended date of the ReversiGame.', - }) - public endedAt: Date | null; - - @Column(id()) - public user1Id: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user1: MiUser | null; - - @Column(id()) - public user2Id: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user2: MiUser | null; - - @Column('boolean', { - default: false, - }) - public user1Ready: boolean; - - @Column('boolean', { - default: false, - }) - public user2Ready: boolean; - - /** - * どちらのプレイヤーが先行(黒)か - * 1 ... user1 - * 2 ... user2 - */ - @Column('integer', { - nullable: true, - }) - public black: number | null; - - @Column('boolean', { - default: false, - }) - public isStarted: boolean; - - @Column('boolean', { - default: false, - }) - public isEnded: boolean; - - @Column({ - ...id(), - nullable: true, - }) - public winnerId: MiUser['id'] | null; - - @Column({ - ...id(), - nullable: true, - }) - public surrenderedUserId: MiUser['id'] | null; - - @Column({ - ...id(), - nullable: true, - }) - public timeoutUserId: MiUser['id'] | null; - - // in sec - @Column('smallint', { - default: 90, - }) - public timeLimitForEachTurn: number; - - @Column('jsonb', { - default: [], - }) - public logs: number[][]; - - @Column('varchar', { - array: true, length: 64, - }) - public map: string[]; - - @Column('varchar', { - length: 32, - }) - public bw: string; - - @Column('boolean', { - default: false, - }) - public noIrregularRules: boolean; - - @Column('boolean', { - default: false, - }) - public isLlotheo: boolean; - - @Column('boolean', { - default: false, - }) - public canPutEverywhere: boolean; - - @Column('boolean', { - default: false, - }) - public loopedBoard: boolean; - - @Column('jsonb', { - nullable: true, default: null, - }) - public form1: any | null; - - @Column('jsonb', { - nullable: true, default: null, - }) - public form2: any | null; - - @Column('varchar', { - length: 32, nullable: true, - }) - public crc32: string | null; -} diff --git a/packages/backend/src/models/SystemAccount.ts b/packages/backend/src/models/SystemAccount.ts deleted file mode 100644 index f32880b81d..0000000000 --- a/packages/backend/src/models/SystemAccount.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; -import { Serialized } from '@/types.js'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -@Entity('system_account') -@Index(['type'], { unique: true }) -export class MiSystemAccount { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Column('varchar', { - length: 256, - }) - public type: string; -} diff --git a/packages/backend/src/models/SystemWebhook.ts b/packages/backend/src/models/SystemWebhook.ts deleted file mode 100644 index 1a7ce4962b..0000000000 --- a/packages/backend/src/models/SystemWebhook.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; -import { Serialized } from '@/types.js'; -import { id } from './util/id.js'; - -export const systemWebhookEventTypes = [ - // ユーザからの通報を受けたとき - 'abuseReport', - // 通報を処理したとき - 'abuseReportResolved', - // ユーザが作成された時 - 'userCreated', - // モデレータが一定期間不在である警告 - 'inactiveModeratorsWarning', - // モデレータが一定期間不在のためシステムにより招待制へと変更された - 'inactiveModeratorsInvitationOnlyChanged', -] as const; -export type SystemWebhookEventType = typeof systemWebhookEventTypes[number]; - -@Entity('system_webhook') -export class MiSystemWebhook { - @PrimaryColumn(id()) - public id: string; - - /** - * 有効かどうか. - */ - @Index('IDX_system_webhook_isActive', { synchronize: false }) - @Column('boolean', { - default: true, - }) - public isActive: boolean; - - /** - * 更新日時. - */ - @Column('timestamp with time zone', { - default: () => 'CURRENT_TIMESTAMP', - }) - public updatedAt: Date; - - /** - * 最後に送信された日時. - */ - @Column('timestamp with time zone', { - nullable: true, - }) - public latestSentAt: Date | null; - - /** - * 最後に送信されたステータスコード - */ - @Column('integer', { - nullable: true, - }) - public latestStatus: number | null; - - /** - * 通知設定名. - */ - @Column('varchar', { - length: 255, - }) - public name: string; - - /** - * イベント種別. - */ - @Index('IDX_system_webhook_on', { synchronize: false }) - @Column('varchar', { - length: 128, - array: true, - default: '{}', - }) - public on: SystemWebhookEventType[]; - - /** - * Webhook送信先のURL. - */ - @Column('varchar', { - length: 1024, - }) - public url: string; - - /** - * Webhook検証用の値. - */ - @Column('varchar', { - length: 1024, - }) - public secret: string; - - static deserialize(obj: Serialized): MiSystemWebhook { - return { - ...obj, - updatedAt: new Date(obj.updatedAt), - latestSentAt: obj.latestSentAt ? new Date(obj.latestSentAt) : null, - }; - } -} diff --git a/packages/backend/src/models/UserListFavorite.ts b/packages/backend/src/models/UserListFavorite.ts deleted file mode 100644 index 80b2d61eb7..0000000000 --- a/packages/backend/src/models/UserListFavorite.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiUserList } from './UserList.js'; - -@Entity('user_list_favorite') -@Index(['userId', 'userListId'], { unique: true }) -export class MiUserListFavorite { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Column(id()) - public userListId: MiUserList['id']; - - @ManyToOne(type => MiUserList, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userList: MiUserList | null; -} diff --git a/packages/backend/src/models/UserListMembership.ts b/packages/backend/src/models/UserListMembership.ts deleted file mode 100644 index af659d071d..0000000000 --- a/packages/backend/src/models/UserListMembership.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiUserList } from './UserList.js'; - -@Entity('user_list_membership') -@Index(['userId', 'userListId'], { unique: true }) -export class MiUserListMembership { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: 'The user ID.', - }) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Index() - @Column({ - ...id(), - comment: 'The list ID.', - }) - public userListId: MiUserList['id']; - - @ManyToOne(type => MiUserList, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userList: MiUserList | null; - - // タイムラインにその人のリプライまで含めるかどうか - @Column('boolean', { - default: false, - }) - public withReplies: boolean; - - //#region Denormalized fields - @Column({ - ...id(), - }) - public userListUserId: MiUser['id']; - //#endregion -} diff --git a/packages/backend/src/models/UserNotePining.ts b/packages/backend/src/models/UserNotePining.ts deleted file mode 100644 index 92c5cd55d0..0000000000 --- a/packages/backend/src/models/UserNotePining.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiNote } from './Note.js'; -import { MiUser } from './User.js'; - -@Entity('user_note_pining') -@Index(['userId', 'noteId'], { unique: true }) -export class MiUserNotePining { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Column(id()) - public noteId: MiNote['id']; - - @ManyToOne(type => MiNote, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: MiNote | null; -} diff --git a/packages/backend/src/models/UserSecurityKey.ts b/packages/backend/src/models/UserSecurityKey.ts deleted file mode 100644 index 0babbe1abe..0000000000 --- a/packages/backend/src/models/UserSecurityKey.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -@Entity('user_security_key') -export class MiUserSecurityKey { - @PrimaryColumn('varchar', { - comment: 'Variable-length id given to navigator.credentials.get()', - }) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Column('varchar', { - comment: 'User-defined name for this key', - length: 30, - }) - public name: string; - - @Index() - @Column('varchar', { - comment: 'The public key of the UserSecurityKey, hex-encoded.', - }) - public publicKey: string; - - @Column('bigint', { - comment: 'The number of times the UserSecurityKey was validated.', - default: 0, - }) - public counter: number; - - @Column('timestamp with time zone', { - comment: 'Timestamp of the last time the UserSecurityKey was used.', - default: () => 'now()', - }) - public lastUsed: Date; - - @Column('varchar', { - comment: 'The type of Backup Eligibility in authenticator data', - length: 32, nullable: true, - }) - public credentialDeviceType: string | null; - - @Column('boolean', { - comment: 'Whether or not the credential has been backed up', - nullable: true, - }) - public credentialBackedUp: boolean | null; - - @Column('varchar', { - comment: 'The type of the credential returned by the browser', - length: 32, array: true, nullable: true, - }) - public transports: string[] | null; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts deleted file mode 100644 index e1ea2a2604..0000000000 --- a/packages/backend/src/models/_.ts +++ /dev/null @@ -1,312 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { - FindOneOptions, - InsertQueryBuilder, - ObjectLiteral, - QueryRunner, - Repository, - SelectQueryBuilder, -} from 'typeorm'; -import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js'; -import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js'; -import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js'; -import { - RawSqlResultsToEntityTransformer, -} from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js'; -import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; -import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; -import { MiAccessToken } from '@/models/AccessToken.js'; -import { MiAd } from '@/models/Ad.js'; -import { MiAnnouncement } from '@/models/Announcement.js'; -import { MiAnnouncementRead } from '@/models/AnnouncementRead.js'; -import { MiAntenna } from '@/models/Antenna.js'; -import { MiApp } from '@/models/App.js'; -import { MiAuthSession } from '@/models/AuthSession.js'; -import { MiAvatarDecoration } from '@/models/AvatarDecoration.js'; -import { MiBlocking } from '@/models/Blocking.js'; -import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; -import { MiChannel } from '@/models/Channel.js'; -import { MiChannelFavorite } from '@/models/ChannelFavorite.js'; -import { MiChannelFollowing } from '@/models/ChannelFollowing.js'; -import { MiChatApproval } from '@/models/ChatApproval.js'; -import { MiChatMessage } from '@/models/ChatMessage.js'; -import { MiChatRoom } from '@/models/ChatRoom.js'; -import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js'; -import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js'; -import { MiClip } from '@/models/Clip.js'; -import { MiClipFavorite } from '@/models/ClipFavorite.js'; -import { MiClipNote } from '@/models/ClipNote.js'; -import { MiDriveFile } from '@/models/DriveFile.js'; -import { MiDriveFolder } from '@/models/DriveFolder.js'; -import { MiEmoji } from '@/models/Emoji.js'; -import { MiFlash } from '@/models/Flash.js'; -import { MiFlashLike } from '@/models/FlashLike.js'; -import { MiFollowing } from '@/models/Following.js'; -import { MiFollowRequest } from '@/models/FollowRequest.js'; -import { MiGalleryLike } from '@/models/GalleryLike.js'; -import { MiGalleryPost } from '@/models/GalleryPost.js'; -import { MiHashtag } from '@/models/Hashtag.js'; -import { MiInstance } from '@/models/Instance.js'; -import { MiMeta } from '@/models/Meta.js'; -import { MiModerationLog } from '@/models/ModerationLog.js'; -import { MiMuting } from '@/models/Muting.js'; -import { MiNote } from '@/models/Note.js'; -import { MiNoteFavorite } from '@/models/NoteFavorite.js'; -import { MiNoteReaction } from '@/models/NoteReaction.js'; -import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js'; -import { MiPage } from '@/models/Page.js'; -import { MiPageLike } from '@/models/PageLike.js'; -import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js'; -import { MiPoll } from '@/models/Poll.js'; -import { MiPollVote } from '@/models/PollVote.js'; -import { MiPromoNote } from '@/models/PromoNote.js'; -import { MiPromoRead } from '@/models/PromoRead.js'; -import { MiRegistrationTicket } from '@/models/RegistrationTicket.js'; -import { MiRegistryItem } from '@/models/RegistryItem.js'; -import { MiRelay } from '@/models/Relay.js'; -import { MiRenoteMuting } from '@/models/RenoteMuting.js'; -import { MiRetentionAggregation } from '@/models/RetentionAggregation.js'; -import { MiReversiGame } from '@/models/ReversiGame.js'; -import { MiRole } from '@/models/Role.js'; -import { MiRoleAssignment } from '@/models/RoleAssignment.js'; -import { MiSignin } from '@/models/Signin.js'; -import { MiSwSubscription } from '@/models/SwSubscription.js'; -import { MiSystemAccount } from '@/models/SystemAccount.js'; -import { MiSystemWebhook } from '@/models/SystemWebhook.js'; -import { MiUsedUsername } from '@/models/UsedUsername.js'; -import { MiUser } from '@/models/User.js'; -import { MiUserIp } from '@/models/UserIp.js'; -import { MiUserKeypair } from '@/models/UserKeypair.js'; -import { MiUserList } from '@/models/UserList.js'; -import { MiUserListFavorite } from '@/models/UserListFavorite.js'; -import { MiUserListMembership } from '@/models/UserListMembership.js'; -import { MiUserMemo } from '@/models/UserMemo.js'; -import { MiUserNotePining } from '@/models/UserNotePining.js'; -import { MiUserPending } from '@/models/UserPending.js'; -import { MiUserProfile } from '@/models/UserProfile.js'; -import { MiUserPublickey } from '@/models/UserPublickey.js'; -import { MiUserSecurityKey } from '@/models/UserSecurityKey.js'; -import { MiWebhook } from '@/models/Webhook.js'; -import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; - -export interface MiRepository { - createTableColumnNames(this: Repository & MiRepository): string[]; - - insertOne(this: Repository & MiRepository, entity: QueryDeepPartialEntity, findOptions?: Pick, 'relations'>): Promise; - - insertOneImpl(this: Repository & MiRepository, entity: QueryDeepPartialEntity, findOptions?: Pick, 'relations'>, queryRunner?: QueryRunner): Promise; - - selectAliasColumnNames(this: Repository & MiRepository, queryBuilder: InsertQueryBuilder, builder: SelectQueryBuilder): void; -} - -export const miRepository = { - createTableColumnNames() { - return this.metadata.columns.filter(column => column.isSelect && !column.isVirtual).map(column => column.databaseName); - }, - async insertOne(entity, findOptions?) { - const opt = this.manager.connection.options as PostgresConnectionOptions; - if (opt.replication) { - const queryRunner = this.manager.connection.createQueryRunner('master'); - try { - return this.insertOneImpl(entity, findOptions, queryRunner); - } finally { - await queryRunner.release(); - } - } else { - return this.insertOneImpl(entity, findOptions); - } - }, - async insertOneImpl(entity, findOptions?, queryRunner?) { - // ---- insert + returningの結果を共通テーブル式(CTE)に保持するクエリを生成 ---- - - const queryBuilder = this.createQueryBuilder().insert().values(entity); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const mainAlias = queryBuilder.expressionMap.mainAlias!; - const name = mainAlias.name; - mainAlias.name = 't'; - const columnNames = this.createTableColumnNames(); - queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2)); - - // ---- 共通テーブル式(CTE)から結果を取得 ---- - const builder = this.createQueryBuilder(undefined, queryRunner).addCommonTableExpression(queryBuilder, 'cte', { columnNames }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - builder.expressionMap.mainAlias!.tablePath = 'cte'; - this.selectAliasColumnNames(queryBuilder, builder); - if (findOptions) { - builder.setFindOptions(findOptions); - } - const raw = await builder.execute(); - mainAlias.name = name; - const relationId = await new RelationIdLoader(builder.connection, this.queryRunner, builder.expressionMap.relationIdAttributes).load(raw); - const relationCount = await new RelationCountLoader(builder.connection, this.queryRunner, builder.expressionMap.relationCountAttributes).load(raw); - const result = new RawSqlResultsToEntityTransformer(builder.expressionMap, builder.connection.driver, relationId, relationCount, this.queryRunner).transform(raw, mainAlias); - return result[0]; - }, - selectAliasColumnNames(queryBuilder, builder) { - let selectOrAddSelect = (selection: string, selectionAliasName?: string) => { - selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName); - return builder.select(selection, selectionAliasName); - }; - for (const columnName of this.createTableColumnNames()) { - selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`); - } - }, -} satisfies MiRepository; - -export { - MiAbuseUserReport, - MiAbuseReportNotificationRecipient, - MiAccessToken, - MiAd, - MiAnnouncement, - MiAnnouncementRead, - MiAntenna, - MiApp, - MiAvatarDecoration, - MiAuthSession, - MiBlocking, - MiChannelFollowing, - MiChannelFavorite, - MiClip, - MiClipNote, - MiClipFavorite, - MiDriveFile, - MiDriveFolder, - MiEmoji, - MiFollowing, - MiFollowRequest, - MiGalleryLike, - MiGalleryPost, - MiHashtag, - MiInstance, - MiMeta, - MiModerationLog, - MiMuting, - MiRenoteMuting, - MiNote, - MiNoteFavorite, - MiNoteReaction, - MiNoteThreadMuting, - MiPage, - MiPageLike, - MiPasswordResetRequest, - MiPoll, - MiPollVote, - MiPromoNote, - MiPromoRead, - MiRegistrationTicket, - MiRegistryItem, - MiRelay, - MiSignin, - MiSwSubscription, - MiSystemAccount, - MiUsedUsername, - MiUser, - MiUserIp, - MiUserKeypair, - MiUserList, - MiUserListFavorite, - MiUserListMembership, - MiUserNotePining, - MiUserPending, - MiUserProfile, - MiUserPublickey, - MiUserSecurityKey, - MiWebhook, - MiSystemWebhook, - MiChannel, - MiRetentionAggregation, - MiRole, - MiRoleAssignment, - MiFlash, - MiFlashLike, - MiUserMemo, - MiChatMessage, - MiChatRoom, - MiChatRoomMembership, - MiChatRoomInvitation, - MiChatApproval, - MiBubbleGameRecord, - MiReversiGame, -}; - -export type AbuseUserReportsRepository = Repository & MiRepository; -export type AbuseReportNotificationRecipientRepository = - Repository - & MiRepository; -export type AccessTokensRepository = Repository & MiRepository; -export type AdsRepository = Repository & MiRepository; -export type AnnouncementsRepository = Repository & MiRepository; -export type AnnouncementReadsRepository = Repository & MiRepository; -export type AntennasRepository = Repository & MiRepository; -export type AppsRepository = Repository & MiRepository; -export type AvatarDecorationsRepository = Repository & MiRepository; -export type AuthSessionsRepository = Repository & MiRepository; -export type BlockingsRepository = Repository & MiRepository; -export type ChannelFollowingsRepository = Repository & MiRepository; -export type ChannelFavoritesRepository = Repository & MiRepository; -export type ClipsRepository = Repository & MiRepository; -export type ClipNotesRepository = Repository & MiRepository; -export type ClipFavoritesRepository = Repository & MiRepository; -export type DriveFilesRepository = Repository & MiRepository; -export type DriveFoldersRepository = Repository & MiRepository; -export type EmojisRepository = Repository & MiRepository; -export type FollowingsRepository = Repository & MiRepository; -export type FollowRequestsRepository = Repository & MiRepository; -export type GalleryLikesRepository = Repository & MiRepository; -export type GalleryPostsRepository = Repository & MiRepository; -export type HashtagsRepository = Repository & MiRepository; -export type InstancesRepository = Repository & MiRepository; -export type MetasRepository = Repository & MiRepository; -export type ModerationLogsRepository = Repository & MiRepository; -export type MutingsRepository = Repository & MiRepository; -export type RenoteMutingsRepository = Repository & MiRepository; -export type NotesRepository = Repository & MiRepository; -export type NoteFavoritesRepository = Repository & MiRepository; -export type NoteReactionsRepository = Repository & MiRepository; -export type NoteThreadMutingsRepository = Repository & MiRepository; -export type PagesRepository = Repository & MiRepository; -export type PageLikesRepository = Repository & MiRepository; -export type PasswordResetRequestsRepository = Repository & MiRepository; -export type PollsRepository = Repository & MiRepository; -export type PollVotesRepository = Repository & MiRepository; -export type PromoNotesRepository = Repository & MiRepository; -export type PromoReadsRepository = Repository & MiRepository; -export type RegistrationTicketsRepository = Repository & MiRepository; -export type RegistryItemsRepository = Repository & MiRepository; -export type RelaysRepository = Repository & MiRepository; -export type SigninsRepository = Repository & MiRepository; -export type SwSubscriptionsRepository = Repository & MiRepository; -export type SystemAccountsRepository = Repository & MiRepository; -export type UsedUsernamesRepository = Repository & MiRepository; -export type UsersRepository = Repository & MiRepository; -export type UserIpsRepository = Repository & MiRepository; -export type UserKeypairsRepository = Repository & MiRepository; -export type UserListsRepository = Repository & MiRepository; -export type UserListFavoritesRepository = Repository & MiRepository; -export type UserListMembershipsRepository = Repository & MiRepository; -export type UserNotePiningsRepository = Repository & MiRepository; -export type UserPendingsRepository = Repository & MiRepository; -export type UserProfilesRepository = Repository & MiRepository; -export type UserPublickeysRepository = Repository & MiRepository; -export type UserSecurityKeysRepository = Repository & MiRepository; -export type WebhooksRepository = Repository & MiRepository; -export type SystemWebhooksRepository = Repository & MiRepository; -export type ChannelsRepository = Repository & MiRepository; -export type RetentionAggregationsRepository = Repository & MiRepository; -export type RolesRepository = Repository & MiRepository; -export type RoleAssignmentsRepository = Repository & MiRepository; -export type FlashsRepository = Repository & MiRepository; -export type FlashLikesRepository = Repository & MiRepository; -export type UserMemoRepository = Repository & MiRepository; -export type ChatMessagesRepository = Repository & MiRepository; -export type ChatRoomsRepository = Repository & MiRepository; -export type ChatRoomMembershipsRepository = Repository & MiRepository; -export type ChatRoomInvitationsRepository = Repository & MiRepository; -export type ChatApprovalsRepository = Repository & MiRepository; -export type BubbleGameRecordsRepository = Repository & MiRepository; -export type ReversiGamesRepository = Repository & MiRepository; diff --git a/packages/backend/src/models/entities/AbuseUserReport.ts b/packages/backend/src/models/entities/AbuseUserReport.ts new file mode 100644 index 0000000000..07305cf23a --- /dev/null +++ b/packages/backend/src/models/entities/AbuseUserReport.ts @@ -0,0 +1,79 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class AbuseUserReport { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the AbuseUserReport.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public targetUserId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public targetUser: User | null; + + @Index() + @Column(id()) + public reporterId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public reporter: User | null; + + @Column({ + ...id(), + nullable: true, + }) + public assigneeId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'SET NULL', + }) + @JoinColumn() + public assignee: User | null; + + @Index() + @Column('boolean', { + default: false, + }) + public resolved: boolean; + + @Column('boolean', { + default: false, + }) + public forwarded: boolean; + + @Column('varchar', { + length: 2048, + }) + public comment: string; + + //#region Denormalized fields + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]', + }) + public targetUserHost: string | null; + + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]', + }) + public reporterHost: string | null; + //#endregion +} diff --git a/packages/backend/src/models/AccessToken.ts b/packages/backend/src/models/entities/AccessToken.ts similarity index 71% rename from packages/backend/src/models/AccessToken.ts rename to packages/backend/src/models/entities/AccessToken.ts index 6f98c14ec1..8e987ffeef 100644 --- a/packages/backend/src/models/AccessToken.ts +++ b/packages/backend/src/models/entities/AccessToken.ts @@ -1,18 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiApp } from './App.js'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { App } from './App.js'; -@Entity('access_token') -export class MiAccessToken { +@Entity() +export class AccessToken { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone', { + comment: 'The created date of the AccessToken.', + }) + public createdAt: Date; + @Column('timestamp with time zone', { nullable: true, }) @@ -39,25 +39,25 @@ export class MiAccessToken { @Index() @Column(id()) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column({ ...id(), nullable: true, }) - public appId: MiApp['id'] | null; + public appId: App['id'] | null; - @ManyToOne(type => MiApp, { + @ManyToOne(type => App, { onDelete: 'CASCADE', }) @JoinColumn() - public app: MiApp | null; + public app: App | null; @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/Ad.ts b/packages/backend/src/models/entities/Ad.ts similarity index 78% rename from packages/backend/src/models/Ad.ts rename to packages/backend/src/models/entities/Ad.ts index 108e991c70..56baf863ca 100644 --- a/packages/backend/src/models/Ad.ts +++ b/packages/backend/src/models/entities/Ad.ts @@ -1,16 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; -import { id } from './util/id.js'; +import { id } from '../id.js'; -@Entity('ad') -export class MiAd { +@Entity() +export class Ad { @PrimaryColumn(id()) public id: string; + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Ad.', + }) + public createdAt: Date; + @Index() @Column('timestamp with time zone', { comment: 'The expired date of the Ad.', @@ -54,11 +55,8 @@ export class MiAd { length: 8192, nullable: false, }) public memo: string; - @Column('integer', { - default: 0, nullable: false, - }) - public dayOfWeek: number; - constructor(data: Partial) { + + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/entities/Announcement.ts b/packages/backend/src/models/entities/Announcement.ts new file mode 100644 index 0000000000..beb2f82462 --- /dev/null +++ b/packages/backend/src/models/entities/Announcement.ts @@ -0,0 +1,43 @@ +import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; +import { id } from '../id.js'; + +@Entity() +export class Announcement { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Announcement.', + }) + public createdAt: Date; + + @Column('timestamp with time zone', { + comment: 'The updated date of the Announcement.', + nullable: true, + }) + public updatedAt: Date | null; + + @Column('varchar', { + length: 8192, nullable: false, + }) + public text: string; + + @Column('varchar', { + length: 256, nullable: false, + }) + public title: string; + + @Column('varchar', { + length: 1024, nullable: true, + }) + public imageUrl: string | null; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/entities/AnnouncementRead.ts b/packages/backend/src/models/entities/AnnouncementRead.ts new file mode 100644 index 0000000000..72cf688800 --- /dev/null +++ b/packages/backend/src/models/entities/AnnouncementRead.ts @@ -0,0 +1,36 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Announcement } from './Announcement.js'; + +@Entity() +@Index(['userId', 'announcementId'], { unique: true }) +export class AnnouncementRead { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the AnnouncementRead.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public announcementId: Announcement['id']; + + @ManyToOne(type => Announcement, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public announcement: Announcement | null; +} diff --git a/packages/backend/src/models/Antenna.ts b/packages/backend/src/models/entities/Antenna.ts similarity index 52% rename from packages/backend/src/models/Antenna.ts rename to packages/backend/src/models/entities/Antenna.ts index ccc8823703..e63e7f2c72 100644 --- a/packages/backend/src/models/Antenna.ts +++ b/packages/backend/src/models/entities/Antenna.ts @@ -1,18 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiUserList } from './UserList.js'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { UserList } from './UserList.js'; -@Entity('antenna') -export class MiAntenna { +@Entity() +export class Antenna { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone', { + comment: 'The created date of the Antenna.', + }) + public createdAt: Date; + @Index() @Column('timestamp with time zone') public lastUsedAt: Date; @@ -22,13 +22,13 @@ export class MiAntenna { ...id(), comment: 'The owner ID.', }) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column('varchar', { length: 128, @@ -36,20 +36,20 @@ export class MiAntenna { }) public name: string; - @Column('enum', { enum: ['home', 'all', 'users', 'list', 'users_blacklist'] }) - public src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist'; + @Column('enum', { enum: ['home', 'all', 'users', 'list'] }) + public src: 'home' | 'all' | 'users' | 'list'; @Column({ ...id(), nullable: true, }) - public userListId: MiUserList['id'] | null; + public userListId: UserList['id'] | null; - @ManyToOne(type => MiUserList, { + @ManyToOne(type => UserList, { onDelete: 'CASCADE', }) @JoinColumn() - public userList: MiUserList | null; + public userList: UserList | null; @Column('varchar', { length: 1024, array: true, @@ -72,11 +72,6 @@ export class MiAntenna { }) public caseSensitive: boolean; - @Column('boolean', { - default: false, - }) - public excludeBots: boolean; - @Column('boolean', { default: false, }) @@ -90,22 +85,12 @@ export class MiAntenna { }) public expression: string | null; + @Column('boolean') + public notify: boolean; + @Index() @Column('boolean', { default: true, }) public isActive: boolean; - - @Column('boolean', { - default: false, - }) - public localOnly: boolean; - - @Column('boolean', { - default: false, - }) - public excludeNotesInSensitiveChannel: boolean; } -// Note for future developers: When you added a new column, -// You should update ExportAntennaProcessorService and ImportAntennaProcessorService -// to export and import antennas correctly. diff --git a/packages/backend/src/models/App.ts b/packages/backend/src/models/entities/App.ts similarity index 73% rename from packages/backend/src/models/App.ts rename to packages/backend/src/models/entities/App.ts index 0185e2995c..3a1ea7732e 100644 --- a/packages/backend/src/models/App.ts +++ b/packages/backend/src/models/entities/App.ts @@ -1,30 +1,31 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('app') -export class MiApp { +@Entity() +export class App { @PrimaryColumn(id()) public id: string; + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the App.', + }) + public createdAt: Date; + @Index() @Column({ ...id(), nullable: true, comment: 'The owner ID.', }) - public userId: MiUser['id'] | null; + public userId: User['id'] | null; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'SET NULL', nullable: true, }) - public user: MiUser | null; + public user: User | null; @Index() @Column('varchar', { diff --git a/packages/backend/src/models/entities/AttestationChallenge.ts b/packages/backend/src/models/entities/AttestationChallenge.ts new file mode 100644 index 0000000000..4795642657 --- /dev/null +++ b/packages/backend/src/models/entities/AttestationChallenge.ts @@ -0,0 +1,46 @@ +import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class AttestationChallenge { + @PrimaryColumn(id()) + public id: string; + + @Index() + @PrimaryColumn(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column('varchar', { + length: 64, + comment: 'Hex-encoded sha256 hash of the challenge.', + }) + public challenge: string; + + @Column('timestamp with time zone', { + comment: 'The date challenge was created for expiry purposes.', + }) + public createdAt: Date; + + @Column('boolean', { + comment: + 'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.', + default: false, + }) + public registrationChallenge: boolean; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/entities/AuthSession.ts b/packages/backend/src/models/entities/AuthSession.ts new file mode 100644 index 0000000000..6b2f50e8d6 --- /dev/null +++ b/packages/backend/src/models/entities/AuthSession.ts @@ -0,0 +1,43 @@ +import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { App } from './App.js'; + +@Entity() +export class AuthSession { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the AuthSession.', + }) + public createdAt: Date; + + @Index() + @Column('varchar', { + length: 128, + }) + public token: string; + + @Column({ + ...id(), + nullable: true, + }) + public userId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + nullable: true, + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public appId: App['id']; + + @ManyToOne(type => App, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public app: App | null; +} diff --git a/packages/backend/src/models/Blocking.ts b/packages/backend/src/models/entities/Blocking.ts similarity index 50% rename from packages/backend/src/models/Blocking.ts rename to packages/backend/src/models/entities/Blocking.ts index 34a6efe5a6..9892ff308e 100644 --- a/packages/backend/src/models/Blocking.ts +++ b/packages/backend/src/models/entities/Blocking.ts @@ -1,41 +1,42 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('blocking') +@Entity() @Index(['blockerId', 'blockeeId'], { unique: true }) -export class MiBlocking { +export class Blocking { @PrimaryColumn(id()) public id: string; + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Blocking.', + }) + public createdAt: Date; + @Index() @Column({ ...id(), comment: 'The blockee user ID.', }) - public blockeeId: MiUser['id']; + public blockeeId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public blockee: MiUser | null; + public blockee: User | null; @Index() @Column({ ...id(), comment: 'The blocker user ID.', }) - public blockerId: MiUser['id']; + public blockerId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public blocker: MiUser | null; + public blocker: User | null; } diff --git a/packages/backend/src/models/Channel.ts b/packages/backend/src/models/entities/Channel.ts similarity index 66% rename from packages/backend/src/models/Channel.ts rename to packages/backend/src/models/entities/Channel.ts index f5e9b17e3e..d7c4583da3 100644 --- a/packages/backend/src/models/Channel.ts +++ b/packages/backend/src/models/entities/Channel.ts @@ -1,18 +1,19 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiDriveFile } from './DriveFile.js'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { DriveFile } from './DriveFile.js'; -@Entity('channel') -export class MiChannel { +@Entity() +export class Channel { @PrimaryColumn(id()) public id: string; + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Channel.', + }) + public createdAt: Date; + @Index() @Column('timestamp with time zone', { nullable: true, @@ -25,13 +26,13 @@ export class MiChannel { nullable: true, comment: 'The owner ID.', }) - public userId: MiUser['id'] | null; + public userId: User['id'] | null; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'SET NULL', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column('varchar', { length: 128, @@ -50,13 +51,13 @@ export class MiChannel { nullable: true, comment: 'The ID of banner Channel.', }) - public bannerId: MiDriveFile['id'] | null; + public bannerId: DriveFile['id'] | null; - @ManyToOne(type => MiDriveFile, { + @ManyToOne(type => DriveFile, { onDelete: 'SET NULL', }) @JoinColumn() - public banner: MiDriveFile | null; + public banner: DriveFile | null; @Column('varchar', { array: true, length: 128, default: '{}', @@ -88,14 +89,4 @@ export class MiChannel { comment: 'The count of users.', }) public usersCount: number; - - @Column('boolean', { - default: false, - }) - public isSensitive: boolean; - - @Column('boolean', { - default: true, - }) - public allowRenoteToExternal: boolean; } diff --git a/packages/backend/src/models/entities/ChannelFavorite.ts b/packages/backend/src/models/entities/ChannelFavorite.ts new file mode 100644 index 0000000000..cfb2c892cf --- /dev/null +++ b/packages/backend/src/models/entities/ChannelFavorite.ts @@ -0,0 +1,41 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Channel } from './Channel.js'; + +@Entity() +@Index(['userId', 'channelId'], { unique: true }) +export class ChannelFavorite { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the ChannelFavorite.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + }) + public channelId: Channel['id']; + + @ManyToOne(type => Channel, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public channel: Channel | null; + + @Index() + @Column({ + ...id(), + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; +} diff --git a/packages/backend/src/models/entities/ChannelFollowing.ts b/packages/backend/src/models/entities/ChannelFollowing.ts new file mode 100644 index 0000000000..c65c38b67d --- /dev/null +++ b/packages/backend/src/models/entities/ChannelFollowing.ts @@ -0,0 +1,43 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Channel } from './Channel.js'; + +@Entity() +@Index(['followerId', 'followeeId'], { unique: true }) +export class ChannelFollowing { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the ChannelFollowing.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The followee channel ID.', + }) + public followeeId: Channel['id']; + + @ManyToOne(type => Channel, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public followee: Channel | null; + + @Index() + @Column({ + ...id(), + comment: 'The follower user ID.', + }) + public followerId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public follower: User | null; +} diff --git a/packages/backend/src/models/Clip.ts b/packages/backend/src/models/entities/Clip.ts similarity index 68% rename from packages/backend/src/models/Clip.ts rename to packages/backend/src/models/entities/Clip.ts index 6295a329fb..825a32c981 100644 --- a/packages/backend/src/models/Clip.ts +++ b/packages/backend/src/models/entities/Clip.ts @@ -1,17 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('clip') -export class MiClip { +@Entity() +export class Clip { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone', { + comment: 'The created date of the Clip.', + }) + public createdAt: Date; + @Index() @Column('timestamp with time zone', { nullable: true, @@ -23,13 +23,13 @@ export class MiClip { ...id(), comment: 'The owner ID.', }) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/entities/ClipFavorite.ts b/packages/backend/src/models/entities/ClipFavorite.ts new file mode 100644 index 0000000000..623471e671 --- /dev/null +++ b/packages/backend/src/models/entities/ClipFavorite.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Clip } from './Clip.js'; + +@Entity() +@Index(['userId', 'clipId'], { unique: true }) +export class ClipFavorite { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public clipId: Clip['id']; + + @ManyToOne(type => Clip, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public clip: Clip | null; +} diff --git a/packages/backend/src/models/entities/ClipNote.ts b/packages/backend/src/models/entities/ClipNote.ts new file mode 100644 index 0000000000..bc9ef4b874 --- /dev/null +++ b/packages/backend/src/models/entities/ClipNote.ts @@ -0,0 +1,37 @@ +import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; +import { id } from '../id.js'; +import { Note } from './Note.js'; +import { Clip } from './Clip.js'; + +@Entity() +@Index(['noteId', 'clipId'], { unique: true }) +export class ClipNote { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + comment: 'The note ID.', + }) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; + + @Index() + @Column({ + ...id(), + comment: 'The clip ID.', + }) + public clipId: Clip['id']; + + @ManyToOne(type => Clip, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public clip: Clip | null; +} diff --git a/packages/backend/src/models/DriveFile.ts b/packages/backend/src/models/entities/DriveFile.ts similarity index 86% rename from packages/backend/src/models/DriveFile.ts rename to packages/backend/src/models/entities/DriveFile.ts index 7b03e3e494..7b9670fb92 100644 --- a/packages/backend/src/models/DriveFile.ts +++ b/packages/backend/src/models/entities/DriveFile.ts @@ -1,32 +1,33 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiDriveFolder } from './DriveFolder.js'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { DriveFolder } from './DriveFolder.js'; -@Entity('drive_file') +@Entity() @Index(['userId', 'folderId', 'id']) -export class MiDriveFile { +export class DriveFile { @PrimaryColumn(id()) public id: string; + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the DriveFile.', + }) + public createdAt: Date; + @Index() @Column({ ...id(), nullable: true, comment: 'The owner ID.', }) - public userId: MiUser['id'] | null; + public userId: User['id'] | null; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'SET NULL', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Index() @Column('varchar', { @@ -82,7 +83,7 @@ export class MiDriveFile { public storedInternal: boolean; @Column('varchar', { - length: 1024, + length: 512, comment: 'The URL of the DriveFile.', }) public url: string; @@ -124,13 +125,13 @@ export class MiDriveFile { @Index() @Column('varchar', { - length: 1024, nullable: true, + length: 512, nullable: true, comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.', }) public uri: string | null; @Column('varchar', { - length: 1024, nullable: true, + length: 512, nullable: true, }) public src: string | null; @@ -140,13 +141,13 @@ export class MiDriveFile { nullable: true, comment: 'The parent folder ID. If null, it means the DriveFile is located in root.', }) - public folderId: MiDriveFolder['id'] | null; + public folderId: DriveFolder['id'] | null; - @ManyToOne(type => MiDriveFolder, { + @ManyToOne(type => DriveFolder, { onDelete: 'SET NULL', }) @JoinColumn() - public folder: MiDriveFolder | null; + public folder: DriveFolder | null; @Index() @Column('boolean', { diff --git a/packages/backend/src/models/DriveFolder.ts b/packages/backend/src/models/entities/DriveFolder.ts similarity index 55% rename from packages/backend/src/models/DriveFolder.ts rename to packages/backend/src/models/entities/DriveFolder.ts index 07046d6e11..2a73a0875d 100644 --- a/packages/backend/src/models/DriveFolder.ts +++ b/packages/backend/src/models/entities/DriveFolder.ts @@ -1,17 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { JoinColumn, ManyToOne, Entity, PrimaryColumn, Index, Column } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('drive_folder') -export class MiDriveFolder { +@Entity() +export class DriveFolder { @PrimaryColumn(id()) public id: string; + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the DriveFolder.', + }) + public createdAt: Date; + @Column('varchar', { length: 128, comment: 'The name of the DriveFolder.', @@ -24,13 +25,13 @@ export class MiDriveFolder { nullable: true, comment: 'The owner ID.', }) - public userId: MiUser['id'] | null; + public userId: User['id'] | null; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Index() @Column({ @@ -38,11 +39,11 @@ export class MiDriveFolder { nullable: true, comment: 'The parent folder ID. If null, it means the DriveFolder is located in root.', }) - public parentId: MiDriveFolder['id'] | null; + public parentId: DriveFolder['id'] | null; - @ManyToOne(type => MiDriveFolder, { + @ManyToOne(type => DriveFolder, { onDelete: 'SET NULL', }) @JoinColumn() - public parent: MiDriveFolder | null; + public parent: DriveFolder | null; } diff --git a/packages/backend/src/models/Emoji.ts b/packages/backend/src/models/entities/Emoji.ts similarity index 88% rename from packages/backend/src/models/Emoji.ts rename to packages/backend/src/models/entities/Emoji.ts index d62b6e9f6f..8fd3e65f5e 100644 --- a/packages/backend/src/models/Emoji.ts +++ b/packages/backend/src/models/entities/Emoji.ts @@ -1,14 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; -import { id } from './util/id.js'; +import { id } from '../id.js'; -@Entity('emoji') +@Entity() @Index(['name', 'host'], { unique: true }) -export class MiEmoji { +export class Emoji { @PrimaryColumn(id()) public id: string; diff --git a/packages/backend/src/models/Flash.ts b/packages/backend/src/models/entities/Flash.ts similarity index 54% rename from packages/backend/src/models/Flash.ts rename to packages/backend/src/models/entities/Flash.ts index 5db7dca992..4ccc908a6a 100644 --- a/packages/backend/src/models/Flash.ts +++ b/packages/backend/src/models/entities/Flash.ts @@ -1,20 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -export const flashVisibility = ['public', 'private'] as const; -export type FlashVisibility = typeof flashVisibility[number]; - -@Entity('flash') -export class MiFlash { +@Entity() +export class Flash { @PrimaryColumn(id()) public id: string; + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Flash.', + }) + public createdAt: Date; + @Index() @Column('timestamp with time zone', { comment: 'The updated date of the Flash.', @@ -36,13 +34,13 @@ export class MiFlash { ...id(), comment: 'The ID of author.', }) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column('varchar', { length: 65536, @@ -58,13 +56,4 @@ export class MiFlash { default: 0, }) public likedCount: number; - - /** - * public ... 公開 - * private ... プロフィールには表示しない - */ - @Column('varchar', { - length: 512, default: 'public', - }) - public visibility: FlashVisibility; } diff --git a/packages/backend/src/models/entities/FlashLike.ts b/packages/backend/src/models/entities/FlashLike.ts new file mode 100644 index 0000000000..81d39191ca --- /dev/null +++ b/packages/backend/src/models/entities/FlashLike.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Flash } from './Flash.js'; + +@Entity() +@Index(['userId', 'flashId'], { unique: true }) +export class FlashLike { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public flashId: Flash['id']; + + @ManyToOne(type => Flash, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public flash: Flash | null; +} diff --git a/packages/backend/src/models/FollowRequest.ts b/packages/backend/src/models/entities/FollowRequest.ts similarity index 73% rename from packages/backend/src/models/FollowRequest.ts rename to packages/backend/src/models/entities/FollowRequest.ts index 3ff5e7a478..0988e7e504 100644 --- a/packages/backend/src/models/FollowRequest.ts +++ b/packages/backend/src/models/entities/FollowRequest.ts @@ -1,43 +1,43 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('follow_request') +@Entity() @Index(['followerId', 'followeeId'], { unique: true }) -export class MiFollowRequest { +export class FollowRequest { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone', { + comment: 'The created date of the FollowRequest.', + }) + public createdAt: Date; + @Index() @Column({ ...id(), comment: 'The followee user ID.', }) - public followeeId: MiUser['id']; + public followeeId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public followee: MiUser | null; + public followee: User | null; @Index() @Column({ ...id(), comment: 'The follower user ID.', }) - public followerId: MiUser['id']; + public followerId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public follower: MiUser | null; + public follower: User | null; @Column('varchar', { length: 128, nullable: true, @@ -45,11 +45,6 @@ export class MiFollowRequest { }) public requestId: string | null; - @Column('boolean', { - default: false, - }) - public withReplies: boolean; - //#region Denormalized fields @Column('varchar', { length: 128, nullable: true, diff --git a/packages/backend/src/models/Following.ts b/packages/backend/src/models/entities/Following.ts similarity index 61% rename from packages/backend/src/models/Following.ts rename to packages/backend/src/models/entities/Following.ts index 62cbc29f26..112afd7e6e 100644 --- a/packages/backend/src/models/Following.ts +++ b/packages/backend/src/models/entities/Following.ts @@ -1,62 +1,44 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('following') +@Entity() @Index(['followerId', 'followeeId'], { unique: true }) -@Index(['followeeId', 'followerHost', 'isFollowerHibernated']) -export class MiFollowing { +export class Following { @PrimaryColumn(id()) public id: string; + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Following.', + }) + public createdAt: Date; + @Index() @Column({ ...id(), comment: 'The followee user ID.', }) - public followeeId: MiUser['id']; + public followeeId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public followee: MiUser | null; + public followee: User | null; @Index() @Column({ ...id(), comment: 'The follower user ID.', }) - public followerId: MiUser['id']; + public followerId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public follower: MiUser | null; - - @Column('boolean', { - default: false, - }) - public isFollowerHibernated: boolean; - - // タイムラインにその人のリプライまで含めるかどうか - @Column('boolean', { - default: false, - }) - public withReplies: boolean; - - @Index() - @Column('varchar', { - length: 32, - nullable: true, - }) - public notify: 'normal' | null; + public follower: User | null; //#region Denormalized fields @Index() diff --git a/packages/backend/src/models/entities/GalleryLike.ts b/packages/backend/src/models/entities/GalleryLike.ts new file mode 100644 index 0000000000..cc54b528e9 --- /dev/null +++ b/packages/backend/src/models/entities/GalleryLike.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { GalleryPost } from './GalleryPost.js'; + +@Entity() +@Index(['userId', 'postId'], { unique: true }) +export class GalleryLike { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public postId: GalleryPost['id']; + + @ManyToOne(type => GalleryPost, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public post: GalleryPost | null; +} diff --git a/packages/backend/src/models/GalleryPost.ts b/packages/backend/src/models/entities/GalleryPost.ts similarity index 69% rename from packages/backend/src/models/GalleryPost.ts rename to packages/backend/src/models/entities/GalleryPost.ts index 04d8823e37..36e879afa7 100644 --- a/packages/backend/src/models/GalleryPost.ts +++ b/packages/backend/src/models/entities/GalleryPost.ts @@ -1,18 +1,19 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import type { MiDriveFile } from './DriveFile.js'; +import { id } from '../id.js'; +import { User } from './User.js'; +import type { DriveFile } from './DriveFile.js'; -@Entity('gallery_post') -export class MiGalleryPost { +@Entity() +export class GalleryPost { @PrimaryColumn(id()) public id: string; + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the GalleryPost.', + }) + public createdAt: Date; + @Index() @Column('timestamp with time zone', { comment: 'The updated date of the GalleryPost.', @@ -34,20 +35,20 @@ export class MiGalleryPost { ...id(), comment: 'The ID of author.', }) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Index() @Column({ ...id(), array: true, default: '{}', }) - public fileIds: MiDriveFile['id'][]; + public fileIds: DriveFile['id'][]; @Index() @Column('boolean', { @@ -68,7 +69,7 @@ export class MiGalleryPost { }) public tags: string[]; - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/Hashtag.ts b/packages/backend/src/models/entities/Hashtag.ts similarity index 66% rename from packages/backend/src/models/Hashtag.ts rename to packages/backend/src/models/entities/Hashtag.ts index 3add06d0c3..2d6bfaa045 100644 --- a/packages/backend/src/models/Hashtag.ts +++ b/packages/backend/src/models/entities/Hashtag.ts @@ -1,14 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; -import { id } from './util/id.js'; -import type { MiUser } from './User.js'; +import { id } from '../id.js'; +import type { User } from './User.js'; -@Entity('hashtag') -export class MiHashtag { +@Entity() +export class Hashtag { @PrimaryColumn(id()) public id: string; @@ -22,7 +17,7 @@ export class MiHashtag { ...id(), array: true, }) - public mentionedUserIds: MiUser['id'][]; + public mentionedUserIds: User['id'][]; @Index() @Column('integer', { @@ -34,7 +29,7 @@ export class MiHashtag { ...id(), array: true, }) - public mentionedLocalUserIds: MiUser['id'][]; + public mentionedLocalUserIds: User['id'][]; @Index() @Column('integer', { @@ -46,7 +41,7 @@ export class MiHashtag { ...id(), array: true, }) - public mentionedRemoteUserIds: MiUser['id'][]; + public mentionedRemoteUserIds: User['id'][]; @Index() @Column('integer', { @@ -58,7 +53,7 @@ export class MiHashtag { ...id(), array: true, }) - public attachedUserIds: MiUser['id'][]; + public attachedUserIds: User['id'][]; @Index() @Column('integer', { @@ -70,7 +65,7 @@ export class MiHashtag { ...id(), array: true, }) - public attachedLocalUserIds: MiUser['id'][]; + public attachedLocalUserIds: User['id'][]; @Index() @Column('integer', { @@ -82,7 +77,7 @@ export class MiHashtag { ...id(), array: true, }) - public attachedRemoteUserIds: MiUser['id'][]; + public attachedRemoteUserIds: User['id'][]; @Index() @Column('integer', { diff --git a/packages/backend/src/models/Instance.ts b/packages/backend/src/models/entities/Instance.ts similarity index 78% rename from packages/backend/src/models/Instance.ts rename to packages/backend/src/models/entities/Instance.ts index 17cd5c6665..09328b57f8 100644 --- a/packages/backend/src/models/Instance.ts +++ b/packages/backend/src/models/entities/Instance.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; -import { id } from './util/id.js'; +import { id } from '../id.js'; -@Entity('instance') -export class MiInstance { +@Entity() +export class Instance { @PrimaryColumn(id()) public id: string; @@ -81,22 +76,13 @@ export class MiInstance { public isNotResponding: boolean; /** - * このインスタンスと不通になった日時 - */ - @Column('timestamp with time zone', { - nullable: true, - }) - public notRespondingSince: Date | null; - - /** - * このインスタンスへの配信状態 + * このインスタンスへの配信を停止するか */ @Index() - @Column('enum', { - default: 'none', - enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'], + @Column('boolean', { + default: false, }) - public suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'; + public isSuspended: boolean; @Column('varchar', { length: 64, nullable: true, @@ -153,9 +139,4 @@ export class MiInstance { nullable: true, }) public infoUpdatedAt: Date | null; - - @Column('varchar', { - length: 16384, default: '', - }) - public moderationNote: string; } diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/entities/Meta.ts similarity index 58% rename from packages/backend/src/models/Meta.ts rename to packages/backend/src/models/entities/Meta.ts index 3ee6190d45..f799551f30 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/entities/Meta.ts @@ -1,42 +1,21 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ +import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import type { Clip } from './Clip.js'; -import { Entity, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -@Entity('meta') -export class MiMeta { +@Entity() +export class Meta { @PrimaryColumn({ type: 'varchar', length: 32, }) public id: string; - @Column({ - ...id(), - nullable: true, - }) - public rootUserId: MiUser['id'] | null; - - @ManyToOne(type => MiUser, { - onDelete: 'SET NULL', - nullable: true, - }) - public rootUser: MiUser | null; - @Column('varchar', { length: 1024, nullable: true, }) public name: string | null; - @Column('varchar', { - length: 64, nullable: true, - }) - public shortName: string | null; - @Column('varchar', { length: 1024, nullable: true, }) @@ -88,26 +67,6 @@ export class MiMeta { }) public sensitiveWords: string[]; - @Column('varchar', { - length: 1024, array: true, default: '{}', - }) - public prohibitedWords: string[]; - - @Column('varchar', { - length: 1024, array: true, default: '{}', - }) - public prohibitedWordsForNameOfUser: string[]; - - @Column('varchar', { - length: 1024, array: true, default: '{}', - }) - public silencedHosts: string[]; - - @Column('varchar', { - length: 1024, array: true, default: '{}', - }) - public mediaSilencedHosts: string[]; - @Column('varchar', { length: 1024, nullable: true, @@ -144,18 +103,6 @@ export class MiMeta { }) public iconUrl: string | null; - @Column('varchar', { - length: 1024, - nullable: true, - }) - public app192IconUrl: string | null; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public app512IconUrl: string | null; - @Column('varchar', { length: 1024, nullable: true, @@ -175,14 +122,21 @@ export class MiMeta { public infoImageUrl: string | null; @Column('boolean', { - default: false, + default: true, }) public cacheRemoteFiles: boolean; - @Column('boolean', { - default: true, + @Column({ + ...id(), + nullable: true, }) - public cacheRemoteSensitiveFiles: boolean; + public proxyAccountId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'SET NULL', + }) + @JoinColumn() + public proxyAccount: User | null; @Column('boolean', { default: false, @@ -206,29 +160,6 @@ export class MiMeta { }) public hcaptchaSecretKey: string | null; - @Column('boolean', { - default: false, - }) - public enableMcaptcha: boolean; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public mcaptchaSitekey: string | null; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public mcaptchaSecretKey: string | null; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public mcaptchaInstanceUrl: string | null; - @Column('boolean', { default: false, }) @@ -263,13 +194,6 @@ export class MiMeta { }) public turnstileSecretKey: string | null; - @Column('boolean', { - default: false, - }) - public enableTestcaptcha: boolean; - - // chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること - @Column('enum', { enum: ['none', 'all', 'local', 'remote'], default: 'none', @@ -292,6 +216,12 @@ export class MiMeta { }) public enableSensitiveMediaDetectionForVideos: boolean; + @Column('varchar', { + length: 1024, + nullable: true, + }) + public summalyProxy: string | null; + @Column('boolean', { default: false, }) @@ -368,9 +298,9 @@ export class MiMeta { @Column('varchar', { length: 1024, default: 'https://github.com/misskey-dev/misskey', - nullable: true, + nullable: false, }) - public repositoryUrl: string | null; + public repositoryUrl: string; @Column('varchar', { length: 1024, @@ -379,24 +309,6 @@ export class MiMeta { }) public feedbackUrl: string | null; - @Column('varchar', { - length: 1024, - nullable: true, - }) - public impressumUrl: string | null; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public privacyPolicyUrl: string | null; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public inquiryUrl: string | null; - @Column('varchar', { length: 8192, nullable: true, @@ -491,34 +403,6 @@ export class MiMeta { }) public enableActiveEmailValidation: boolean; - @Column('boolean', { - default: false, - }) - public enableVerifymailApi: boolean; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public verifymailAuthKey: string | null; - - @Column('boolean', { - default: false, - }) - public enableTruemailApi: boolean; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public truemailInstance: string | null; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public truemailAuthKey: string | null; - @Column('boolean', { default: true, }) @@ -529,21 +413,6 @@ export class MiMeta { }) public enableChartsForFederatedInstances: boolean; - @Column('boolean', { - default: true, - }) - public enableStatsForFederatedInstances: boolean; - - @Column('boolean', { - default: false, - }) - public enableServerMachineStats: boolean; - - @Column('boolean', { - default: true, - }) - public enableIdenticonGeneration: boolean; - @Column('jsonb', { default: { }, }) @@ -556,153 +425,8 @@ export class MiMeta { }) public serverRules: string[]; - @Column('varchar', { - length: 8192, - default: '{}', - }) - public manifestJsonOverride: string; - - @Column('varchar', { - length: 1024, - array: true, - default: '{}', - }) - public bannedEmailDomains: string[]; - @Column('varchar', { length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }', }) public preservedUsernames: string[]; - - @Column('boolean', { - default: true, - }) - public enableFanoutTimeline: boolean; - - @Column('boolean', { - default: true, - }) - public enableFanoutTimelineDbFallback: boolean; - - @Column('integer', { - default: 300, - }) - public perLocalUserUserTimelineCacheMax: number; - - @Column('integer', { - default: 100, - }) - public perRemoteUserUserTimelineCacheMax: number; - - @Column('integer', { - default: 300, - }) - public perUserHomeTimelineCacheMax: number; - - @Column('integer', { - default: 300, - }) - public perUserListTimelineCacheMax: number; - - @Column('boolean', { - default: false, - }) - public enableReactionsBuffering: boolean; - - @Column('integer', { - default: 0, - }) - public notesPerOneAd: number; - - @Column('boolean', { - default: true, - }) - public urlPreviewEnabled: boolean; - - @Column('boolean', { - default: true, - }) - public urlPreviewAllowRedirect: boolean; - - @Column('integer', { - default: 10000, - }) - public urlPreviewTimeout: number; - - @Column('bigint', { - default: 1024 * 1024 * 10, - }) - public urlPreviewMaximumContentLength: number; - - @Column('boolean', { - default: true, - }) - public urlPreviewRequireContentLength: boolean; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public urlPreviewSummaryProxyUrl: string | null; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public urlPreviewUserAgent: string | null; - - @Column('varchar', { - length: 128, - default: 'all', - }) - public federation: 'all' | 'specified' | 'none'; - - @Column('varchar', { - length: 1024, - array: true, - default: '{}', - }) - public federationHosts: string[]; - - @Column('varchar', { - length: 128, - default: 'local', - }) - public ugcVisibilityForVisitor: 'all' | 'local' | 'none'; - - @Column('varchar', { - length: 64, - nullable: true, - }) - public googleAnalyticsMeasurementId: string | null; - - @Column('jsonb', { - default: [], - }) - public deliverSuspendedSoftware: SoftwareSuspension[]; - - @Column('boolean', { - default: false, - }) - public singleUserMode: boolean; - - @Column('boolean', { - default: true, - }) - public proxyRemoteFiles: boolean; - - @Column('boolean', { - default: true, - }) - public signToActivityPubGet: boolean; - - @Column('boolean', { - default: true, - }) - public allowExternalApRedirect: boolean; } - -export type SoftwareSuspension = { - software: string, - versionRange: string, -}; diff --git a/packages/backend/src/models/ModerationLog.ts b/packages/backend/src/models/entities/ModerationLog.ts similarity index 50% rename from packages/backend/src/models/ModerationLog.ts rename to packages/backend/src/models/entities/ModerationLog.ts index edde315fdf..ab6a226cf7 100644 --- a/packages/backend/src/models/ModerationLog.ts +++ b/packages/backend/src/models/entities/ModerationLog.ts @@ -1,26 +1,26 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('moderation_log') -export class MiModerationLog { +@Entity() +export class ModerationLog { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone', { + comment: 'The created date of the ModerationLog.', + }) + public createdAt: Date; + @Index() @Column(id()) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/entities/MutedNote.ts b/packages/backend/src/models/entities/MutedNote.ts new file mode 100644 index 0000000000..78347d8917 --- /dev/null +++ b/packages/backend/src/models/entities/MutedNote.ts @@ -0,0 +1,48 @@ +import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; +import { id } from '../id.js'; +import { mutedNoteReasons } from '../../types.js'; +import { Note } from './Note.js'; +import { User } from './User.js'; + +@Entity() +@Index(['noteId', 'userId'], { unique: true }) +export class MutedNote { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + comment: 'The note ID.', + }) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; + + @Index() + @Column({ + ...id(), + comment: 'The user ID.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + /** + * ミュートされた理由。 + */ + @Index() + @Column('enum', { + enum: mutedNoteReasons, + comment: 'The reason of the MutedNote.', + }) + public reason: typeof mutedNoteReasons[number]; +} diff --git a/packages/backend/src/models/Muting.ts b/packages/backend/src/models/entities/Muting.ts similarity index 56% rename from packages/backend/src/models/Muting.ts rename to packages/backend/src/models/entities/Muting.ts index e1240b9c4e..bf5498b96a 100644 --- a/packages/backend/src/models/Muting.ts +++ b/packages/backend/src/models/entities/Muting.ts @@ -1,18 +1,19 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('muting') +@Entity() @Index(['muterId', 'muteeId'], { unique: true }) -export class MiMuting { +export class Muting { @PrimaryColumn(id()) public id: string; + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Muting.', + }) + public createdAt: Date; + @Index() @Column('timestamp with time zone', { nullable: true, @@ -24,24 +25,24 @@ export class MiMuting { ...id(), comment: 'The mutee user ID.', }) - public muteeId: MiUser['id']; + public muteeId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public mutee: MiUser | null; + public mutee: User | null; @Index() @Column({ ...id(), comment: 'The muter user ID.', }) - public muterId: MiUser['id']; + public muterId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public muter: MiUser | null; + public muter: User | null; } diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/entities/Note.ts similarity index 61% rename from packages/backend/src/models/Note.ts rename to packages/backend/src/models/entities/Note.ts index 3dcbdb735b..4f49a05950 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/entities/Note.ts @@ -1,44 +1,37 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { noteVisibilities } from '@/types.js'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiChannel } from './Channel.js'; -import type { MiDriveFile } from './DriveFile.js'; +import { id } from '../id.js'; +import { noteVisibilities } from '../../types.js'; +import { User } from './User.js'; +import { Channel } from './Channel.js'; +import type { DriveFile } from './DriveFile.js'; -// Note: When you create a new index for existing column of this table, -// it might be better to index concurrently under isConcurrentIndexMigrationEnabled flag -// by editing generated migration file since this table is very large, -// and it will make a long lock to create index in most cases. -// Please note that `CREATE INDEX CONCURRENTLY` is not supported in transaction, -// so you need to set `transaction = false` in migration if isConcurrentIndexMigrationEnabled() is true. -// Please refer 1745378064470-composite-note-index.js for example. -// You should not use `@Index({ concurrent: true })` decorator because database initialization for test will fail -// because it will always run CREATE INDEX in transaction based on decorators. -// Not appending `{ concurrent: true }` to `@Index` will not cause any problem in production, -@Index(['userId', 'id']) -@Entity('note') -export class MiNote { +@Entity() +@Index('IDX_NOTE_TAGS', { synchronize: false }) +@Index('IDX_NOTE_MENTIONS', { synchronize: false }) +@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false }) +export class Note { @PrimaryColumn(id()) public id: string; + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Note.', + }) + public createdAt: Date; + @Index() @Column({ ...id(), nullable: true, comment: 'The ID of reply target.', }) - public replyId: MiNote['id'] | null; + public replyId: Note['id'] | null; - @ManyToOne(type => MiNote, { + @ManyToOne(type => Note, { onDelete: 'CASCADE', }) @JoinColumn() - public reply: MiNote | null; + public reply: Note | null; @Index() @Column({ @@ -46,13 +39,13 @@ export class MiNote { nullable: true, comment: 'The ID of renote target.', }) - public renoteId: MiNote['id'] | null; + public renoteId: Note['id'] | null; - @ManyToOne(type => MiNote, { + @ManyToOne(type => Note, { onDelete: 'CASCADE', }) @JoinColumn() - public renote: MiNote | null; + public renote: Note | null; @Index() @Column('varchar', { @@ -76,17 +69,18 @@ export class MiNote { }) public cw: string | null; + @Index() @Column({ ...id(), comment: 'The ID of author.', }) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column('boolean', { default: false, @@ -108,11 +102,6 @@ export class MiNote { }) public repliesCount: number; - @Column('smallint', { - default: 0, - }) - public clippedCount: number; - @Column('jsonb', { default: {}, }) @@ -140,48 +129,49 @@ export class MiNote { }) public url: string | null; - @Index('IDX_NOTE_FILE_IDS', { synchronize: false }) + @Column('integer', { + default: 0, select: false, + }) + public score: number; + + @Index() @Column({ ...id(), array: true, default: '{}', }) - public fileIds: MiDriveFile['id'][]; + public fileIds: DriveFile['id'][]; + @Index() @Column('varchar', { length: 256, array: true, default: '{}', }) public attachedFileTypes: string[]; - @Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false }) + @Index() @Column({ ...id(), array: true, default: '{}', }) - public visibleUserIds: MiUser['id'][]; + public visibleUserIds: User['id'][]; - @Index('IDX_NOTE_MENTIONS', { synchronize: false }) + @Index() @Column({ ...id(), array: true, default: '{}', }) - public mentions: MiUser['id'][]; + public mentions: User['id'][]; @Column('text', { default: '[]', }) public mentionedRemoteUsers: string; - @Column('varchar', { - length: 1024, array: true, default: '{}', - }) - public reactionAndUserPairCache: string[]; - @Column('varchar', { length: 128, array: true, default: '{}', }) public emojis: string[]; - @Index('IDX_NOTE_TAGS', { synchronize: false }) + @Index() @Column('varchar', { length: 128, array: true, default: '{}', }) @@ -198,13 +188,13 @@ export class MiNote { nullable: true, comment: 'The ID of source channel.', }) - public channelId: MiChannel['id'] | null; + public channelId: Channel['id'] | null; - @ManyToOne(type => MiChannel, { + @ManyToOne(type => Channel, { onDelete: 'CASCADE', }) @JoinColumn() - public channel: MiChannel | null; + public channel: Channel | null; //#region Denormalized fields @Index() @@ -219,7 +209,7 @@ export class MiNote { nullable: true, comment: '[Denormalized]', }) - public replyUserId: MiUser['id'] | null; + public replyUserId: User['id'] | null; @Column('varchar', { length: 128, nullable: true, @@ -232,15 +222,16 @@ export class MiNote { nullable: true, comment: '[Denormalized]', }) - public renoteUserId: MiUser['id'] | null; + public renoteUserId: User['id'] | null; @Column('varchar', { length: 128, nullable: true, comment: '[Denormalized]', }) public renoteUserHost: string | null; + //#endregion - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/entities/NoteFavorite.ts b/packages/backend/src/models/entities/NoteFavorite.ts new file mode 100644 index 0000000000..80c97cb531 --- /dev/null +++ b/packages/backend/src/models/entities/NoteFavorite.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { Note } from './Note.js'; +import { User } from './User.js'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class NoteFavorite { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the NoteFavorite.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; +} diff --git a/packages/backend/src/models/NoteReaction.ts b/packages/backend/src/models/entities/NoteReaction.ts similarity index 55% rename from packages/backend/src/models/NoteReaction.ts rename to packages/backend/src/models/entities/NoteReaction.ts index 42dfcaa9ad..c3c381af56 100644 --- a/packages/backend/src/models/NoteReaction.ts +++ b/packages/backend/src/models/entities/NoteReaction.ts @@ -1,38 +1,39 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiNote } from './Note.js'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Note } from './Note.js'; -@Entity('note_reaction') +@Entity() @Index(['userId', 'noteId'], { unique: true }) -export class MiNoteReaction { +export class NoteReaction { @PrimaryColumn(id()) public id: string; @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', + @Column('timestamp with time zone', { + comment: 'The created date of the NoteReaction.', }) - @JoinColumn() - public user?: MiUser | null; + public createdAt: Date; @Index() @Column(id()) - public noteId: MiNote['id']; + public userId: User['id']; - @ManyToOne(type => MiNote, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public note?: MiNote | null; + public user?: User | null; + + @Index() + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note?: Note | null; // TODO: 対象noteのuserIdを非正規化したい(「受け取ったリアクション一覧」のようなものを(JOIN無しで)実装したいため) diff --git a/packages/backend/src/models/NoteThreadMuting.ts b/packages/backend/src/models/entities/NoteThreadMuting.ts similarity index 50% rename from packages/backend/src/models/NoteThreadMuting.ts rename to packages/backend/src/models/entities/NoteThreadMuting.ts index e7bd39f348..3c884fe615 100644 --- a/packages/backend/src/models/NoteThreadMuting.ts +++ b/packages/backend/src/models/entities/NoteThreadMuting.ts @@ -1,29 +1,28 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('note_thread_muting') +@Entity() @Index(['userId', 'threadId'], { unique: true }) -export class MiNoteThreadMuting { +export class NoteThreadMuting { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone', { + }) + public createdAt: Date; + @Index() @Column({ ...id(), }) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Index() @Column('varchar', { diff --git a/packages/backend/src/models/entities/NoteUnread.ts b/packages/backend/src/models/entities/NoteUnread.ts new file mode 100644 index 0000000000..af91234d0f --- /dev/null +++ b/packages/backend/src/models/entities/NoteUnread.ts @@ -0,0 +1,63 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Note } from './Note.js'; +import type { Channel } from './Channel.js'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class NoteUnread { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; + + /** + * メンションか否か + */ + @Index() + @Column('boolean') + public isMentioned: boolean; + + /** + * ダイレクト投稿か否か + */ + @Index() + @Column('boolean') + public isSpecified: boolean; + + //#region Denormalized fields + @Index() + @Column({ + ...id(), + comment: '[Denormalized]', + }) + public noteUserId: User['id']; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: '[Denormalized]', + }) + public noteChannelId: Channel['id'] | null; + //#endregion +} diff --git a/packages/backend/src/models/entities/Notification.ts b/packages/backend/src/models/entities/Notification.ts new file mode 100644 index 0000000000..aa6f997124 --- /dev/null +++ b/packages/backend/src/models/entities/Notification.ts @@ -0,0 +1,65 @@ +import { notificationTypes } from '@/types.js'; +import { User } from './User.js'; +import { Note } from './Note.js'; +import { FollowRequest } from './FollowRequest.js'; +import { AccessToken } from './AccessToken.js'; + +export type Notification = { + id: string; + + // RedisのためDateではなくstring + createdAt: string; + + /** + * 通知の送信者(initiator) + */ + notifierId: User['id'] | null; + + /** + * 通知の種類。 + * follow - フォローされた + * mention - 投稿で自分が言及された + * reply - 投稿に返信された + * renote - 投稿がRenoteされた + * quote - 投稿が引用Renoteされた + * reaction - 投稿にリアクションされた + * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した + * receiveFollowRequest - フォローリクエストされた + * followRequestAccepted - 自分の送ったフォローリクエストが承認された + * achievementEarned - 実績を獲得 + * app - アプリ通知 + */ + type: typeof notificationTypes[number]; + + noteId: Note['id'] | null; + + followRequestId: FollowRequest['id'] | null; + + reaction: string | null; + + choice: number | null; + + achievement: string | null; + + /** + * アプリ通知のbody + */ + customBody: string | null; + + /** + * アプリ通知のheader + * (省略時はアプリ名で表示されることを期待) + */ + customHeader: string | null; + + /** + * アプリ通知のicon(URL) + * (省略時はアプリアイコンで表示されることを期待) + */ + customIcon: string | null; + + /** + * アプリ通知のアプリ(のトークン) + */ + appAccessTokenId: AccessToken['id'] | null; +} diff --git a/packages/backend/src/models/Page.ts b/packages/backend/src/models/entities/Page.ts similarity index 70% rename from packages/backend/src/models/Page.ts rename to packages/backend/src/models/entities/Page.ts index 0b59e7a92c..6078bc1bc7 100644 --- a/packages/backend/src/models/Page.ts +++ b/packages/backend/src/models/entities/Page.ts @@ -1,19 +1,20 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiDriveFile } from './DriveFile.js'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { DriveFile } from './DriveFile.js'; -@Entity('page') +@Entity() @Index(['userId', 'name'], { unique: true }) -export class MiPage { +export class Page { @PrimaryColumn(id()) public id: string; + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Page.', + }) + public createdAt: Date; + @Index() @Column('timestamp with time zone', { comment: 'The updated date of the Page.', @@ -54,25 +55,25 @@ export class MiPage { ...id(), comment: 'The ID of author.', }) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column({ ...id(), nullable: true, }) - public eyeCatchingImageId: MiDriveFile['id'] | null; + public eyeCatchingImageId: DriveFile['id'] | null; - @ManyToOne(type => MiDriveFile, { + @ManyToOne(type => DriveFile, { onDelete: 'CASCADE', }) @JoinColumn() - public eyeCatchingImage: MiDriveFile | null; + public eyeCatchingImage: DriveFile | null; @Column('jsonb', { default: [], @@ -103,14 +104,14 @@ export class MiPage { ...id(), array: true, default: '{}', }) - public visibleUserIds: MiUser['id'][]; + public visibleUserIds: User['id'][]; @Column('integer', { default: 0, }) public likedCount: number; - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { @@ -118,5 +119,3 @@ export class MiPage { } } } - -export const pageNameSchema = { type: 'string', pattern: /^[^\s:\/?#\[\]@!$&'()*+,;=\\%\x00-\x20]{1,256}$/.source } as const; diff --git a/packages/backend/src/models/entities/PageLike.ts b/packages/backend/src/models/entities/PageLike.ts new file mode 100644 index 0000000000..f8c5943a3e --- /dev/null +++ b/packages/backend/src/models/entities/PageLike.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Page } from './Page.js'; + +@Entity() +@Index(['userId', 'pageId'], { unique: true }) +export class PageLike { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public pageId: Page['id']; + + @ManyToOne(type => Page, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public page: Page | null; +} diff --git a/packages/backend/src/models/entities/PasswordResetRequest.ts b/packages/backend/src/models/entities/PasswordResetRequest.ts new file mode 100644 index 0000000000..939fcc460f --- /dev/null +++ b/packages/backend/src/models/entities/PasswordResetRequest.ts @@ -0,0 +1,30 @@ +import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class PasswordResetRequest { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index({ unique: true }) + @Column('varchar', { + length: 256, + }) + public token: string; + + @Index() + @Column({ + ...id(), + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; +} diff --git a/packages/backend/src/models/Poll.ts b/packages/backend/src/models/entities/Poll.ts similarity index 62% rename from packages/backend/src/models/Poll.ts rename to packages/backend/src/models/entities/Poll.ts index ca985c8b24..ee1d646020 100644 --- a/packages/backend/src/models/Poll.ts +++ b/packages/backend/src/models/entities/Poll.ts @@ -1,25 +1,19 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; -import { noteVisibilities } from '@/types.js'; -import { id } from './util/id.js'; -import { MiNote } from './Note.js'; -import type { MiUser } from './User.js'; -import type { MiChannel } from "@/models/Channel.js"; +import { id } from '../id.js'; +import { noteVisibilities } from '../../types.js'; +import { Note } from './Note.js'; +import type { User } from './User.js'; -@Entity('poll') -export class MiPoll { +@Entity() +export class Poll { @PrimaryColumn(id()) - public noteId: MiNote['id']; + public noteId: Note['id']; - @OneToOne(type => MiNote, { + @OneToOne(type => Note, { onDelete: 'CASCADE', }) @JoinColumn() - public note: MiNote | null; + public note: Note | null; @Column('timestamp with time zone', { nullable: true, @@ -51,7 +45,7 @@ export class MiPoll { ...id(), comment: '[Denormalized]', }) - public userId: MiUser['id']; + public userId: User['id']; @Index() @Column('varchar', { @@ -59,17 +53,9 @@ export class MiPoll { comment: '[Denormalized]', }) public userHost: string | null; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: '[Denormalized]', - }) - public channelId: MiChannel['id'] | null; //#endregion - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/entities/PollVote.ts b/packages/backend/src/models/entities/PollVote.ts new file mode 100644 index 0000000000..d447a7be8f --- /dev/null +++ b/packages/backend/src/models/entities/PollVote.ts @@ -0,0 +1,40 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Note } from './Note.js'; + +@Entity() +@Index(['userId', 'noteId', 'choice'], { unique: true }) +export class PollVote { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the PollVote.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; + + @Column('integer') + public choice: number; +} diff --git a/packages/backend/src/models/entities/PromoNote.ts b/packages/backend/src/models/entities/PromoNote.ts new file mode 100644 index 0000000000..958008338a --- /dev/null +++ b/packages/backend/src/models/entities/PromoNote.ts @@ -0,0 +1,28 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; +import { id } from '../id.js'; +import { Note } from './Note.js'; +import type { User } from './User.js'; + +@Entity() +export class PromoNote { + @PrimaryColumn(id()) + public noteId: Note['id']; + + @OneToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; + + @Column('timestamp with time zone') + public expiresAt: Date; + + //#region Denormalized fields + @Index() + @Column({ + ...id(), + comment: '[Denormalized]', + }) + public userId: User['id']; + //#endregion +} diff --git a/packages/backend/src/models/entities/PromoRead.ts b/packages/backend/src/models/entities/PromoRead.ts new file mode 100644 index 0000000000..27f5d0dc11 --- /dev/null +++ b/packages/backend/src/models/entities/PromoRead.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { Note } from './Note.js'; +import { User } from './User.js'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class PromoRead { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the PromoRead.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; +} diff --git a/packages/backend/src/models/entities/RegistrationTicket.ts b/packages/backend/src/models/entities/RegistrationTicket.ts new file mode 100644 index 0000000000..139e40f85e --- /dev/null +++ b/packages/backend/src/models/entities/RegistrationTicket.ts @@ -0,0 +1,17 @@ +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from '../id.js'; + +@Entity() +export class RegistrationTicket { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index({ unique: true }) + @Column('varchar', { + length: 64, + }) + public code: string; +} diff --git a/packages/backend/src/models/RegistryItem.ts b/packages/backend/src/models/entities/RegistryItem.ts similarity index 75% rename from packages/backend/src/models/RegistryItem.ts rename to packages/backend/src/models/entities/RegistryItem.ts index 335e8b9eab..670a236ea0 100644 --- a/packages/backend/src/models/RegistryItem.ts +++ b/packages/backend/src/models/entities/RegistryItem.ts @@ -1,18 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; // TODO: 同じdomain、同じscope、同じkeyのレコードは二つ以上存在しないように制約付けたい -@Entity('registry_item') -export class MiRegistryItem { +@Entity() +export class RegistryItem { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone', { + comment: 'The created date of the RegistryItem.', + }) + public createdAt: Date; + @Column('timestamp with time zone', { comment: 'The updated date of the RegistryItem.', }) @@ -23,13 +23,13 @@ export class MiRegistryItem { ...id(), comment: 'The owner ID.', }) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column('varchar', { length: 1024, diff --git a/packages/backend/src/models/Relay.ts b/packages/backend/src/models/entities/Relay.ts similarity index 65% rename from packages/backend/src/models/Relay.ts rename to packages/backend/src/models/entities/Relay.ts index eca2916032..94d1929574 100644 --- a/packages/backend/src/models/Relay.ts +++ b/packages/backend/src/models/entities/Relay.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; -import { id } from './util/id.js'; +import { id } from '../id.js'; -@Entity('relay') -export class MiRelay { +@Entity() +export class Relay { @PrimaryColumn(id()) public id: string; diff --git a/packages/backend/src/models/entities/RenoteMuting.ts b/packages/backend/src/models/entities/RenoteMuting.ts new file mode 100644 index 0000000000..2f803a5fa8 --- /dev/null +++ b/packages/backend/src/models/entities/RenoteMuting.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +@Index(['muterId', 'muteeId'], { unique: true }) +export class RenoteMuting { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Muting.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The mutee user ID.', + }) + public muteeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public mutee: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The muter user ID.', + }) + public muterId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public muter: User | null; +} diff --git a/packages/backend/src/models/RetentionAggregation.ts b/packages/backend/src/models/entities/RetentionAggregation.ts similarity index 69% rename from packages/backend/src/models/RetentionAggregation.ts rename to packages/backend/src/models/entities/RetentionAggregation.ts index 139f3e4dfd..c7bf38b3af 100644 --- a/packages/backend/src/models/RetentionAggregation.ts +++ b/packages/backend/src/models/entities/RetentionAggregation.ts @@ -1,14 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; -import { id } from './util/id.js'; -import type { MiUser } from './User.js'; +import { id } from '../id.js'; +import type { User } from './User.js'; -@Entity('retention_aggregation') -export class MiRetentionAggregation { +@Entity() +export class RetentionAggregation { @PrimaryColumn(id()) public id: string; @@ -33,7 +28,7 @@ export class MiRetentionAggregation { ...id(), array: true, }) - public userIds: MiUser['id'][]; + public userIds: User['id'][]; @Column('integer', { }) diff --git a/packages/backend/src/models/Role.ts b/packages/backend/src/models/entities/Role.ts similarity index 56% rename from packages/backend/src/models/Role.ts rename to packages/backend/src/models/entities/Role.ts index 4c7da252bd..61f40d59da 100644 --- a/packages/backend/src/models/Role.ts +++ b/packages/backend/src/models/entities/Role.ts @@ -1,171 +1,75 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Entity, Column, PrimaryColumn } from 'typeorm'; -import { id } from './util/id.js'; +import { id } from '../id.js'; -/** - * ~かつ~ - * 複数の条件を同時に満たす場合のみ成立とする - */ type CondFormulaValueAnd = { type: 'and'; values: RoleCondFormulaValue[]; }; -/** - * ~または~ - * 複数の条件のうち、いずれかを満たす場合のみ成立とする - */ type CondFormulaValueOr = { type: 'or'; values: RoleCondFormulaValue[]; }; -/** - * ~ではない - * 条件を満たさない場合のみ成立とする - */ type CondFormulaValueNot = { type: 'not'; value: RoleCondFormulaValue; }; -/** - * ローカルユーザーのみ成立とする - */ type CondFormulaValueIsLocal = { type: 'isLocal'; }; -/** - * リモートユーザーのみ成立とする - */ type CondFormulaValueIsRemote = { type: 'isRemote'; }; -/** - * 既に指定のマニュアルロールにアサインされている場合のみ成立とする - */ -type CondFormulaValueRoleAssignedTo = { - type: 'roleAssignedTo'; - roleId: string; -}; - -/** - * サスペンド済みアカウントの場合のみ成立とする - */ -type CondFormulaValueIsSuspended = { - type: 'isSuspended'; -}; - -/** - * 鍵アカウントの場合のみ成立とする - */ -type CondFormulaValueIsLocked = { - type: 'isLocked'; -}; - -/** - * botアカウントの場合のみ成立とする - */ -type CondFormulaValueIsBot = { - type: 'isBot'; -}; - -/** - * 猫アカウントの場合のみ成立とする - */ -type CondFormulaValueIsCat = { - type: 'isCat'; -}; - -/** - * 「ユーザを見つけやすくする」が有効なアカウントの場合のみ成立とする - */ -type CondFormulaValueIsExplorable = { - type: 'isExplorable'; -}; - -/** - * ユーザが作成されてから指定期間経過した場合のみ成立とする - */ type CondFormulaValueCreatedLessThan = { type: 'createdLessThan'; sec: number; }; -/** - * ユーザが作成されてから指定期間経っていない場合のみ成立とする - */ type CondFormulaValueCreatedMoreThan = { type: 'createdMoreThan'; sec: number; }; -/** - * フォロワー数が指定値以下の場合のみ成立とする - */ type CondFormulaValueFollowersLessThanOrEq = { type: 'followersLessThanOrEq'; value: number; }; -/** - * フォロワー数が指定値以上の場合のみ成立とする - */ type CondFormulaValueFollowersMoreThanOrEq = { type: 'followersMoreThanOrEq'; value: number; }; -/** - * フォロー数が指定値以下の場合のみ成立とする - */ type CondFormulaValueFollowingLessThanOrEq = { type: 'followingLessThanOrEq'; value: number; }; -/** - * フォロー数が指定値以上の場合のみ成立とする - */ type CondFormulaValueFollowingMoreThanOrEq = { type: 'followingMoreThanOrEq'; value: number; }; -/** - * 投稿数が指定値以下の場合のみ成立とする - */ type CondFormulaValueNotesLessThanOrEq = { type: 'notesLessThanOrEq'; value: number; }; -/** - * 投稿数が指定値以上の場合のみ成立とする - */ type CondFormulaValueNotesMoreThanOrEq = { type: 'notesMoreThanOrEq'; value: number; }; -export type RoleCondFormulaValue = { id: string } & ( +export type RoleCondFormulaValue = CondFormulaValueAnd | CondFormulaValueOr | CondFormulaValueNot | CondFormulaValueIsLocal | CondFormulaValueIsRemote | - CondFormulaValueIsSuspended | - CondFormulaValueIsLocked | - CondFormulaValueIsBot | - CondFormulaValueIsCat | - CondFormulaValueIsExplorable | - CondFormulaValueRoleAssignedTo | CondFormulaValueCreatedLessThan | CondFormulaValueCreatedMoreThan | CondFormulaValueFollowersLessThanOrEq | @@ -173,14 +77,18 @@ export type RoleCondFormulaValue = { id: string } & ( CondFormulaValueFollowingLessThanOrEq | CondFormulaValueFollowingMoreThanOrEq | CondFormulaValueNotesLessThanOrEq | - CondFormulaValueNotesMoreThanOrEq -); + CondFormulaValueNotesMoreThanOrEq; -@Entity('role') -export class MiRole { +@Entity() +export class Role { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone', { + comment: 'The created date of the Role.', + }) + public createdAt: Date; + @Column('timestamp with time zone', { comment: 'The updated date of the Role.', }) @@ -248,11 +156,6 @@ export class MiRole { }) public isExplorable: boolean; - @Column('boolean', { - default: false, - }) - public preserveAssignmentOnMoveAccount: boolean; - @Column('boolean', { default: false, }) diff --git a/packages/backend/src/models/RoleAssignment.ts b/packages/backend/src/models/entities/RoleAssignment.ts similarity index 52% rename from packages/backend/src/models/RoleAssignment.ts rename to packages/backend/src/models/entities/RoleAssignment.ts index 37755d631b..972810940f 100644 --- a/packages/backend/src/models/RoleAssignment.ts +++ b/packages/backend/src/models/entities/RoleAssignment.ts @@ -1,44 +1,44 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiRole } from './Role.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { Role } from './Role.js'; +import { User } from './User.js'; -@Entity('role_assignment') +@Entity() @Index(['userId', 'roleId'], { unique: true }) -export class MiRoleAssignment { +export class RoleAssignment { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone', { + comment: 'The created date of the RoleAssignment.', + }) + public createdAt: Date; + @Index() @Column({ ...id(), comment: 'The user ID.', }) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Index() @Column({ ...id(), comment: 'The role ID.', }) - public roleId: MiRole['id']; + public roleId: Role['id']; - @ManyToOne(type => MiRole, { + @ManyToOne(type => Role, { onDelete: 'CASCADE', }) @JoinColumn() - public role: MiRole | null; + public role: Role | null; @Index() @Column('timestamp with time zone', { diff --git a/packages/backend/src/models/Signin.ts b/packages/backend/src/models/entities/Signin.ts similarity index 54% rename from packages/backend/src/models/Signin.ts rename to packages/backend/src/models/entities/Signin.ts index f8ff9c57d7..380bf028a6 100644 --- a/packages/backend/src/models/Signin.ts +++ b/packages/backend/src/models/entities/Signin.ts @@ -1,26 +1,26 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('signin') -export class MiSignin { +@Entity() +export class Signin { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone', { + comment: 'The created date of the Signin.', + }) + public createdAt: Date; + @Index() @Column(id()) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/SwSubscription.ts b/packages/backend/src/models/entities/SwSubscription.ts similarity index 59% rename from packages/backend/src/models/SwSubscription.ts rename to packages/backend/src/models/entities/SwSubscription.ts index 0c531132b3..0658294983 100644 --- a/packages/backend/src/models/SwSubscription.ts +++ b/packages/backend/src/models/entities/SwSubscription.ts @@ -1,26 +1,24 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('sw_subscription') -export class MiSwSubscription { +@Entity() +export class SwSubscription { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone') + public createdAt: Date; + @Index() @Column(id()) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column('varchar', { length: 512, diff --git a/packages/backend/src/models/UsedUsername.ts b/packages/backend/src/models/entities/UsedUsername.ts similarity index 59% rename from packages/backend/src/models/UsedUsername.ts rename to packages/backend/src/models/entities/UsedUsername.ts index fbfc126763..eb90bef6ca 100644 --- a/packages/backend/src/models/UsedUsername.ts +++ b/packages/backend/src/models/entities/UsedUsername.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Column } from 'typeorm'; -@Entity('used_username') -export class MiUsedUsername { +@Entity() +export class UsedUsername { @PrimaryColumn('varchar', { length: 128, }) @@ -15,7 +10,7 @@ export class MiUsedUsername { @Column('timestamp with time zone') public createdAt: Date; - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/entities/User.ts similarity index 69% rename from packages/backend/src/models/User.ts rename to packages/backend/src/models/entities/User.ts index baf4eefdf1..6669890cf6 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/entities/User.ts @@ -1,18 +1,19 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; -import { id } from './util/id.js'; -import { MiDriveFile } from './DriveFile.js'; +import { id } from '../id.js'; +import { DriveFile } from './DriveFile.js'; -@Entity('user') +@Entity() @Index(['usernameLower', 'host'], { unique: true }) -export class MiUser { +export class User { @PrimaryColumn(id()) public id: string; + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the User.', + }) + public createdAt: Date; + @Index() @Column('timestamp with time zone', { nullable: true, @@ -97,73 +98,53 @@ export class MiUser { nullable: true, comment: 'The ID of avatar DriveFile.', }) - public avatarId: MiDriveFile['id'] | null; + public avatarId: DriveFile['id'] | null; - @OneToOne(type => MiDriveFile, { + @OneToOne(type => DriveFile, { onDelete: 'SET NULL', }) @JoinColumn() - public avatar: MiDriveFile | null; + public avatar: DriveFile | null; @Column({ ...id(), nullable: true, comment: 'The ID of banner DriveFile.', }) - public bannerId: MiDriveFile['id'] | null; + public bannerId: DriveFile['id'] | null; - @OneToOne(type => MiDriveFile, { + @OneToOne(type => DriveFile, { onDelete: 'SET NULL', }) @JoinColumn() - public banner: MiDriveFile | null; + public banner: DriveFile | null; - // avatarId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは avatarId の non-null チェックをすること @Column('varchar', { length: 512, nullable: true, }) public avatarUrl: string | null; - // bannerId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは bannerId の non-null チェックをすること @Column('varchar', { length: 512, nullable: true, }) public bannerUrl: string | null; - // avatarId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは avatarId の non-null チェックをすること @Column('varchar', { length: 128, nullable: true, }) public avatarBlurhash: string | null; - // bannerId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは bannerId の non-null チェックをすること @Column('varchar', { length: 128, nullable: true, }) public bannerBlurhash: string | null; - @Column('jsonb', { - default: [], - }) - public avatarDecorations: { - id: string; - angle?: number; - flipH?: boolean; - offsetX?: number; - offsetY?: number; - }[]; - @Index() @Column('varchar', { length: 128, array: true, default: '{}', }) public tags: string[]; - @Column('integer', { - default: 0, - }) - public score: number; - @Column('boolean', { default: false, comment: 'Whether the User is suspended.', @@ -188,6 +169,12 @@ export class MiUser { }) public isCat: boolean; + @Column('boolean', { + default: false, + comment: 'Whether the User is the root.', + }) + public isRoot: boolean; + @Index() @Column('boolean', { default: true, @@ -195,28 +182,6 @@ export class MiUser { }) public isExplorable: boolean; - @Column('boolean', { - default: false, - }) - public isHibernated: boolean; - - @Column('boolean', { - default: false, - }) - public requireSigninToViewContents: boolean; - - // in sec, マイナスで相対時間 - @Column('integer', { - nullable: true, - }) - public makeNotesFollowersOnlyBefore: number | null; - - // in sec, マイナスで相対時間 - @Column('integer', { - nullable: true, - }) - public makeNotesHiddenBefore: number | null; - // アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ @Column('boolean', { default: false, @@ -229,17 +194,6 @@ export class MiUser { }) public emojis: string[]; - // チャットを許可する相手 - // everyone: 誰からでも - // followers: フォロワーのみ - // following: フォローしているユーザーのみ - // mutual: 相互フォローのみ - // none: 誰からも受け付けない - @Column('varchar', { - length: 128, default: 'mutual', - }) - public chatScope: 'everyone' | 'followers' | 'following' | 'mutual' | 'none'; - @Index() @Column('varchar', { length: 128, nullable: true, @@ -285,7 +239,7 @@ export class MiUser { }) public token: string | null; - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { @@ -294,32 +248,31 @@ export class MiUser { } } -export type MiLocalUser = MiUser & { +export type LocalUser = User & { host: null; uri: null; -}; +} -export type MiPartialLocalUser = Partial & { - id: MiUser['id']; +export type PartialLocalUser = Partial & { + id: User['id']; host: null; uri: null; -}; +} -export type MiRemoteUser = MiUser & { +export type RemoteUser = User & { host: string; uri: string; -}; +} -export type MiPartialRemoteUser = Partial & { - id: MiUser['id']; +export type PartialRemoteUser = Partial & { + id: User['id']; host: string; uri: string; -}; +} export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const; export const passwordSchema = { type: 'string', minLength: 1 } as const; export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const; -export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } as const; export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const; diff --git a/packages/backend/src/models/UserIp.ts b/packages/backend/src/models/entities/UserIp.ts similarity index 56% rename from packages/backend/src/models/UserIp.ts rename to packages/backend/src/models/entities/UserIp.ts index 3e757fcf79..628e3d0361 100644 --- a/packages/backend/src/models/UserIp.ts +++ b/packages/backend/src/models/entities/UserIp.ts @@ -1,15 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Entity, Index, Column, PrimaryGeneratedColumn } from 'typeorm'; -import { id } from './util/id.js'; -import type { MiUser } from './User.js'; +import { id } from '../id.js'; +import type { User } from './User.js'; -@Entity('user_ip') +@Entity() @Index(['userId', 'ip'], { unique: true }) -export class MiUserIp { +export class UserIp { @PrimaryGeneratedColumn() public id: string; @@ -19,7 +14,7 @@ export class MiUserIp { @Index() @Column(id()) - public userId: MiUser['id']; + public userId: User['id']; @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/UserKeypair.ts b/packages/backend/src/models/entities/UserKeypair.ts similarity index 52% rename from packages/backend/src/models/UserKeypair.ts rename to packages/backend/src/models/entities/UserKeypair.ts index f5252d126c..3cd02d3c4f 100644 --- a/packages/backend/src/models/UserKeypair.ts +++ b/packages/backend/src/models/entities/UserKeypair.ts @@ -1,22 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, JoinColumn, Column, OneToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('user_keypair') -export class MiUserKeypair { +@Entity() +export class UserKeypair { @PrimaryColumn(id()) - public userId: MiUser['id']; + public userId: User['id']; - @OneToOne(type => MiUser, { + @OneToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column('varchar', { length: 4096, @@ -28,7 +23,7 @@ export class MiUserKeypair { }) public privateKey: string; - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/UserList.ts b/packages/backend/src/models/entities/UserList.ts similarity index 57% rename from packages/backend/src/models/UserList.ts rename to packages/backend/src/models/entities/UserList.ts index 5fb991a87d..94f3dc3cb3 100644 --- a/packages/backend/src/models/UserList.ts +++ b/packages/backend/src/models/entities/UserList.ts @@ -1,23 +1,23 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('user_list') -export class MiUserList { +@Entity() +export class UserList { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone', { + comment: 'The created date of the UserList.', + }) + public createdAt: Date; + @Index() @Column({ ...id(), comment: 'The owner ID.', }) - public userId: MiUser['id']; + public userId: User['id']; @Index() @Column('boolean', { @@ -25,11 +25,11 @@ export class MiUserList { }) public isPublic: boolean; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/entities/UserListFavorite.ts b/packages/backend/src/models/entities/UserListFavorite.ts new file mode 100644 index 0000000000..e57abb460a --- /dev/null +++ b/packages/backend/src/models/entities/UserListFavorite.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { UserList } from './UserList.js'; + +@Entity() +@Index(['userId', 'userListId'], { unique: true }) +export class UserListFavorite { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public userListId: UserList['id']; + + @ManyToOne(type => UserList, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public userList: UserList | null; +} diff --git a/packages/backend/src/models/entities/UserListJoining.ts b/packages/backend/src/models/entities/UserListJoining.ts new file mode 100644 index 0000000000..a40793a3e8 --- /dev/null +++ b/packages/backend/src/models/entities/UserListJoining.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { UserList } from './UserList.js'; + +@Entity() +@Index(['userId', 'userListId'], { unique: true }) +export class UserListJoining { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserListJoining.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The user ID.', + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The list ID.', + }) + public userListId: UserList['id']; + + @ManyToOne(type => UserList, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public userList: UserList | null; +} diff --git a/packages/backend/src/models/UserMemo.ts b/packages/backend/src/models/entities/UserMemo.ts similarity index 54% rename from packages/backend/src/models/UserMemo.ts rename to packages/backend/src/models/entities/UserMemo.ts index 29e28d290a..7dc34b4346 100644 --- a/packages/backend/src/models/UserMemo.ts +++ b/packages/backend/src/models/entities/UserMemo.ts @@ -1,15 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('user_memo') +@Entity() @Index(['userId', 'targetUserId'], { unique: true }) -export class MiUserMemo { +export class UserMemo { @PrimaryColumn(id()) public id: string; @@ -18,26 +13,26 @@ export class MiUserMemo { ...id(), comment: 'The ID of author.', }) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Index() @Column({ ...id(), comment: 'The ID of target user.', }) - public targetUserId: MiUser['id']; + public targetUserId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public targetUser: MiUser | null; + public targetUser: User | null; @Column('varchar', { length: 2048, diff --git a/packages/backend/src/models/entities/UserNotePining.ts b/packages/backend/src/models/entities/UserNotePining.ts new file mode 100644 index 0000000000..fee95d4f7d --- /dev/null +++ b/packages/backend/src/models/entities/UserNotePining.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { Note } from './Note.js'; +import { User } from './User.js'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class UserNotePining { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserNotePinings.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: Note | null; +} diff --git a/packages/backend/src/models/UserPending.ts b/packages/backend/src/models/entities/UserPending.ts similarity index 67% rename from packages/backend/src/models/UserPending.ts rename to packages/backend/src/models/entities/UserPending.ts index 99f8a22a84..7637948841 100644 --- a/packages/backend/src/models/UserPending.ts +++ b/packages/backend/src/models/entities/UserPending.ts @@ -1,16 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; -import { id } from './util/id.js'; +import { id } from '../id.js'; -@Entity('user_pending') -export class MiUserPending { +@Entity() +export class UserPending { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone') + public createdAt: Date; + @Index({ unique: true }) @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/entities/UserProfile.ts similarity index 56% rename from packages/backend/src/models/UserProfile.ts rename to packages/backend/src/models/entities/UserProfile.ts index c4c1fa5ec9..236ee8f988 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/entities/UserProfile.ts @@ -1,27 +1,21 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; -import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiPage } from './Page.js'; -import { MiUserList } from './UserList.js'; +import { obsoleteNotificationTypes, ffVisibility, notificationTypes } from '@/types.js'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Page } from './Page.js'; // TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも // ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン -@Entity('user_profile') -export class MiUserProfile { +@Entity() +export class UserProfile { @PrimaryColumn(id()) - public userId: MiUser['id']; + public userId: User['id']; - @OneToOne(type => MiUser, { + @OneToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column('varchar', { length: 128, nullable: true, @@ -29,7 +23,6 @@ export class MiUserProfile { }) public location: string | null; - @Index() @Column('char', { length: 10, nullable: true, comment: 'The birthday (YYYY-MM-DD) of the User.', @@ -42,14 +35,6 @@ export class MiUserProfile { }) public description: string | null; - // フォローされた際のメッセージ - @Column('varchar', { - length: 256, nullable: true, - }) - public followedMessage: string | null; - - // TODO: 鍵アカウントの場合の、フォローリクエスト受信時のメッセージも設定できるようにする - @Column('jsonb', { default: [], }) @@ -58,12 +43,6 @@ export class MiUserProfile { value: string; }[]; - @Column('varchar', { - array: true, - default: '{}', - }) - public verifiedLinks: string[]; - @Column('varchar', { length: 32, nullable: true, }) @@ -102,16 +81,10 @@ export class MiUserProfile { public publicReactions: boolean; @Column('enum', { - enum: followingVisibilities, + enum: ffVisibility, default: 'public', }) - public followingVisibility: typeof followingVisibilities[number]; - - @Column('enum', { - enum: followersVisibilities, - default: 'public', - }) - public followersVisibility: typeof followersVisibilities[number]; + public ffVisibility: typeof ffVisibility[number]; @Column('varchar', { length: 128, nullable: true, @@ -123,11 +96,6 @@ export class MiUserProfile { }) public twoFactorSecret: string | null; - @Column('varchar', { - nullable: true, array: true, - }) - public twoFactorBackupSecret: string[] | null; - @Column('boolean', { default: false, }) @@ -213,13 +181,13 @@ export class MiUserProfile { ...id(), nullable: true, }) - public pinnedPageId: MiPage['id'] | null; + public pinnedPageId: Page['id'] | null; - @OneToOne(type => MiPage, { + @OneToOne(type => Page, { onDelete: 'SET NULL', }) @JoinColumn() - public pinnedPage: MiPage | null; + public pinnedPage: Page | null; @Index() @Column('boolean', { @@ -230,12 +198,7 @@ export class MiUserProfile { @Column('jsonb', { default: [], }) - public mutedWords: (string[] | string)[]; - - @Column('jsonb', { - default: [], - }) - public hardMutedWords: (string[] | string)[]; + public mutedWords: string[][]; @Column('jsonb', { default: [], @@ -243,27 +206,16 @@ export class MiUserProfile { }) public mutedInstances: string[]; - @Column('jsonb', { - default: {}, + @Column('enum', { + enum: [ + ...notificationTypes, + // マイグレーションで削除が困難なので古いenumは残しておく + ...obsoleteNotificationTypes, + ], + array: true, + default: [], }) - public notificationRecieveConfig: { - [notificationType in typeof notificationTypes[number]]?: { - type: 'all'; - } | { - type: 'never'; - } | { - type: 'following'; - } | { - type: 'follower'; - } | { - type: 'mutualFollow'; - } | { - type: 'followingOrFollower'; - } | { - type: 'list'; - userListId: MiUserList['id']; - }; - }; + public mutingNotificationTypes: typeof notificationTypes[number][]; @Column('varchar', { length: 32, array: true, default: '{}', @@ -274,7 +226,7 @@ export class MiUserProfile { default: [], }) public achievements: { - name: typeof ACHIEVEMENT_TYPES[number]; + name: string; unlockedAt: number; }[]; @@ -287,7 +239,7 @@ export class MiUserProfile { public userHost: string | null; //#endregion - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { @@ -295,84 +247,3 @@ export class MiUserProfile { } } } - -export const ACHIEVEMENT_TYPES = [ - 'notes1', - 'notes10', - 'notes100', - 'notes500', - 'notes1000', - 'notes5000', - 'notes10000', - 'notes20000', - 'notes30000', - 'notes40000', - 'notes50000', - 'notes60000', - 'notes70000', - 'notes80000', - 'notes90000', - 'notes100000', - 'login3', - 'login7', - 'login15', - 'login30', - 'login60', - 'login100', - 'login200', - 'login300', - 'login400', - 'login500', - 'login600', - 'login700', - 'login800', - 'login900', - 'login1000', - 'passedSinceAccountCreated1', - 'passedSinceAccountCreated2', - 'passedSinceAccountCreated3', - 'loggedInOnBirthday', - 'loggedInOnNewYearsDay', - 'noteClipped1', - 'noteFavorited1', - 'myNoteFavorited1', - 'profileFilled', - 'markedAsCat', - 'following1', - 'following10', - 'following50', - 'following100', - 'following300', - 'followers1', - 'followers10', - 'followers50', - 'followers100', - 'followers300', - 'followers500', - 'followers1000', - 'collectAchievements30', - 'viewAchievements3min', - 'iLoveMisskey', - 'foundTreasure', - 'client30min', - 'client60min', - 'noteDeletedWithin1min', - 'postedAtLateNight', - 'postedAt0min0sec', - 'selfQuote', - 'htl20npm', - 'viewInstanceChart', - 'outputHelloWorldOnScratchpad', - 'open3windows', - 'driveFolderCircularReference', - 'reactWithoutRead', - 'clickedClickHere', - 'justPlainLucky', - 'setNameToSyuilo', - 'cookieClicked', - 'brainDiver', - 'smashTestNotificationButton', - 'tutorialCompleted', - 'bubbleGameExplodingHead', - 'bubbleGameDoubleExplodingHead', -] as const; diff --git a/packages/backend/src/models/UserPublickey.ts b/packages/backend/src/models/entities/UserPublickey.ts similarity index 53% rename from packages/backend/src/models/UserPublickey.ts rename to packages/backend/src/models/entities/UserPublickey.ts index 6bcd785304..7b505e5b4c 100644 --- a/packages/backend/src/models/UserPublickey.ts +++ b/packages/backend/src/models/entities/UserPublickey.ts @@ -1,22 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; -@Entity('user_publickey') -export class MiUserPublickey { +@Entity() +export class UserPublickey { @PrimaryColumn(id()) - public userId: MiUser['id']; + public userId: User['id']; - @OneToOne(type => MiUser, { + @OneToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Index({ unique: true }) @Column('varchar', { @@ -29,7 +24,7 @@ export class MiUserPublickey { }) public keyPem: string; - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/entities/UserSecurityKey.ts b/packages/backend/src/models/entities/UserSecurityKey.ts new file mode 100644 index 0000000000..947692a32b --- /dev/null +++ b/packages/backend/src/models/entities/UserSecurityKey.ts @@ -0,0 +1,48 @@ +import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +export class UserSecurityKey { + @PrimaryColumn('varchar', { + comment: 'Variable-length id given to navigator.credentials.get()', + }) + public id: string; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column('varchar', { + comment: + 'Variable-length public key used to verify attestations (hex-encoded).', + }) + public publicKey: string; + + @Column('timestamp with time zone', { + comment: + 'The date of the last time the UserSecurityKey was successfully validated.', + }) + public lastUsed: Date; + + @Column('varchar', { + comment: 'User-defined name for this key', + length: 30, + }) + public name: string; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/Webhook.ts b/packages/backend/src/models/entities/Webhook.ts similarity index 75% rename from packages/backend/src/models/Webhook.ts rename to packages/backend/src/models/entities/Webhook.ts index b4cab4edc8..eabb604de9 100644 --- a/packages/backend/src/models/Webhook.ts +++ b/packages/backend/src/models/entities/Webhook.ts @@ -1,32 +1,31 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; +import { id } from '../id.js'; +import { User } from './User.js'; export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const; -export type WebhookEventTypes = typeof webhookEventTypes[number]; -@Entity('webhook') -export class MiWebhook { +@Entity() +export class Webhook { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone', { + comment: 'The created date of the Antenna.', + }) + public createdAt: Date; + @Index() @Column({ ...id(), comment: 'The owner ID.', }) - public userId: MiUser['id']; + public userId: User['id']; - @ManyToOne(type => MiUser, { + @ManyToOne(type => User, { onDelete: 'CASCADE', }) @JoinColumn() - public user: MiUser | null; + public user: User | null; @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/id.ts b/packages/backend/src/models/id.ts new file mode 100644 index 0000000000..d614fc5048 --- /dev/null +++ b/packages/backend/src/models/id.ts @@ -0,0 +1,4 @@ +export const id = () => ({ + type: 'varchar' as const, + length: 32, +}); diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts new file mode 100644 index 0000000000..4b230ab742 --- /dev/null +++ b/packages/backend/src/models/index.ts @@ -0,0 +1,203 @@ +import { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; +import { AccessToken } from '@/models/entities/AccessToken.js'; +import { Ad } from '@/models/entities/Ad.js'; +import { Announcement } from '@/models/entities/Announcement.js'; +import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js'; +import { Antenna } from '@/models/entities/Antenna.js'; +import { App } from '@/models/entities/App.js'; +import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js'; +import { AuthSession } from '@/models/entities/AuthSession.js'; +import { Blocking } from '@/models/entities/Blocking.js'; +import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js'; +import { ChannelFavorite } from '@/models/entities/ChannelFavorite.js'; +import { Clip } from '@/models/entities/Clip.js'; +import { ClipNote } from '@/models/entities/ClipNote.js'; +import { ClipFavorite } from '@/models/entities/ClipFavorite.js'; +import { DriveFile } from '@/models/entities/DriveFile.js'; +import { DriveFolder } from '@/models/entities/DriveFolder.js'; +import { Emoji } from '@/models/entities/Emoji.js'; +import { Following } from '@/models/entities/Following.js'; +import { FollowRequest } from '@/models/entities/FollowRequest.js'; +import { GalleryLike } from '@/models/entities/GalleryLike.js'; +import { GalleryPost } from '@/models/entities/GalleryPost.js'; +import { Hashtag } from '@/models/entities/Hashtag.js'; +import { Instance } from '@/models/entities/Instance.js'; +import { Meta } from '@/models/entities/Meta.js'; +import { ModerationLog } from '@/models/entities/ModerationLog.js'; +import { MutedNote } from '@/models/entities/MutedNote.js'; +import { Muting } from '@/models/entities/Muting.js'; +import { RenoteMuting } from '@/models/entities/RenoteMuting.js'; +import { Note } from '@/models/entities/Note.js'; +import { NoteFavorite } from '@/models/entities/NoteFavorite.js'; +import { NoteReaction } from '@/models/entities/NoteReaction.js'; +import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js'; +import { NoteUnread } from '@/models/entities/NoteUnread.js'; +import { Page } from '@/models/entities/Page.js'; +import { PageLike } from '@/models/entities/PageLike.js'; +import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js'; +import { Poll } from '@/models/entities/Poll.js'; +import { PollVote } from '@/models/entities/PollVote.js'; +import { PromoNote } from '@/models/entities/PromoNote.js'; +import { PromoRead } from '@/models/entities/PromoRead.js'; +import { RegistrationTicket } from '@/models/entities/RegistrationTicket.js'; +import { RegistryItem } from '@/models/entities/RegistryItem.js'; +import { Relay } from '@/models/entities/Relay.js'; +import { Signin } from '@/models/entities/Signin.js'; +import { SwSubscription } from '@/models/entities/SwSubscription.js'; +import { UsedUsername } from '@/models/entities/UsedUsername.js'; +import { User } from '@/models/entities/User.js'; +import { UserIp } from '@/models/entities/UserIp.js'; +import { UserKeypair } from '@/models/entities/UserKeypair.js'; +import { UserList } from '@/models/entities/UserList.js'; +import { UserListFavorite } from './entities/UserListFavorite.js'; +import { UserListJoining } from '@/models/entities/UserListJoining.js'; +import { UserNotePining } from '@/models/entities/UserNotePining.js'; +import { UserPending } from '@/models/entities/UserPending.js'; +import { UserProfile } from '@/models/entities/UserProfile.js'; +import { UserPublickey } from '@/models/entities/UserPublickey.js'; +import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js'; +import { UserMemo } from '@/models/entities/UserMemo.js'; +import { Webhook } from '@/models/entities/Webhook.js'; +import { Channel } from '@/models/entities/Channel.js'; +import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js'; +import { Role } from '@/models/entities/Role.js'; +import { RoleAssignment } from '@/models/entities/RoleAssignment.js'; +import { Flash } from '@/models/entities/Flash.js'; +import { FlashLike } from '@/models/entities/FlashLike.js'; +import type { Repository } from 'typeorm'; + +export { + AbuseUserReport, + AccessToken, + Ad, + Announcement, + AnnouncementRead, + Antenna, + App, + AttestationChallenge, + AuthSession, + Blocking, + ChannelFollowing, + ChannelFavorite, + Clip, + ClipNote, + ClipFavorite, + DriveFile, + DriveFolder, + Emoji, + Following, + FollowRequest, + GalleryLike, + GalleryPost, + Hashtag, + Instance, + Meta, + ModerationLog, + MutedNote, + Muting, + RenoteMuting, + Note, + NoteFavorite, + NoteReaction, + NoteThreadMuting, + NoteUnread, + Page, + PageLike, + PasswordResetRequest, + Poll, + PollVote, + PromoNote, + PromoRead, + RegistrationTicket, + RegistryItem, + Relay, + Signin, + SwSubscription, + UsedUsername, + User, + UserIp, + UserKeypair, + UserList, + UserListFavorite, + UserListJoining, + UserNotePining, + UserPending, + UserProfile, + UserPublickey, + UserSecurityKey, + Webhook, + Channel, + RetentionAggregation, + Role, + RoleAssignment, + Flash, + FlashLike, + UserMemo, +}; + +export type AbuseUserReportsRepository = Repository; +export type AccessTokensRepository = Repository; +export type AdsRepository = Repository; +export type AnnouncementsRepository = Repository; +export type AnnouncementReadsRepository = Repository; +export type AntennasRepository = Repository; +export type AppsRepository = Repository; +export type AttestationChallengesRepository = Repository; +export type AuthSessionsRepository = Repository; +export type BlockingsRepository = Repository; +export type ChannelFollowingsRepository = Repository; +export type ChannelFavoritesRepository = Repository; +export type ClipsRepository = Repository; +export type ClipNotesRepository = Repository; +export type ClipFavoritesRepository = Repository; +export type DriveFilesRepository = Repository; +export type DriveFoldersRepository = Repository; +export type EmojisRepository = Repository; +export type FollowingsRepository = Repository; +export type FollowRequestsRepository = Repository; +export type GalleryLikesRepository = Repository; +export type GalleryPostsRepository = Repository; +export type HashtagsRepository = Repository; +export type InstancesRepository = Repository; +export type MetasRepository = Repository; +export type ModerationLogsRepository = Repository; +export type MutedNotesRepository = Repository; +export type MutingsRepository = Repository; +export type RenoteMutingsRepository = Repository; +export type NotesRepository = Repository; +export type NoteFavoritesRepository = Repository; +export type NoteReactionsRepository = Repository; +export type NoteThreadMutingsRepository = Repository; +export type NoteUnreadsRepository = Repository; +export type PagesRepository = Repository; +export type PageLikesRepository = Repository; +export type PasswordResetRequestsRepository = Repository; +export type PollsRepository = Repository; +export type PollVotesRepository = Repository; +export type PromoNotesRepository = Repository; +export type PromoReadsRepository = Repository; +export type RegistrationTicketsRepository = Repository; +export type RegistryItemsRepository = Repository; +export type RelaysRepository = Repository; +export type SigninsRepository = Repository; +export type SwSubscriptionsRepository = Repository; +export type UsedUsernamesRepository = Repository; +export type UsersRepository = Repository; +export type UserIpsRepository = Repository; +export type UserKeypairsRepository = Repository; +export type UserListsRepository = Repository; +export type UserListFavoritesRepository = Repository; +export type UserListJoiningsRepository = Repository; +export type UserNotePiningsRepository = Repository; +export type UserPendingsRepository = Repository; +export type UserProfilesRepository = Repository; +export type UserPublickeysRepository = Repository; +export type UserSecurityKeysRepository = Repository; +export type WebhooksRepository = Repository; +export type ChannelsRepository = Repository; +export type RetentionAggregationsRepository = Repository; +export type RolesRepository = Repository; +export type RoleAssignmentsRepository = Repository; +export type FlashsRepository = Repository; +export type FlashLikesRepository = Repository; +export type UserMemoRepository = Repository; diff --git a/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts b/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts deleted file mode 100644 index 6215f0f5a2..0000000000 --- a/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedAbuseReportNotificationRecipientSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - isActive: { - type: 'boolean', - optional: false, nullable: false, - }, - updatedAt: { - type: 'string', - format: 'date-time', - optional: false, nullable: false, - }, - name: { - type: 'string', - optional: false, nullable: false, - }, - method: { - type: 'string', - optional: false, nullable: false, - enum: ['email', 'webhook'], - }, - userId: { - type: 'string', - optional: true, nullable: false, - }, - user: { - type: 'object', - optional: true, nullable: false, - ref: 'UserLite', - }, - systemWebhookId: { - type: 'string', - optional: true, nullable: false, - }, - systemWebhook: { - type: 'object', - optional: true, nullable: false, - ref: 'SystemWebhook', - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/achievement.ts b/packages/backend/src/models/json-schema/achievement.ts deleted file mode 100644 index 39a621a570..0000000000 --- a/packages/backend/src/models/json-schema/achievement.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ACHIEVEMENT_TYPES } from '@/models/UserProfile.js'; - -export const packedAchievementNameSchema = { - type: 'string', - enum: ACHIEVEMENT_TYPES, - optional: false, -} as const; - -export const packedAchievementSchema = { - type: 'object', - properties: { - name: { - ref: 'AchievementName', - }, - unlockedAt: { - type: 'number', - optional: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/ad.ts b/packages/backend/src/models/json-schema/ad.ts deleted file mode 100644 index b01b39a38b..0000000000 --- a/packages/backend/src/models/json-schema/ad.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedAdSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, - nullable: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - expiresAt: { - type: 'string', - optional: false, - nullable: false, - format: 'date-time', - }, - startsAt: { - type: 'string', - optional: false, - nullable: false, - format: 'date-time', - }, - place: { - type: 'string', - optional: false, - nullable: false, - }, - priority: { - type: 'string', - optional: false, - nullable: false, - }, - ratio: { - type: 'number', - optional: false, - nullable: false, - }, - url: { - type: 'string', - optional: false, - nullable: false, - }, - imageUrl: { - type: 'string', - optional: false, - nullable: false, - }, - memo: { - type: 'string', - optional: false, - nullable: false, - }, - dayOfWeek: { - type: 'integer', - optional: false, - nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/announcement.ts b/packages/backend/src/models/json-schema/announcement.ts deleted file mode 100644 index b9352bd31e..0000000000 --- a/packages/backend/src/models/json-schema/announcement.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedAnnouncementSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - createdAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - updatedAt: { - type: 'string', - optional: false, nullable: true, - format: 'date-time', - }, - text: { - type: 'string', - optional: false, nullable: false, - }, - title: { - type: 'string', - optional: false, nullable: false, - }, - imageUrl: { - type: 'string', - optional: false, nullable: true, - }, - icon: { - type: 'string', - optional: false, nullable: false, - enum: ['info', 'warning', 'error', 'success'], - }, - display: { - type: 'string', - optional: false, nullable: false, - enum: ['dialog', 'normal', 'banner'], - }, - needConfirmationToRead: { - type: 'boolean', - optional: false, nullable: false, - }, - silence: { - type: 'boolean', - optional: false, nullable: false, - }, - forYou: { - type: 'boolean', - optional: false, nullable: false, - }, - isRead: { - type: 'boolean', - optional: true, nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/antenna.ts b/packages/backend/src/models/json-schema/antenna.ts index eca7563066..4483510610 100644 --- a/packages/backend/src/models/json-schema/antenna.ts +++ b/packages/backend/src/models/json-schema/antenna.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedAntennaSchema = { type: 'object', properties: { @@ -47,7 +42,7 @@ export const packedAntennaSchema = { src: { type: 'string', optional: false, nullable: false, - enum: ['home', 'all', 'users', 'list', 'users_blacklist'], + enum: ['home', 'all', 'users', 'list'], }, userListId: { type: 'string', @@ -67,15 +62,9 @@ export const packedAntennaSchema = { optional: false, nullable: false, default: false, }, - localOnly: { + notify: { type: 'boolean', optional: false, nullable: false, - default: false, - }, - excludeBots: { - type: 'boolean', - optional: false, nullable: false, - default: false, }, withReplies: { type: 'boolean', @@ -95,15 +84,5 @@ export const packedAntennaSchema = { optional: false, nullable: false, default: false, }, - notify: { - type: 'boolean', - optional: false, nullable: false, - default: false, - }, - excludeNotesInSensitiveChannel: { - type: 'boolean', - optional: false, nullable: false, - default: false, - }, }, } as const; diff --git a/packages/backend/src/models/json-schema/app.ts b/packages/backend/src/models/json-schema/app.ts index 6148232224..c80dc81c33 100644 --- a/packages/backend/src/models/json-schema/app.ts +++ b/packages/backend/src/models/json-schema/app.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedAppSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/blocking.ts b/packages/backend/src/models/json-schema/blocking.ts index 2d02ba6a70..5532322420 100644 --- a/packages/backend/src/models/json-schema/blocking.ts +++ b/packages/backend/src/models/json-schema/blocking.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedBlockingSchema = { type: 'object', properties: { @@ -25,7 +20,7 @@ export const packedBlockingSchema = { blockee: { type: 'object', optional: false, nullable: false, - ref: 'UserDetailedNotMe', + ref: 'UserDetailed', }, }, } as const; diff --git a/packages/backend/src/models/json-schema/channel.ts b/packages/backend/src/models/json-schema/channel.ts index d233f7858d..fd61a70c0e 100644 --- a/packages/backend/src/models/json-schema/channel.ts +++ b/packages/backend/src/models/json-schema/channel.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedChannelSchema = { type: 'object', properties: { @@ -19,7 +14,7 @@ export const packedChannelSchema = { }, lastNotedAt: { type: 'string', - nullable: true, optional: false, + optional: false, nullable: true, format: 'date-time', }, name: { @@ -27,19 +22,39 @@ export const packedChannelSchema = { optional: false, nullable: false, }, description: { - type: 'string', - optional: false, nullable: true, - }, - userId: { type: 'string', nullable: true, optional: false, - format: 'id', }, bannerUrl: { type: 'string', format: 'url', nullable: true, optional: false, }, + isArchived: { + type: 'boolean', + optional: false, nullable: false, + }, + notesCount: { + type: 'number', + nullable: false, optional: false, + }, + usersCount: { + type: 'number', + nullable: false, optional: false, + }, + isFollowing: { + type: 'boolean', + optional: true, nullable: false, + }, + isFavorited: { + type: 'boolean', + optional: true, nullable: false, + }, + userId: { + type: 'string', + nullable: true, optional: false, + format: 'id', + }, pinnedNoteIds: { type: 'array', nullable: false, optional: false, @@ -52,42 +67,5 @@ export const packedChannelSchema = { type: 'string', optional: false, nullable: false, }, - isArchived: { - type: 'boolean', - optional: false, nullable: false, - }, - usersCount: { - type: 'number', - nullable: false, optional: false, - }, - notesCount: { - type: 'number', - nullable: false, optional: false, - }, - isSensitive: { - type: 'boolean', - optional: false, nullable: false, - }, - allowRenoteToExternal: { - type: 'boolean', - optional: false, nullable: false, - }, - isFollowing: { - type: 'boolean', - optional: true, nullable: false, - }, - isFavorited: { - type: 'boolean', - optional: true, nullable: false, - }, - pinnedNotes: { - type: 'array', - optional: true, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - }, }, } as const; diff --git a/packages/backend/src/models/json-schema/chat-message.ts b/packages/backend/src/models/json-schema/chat-message.ts deleted file mode 100644 index 3b5e85ab69..0000000000 --- a/packages/backend/src/models/json-schema/chat-message.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedChatMessageSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - createdAt: { - type: 'string', - format: 'date-time', - optional: false, nullable: false, - }, - fromUserId: { - type: 'string', - optional: false, nullable: false, - }, - fromUser: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - toUserId: { - type: 'string', - optional: true, nullable: true, - }, - toUser: { - type: 'object', - optional: true, nullable: true, - ref: 'UserLite', - }, - toRoomId: { - type: 'string', - optional: true, nullable: true, - }, - toRoom: { - type: 'object', - optional: true, nullable: true, - ref: 'ChatRoom', - }, - text: { - type: 'string', - optional: true, nullable: true, - }, - fileId: { - type: 'string', - optional: true, nullable: true, - }, - file: { - type: 'object', - optional: true, nullable: true, - ref: 'DriveFile', - }, - isRead: { - type: 'boolean', - optional: true, nullable: false, - }, - reactions: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - reaction: { - type: 'string', - optional: false, nullable: false, - }, - user: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - }, - }, - }, - }, -} as const; - -export const packedChatMessageLiteSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - createdAt: { - type: 'string', - format: 'date-time', - optional: false, nullable: false, - }, - fromUserId: { - type: 'string', - optional: false, nullable: false, - }, - fromUser: { - type: 'object', - optional: true, nullable: false, - ref: 'UserLite', - }, - toUserId: { - type: 'string', - optional: true, nullable: true, - }, - toRoomId: { - type: 'string', - optional: true, nullable: true, - }, - text: { - type: 'string', - optional: true, nullable: true, - }, - fileId: { - type: 'string', - optional: true, nullable: true, - }, - file: { - type: 'object', - optional: true, nullable: true, - ref: 'DriveFile', - }, - reactions: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - reaction: { - type: 'string', - optional: false, nullable: false, - }, - user: { - type: 'object', - optional: true, nullable: true, - ref: 'UserLite', - }, - }, - }, - }, - }, -} as const; - -export const packedChatMessageLiteFor1on1Schema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - createdAt: { - type: 'string', - format: 'date-time', - optional: false, nullable: false, - }, - fromUserId: { - type: 'string', - optional: false, nullable: false, - }, - toUserId: { - type: 'string', - optional: false, nullable: false, - }, - text: { - type: 'string', - optional: true, nullable: true, - }, - fileId: { - type: 'string', - optional: true, nullable: true, - }, - file: { - type: 'object', - optional: true, nullable: true, - ref: 'DriveFile', - }, - reactions: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - reaction: { - type: 'string', - optional: false, nullable: false, - }, - }, - }, - }, - }, -} as const; - -export const packedChatMessageLiteForRoomSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - createdAt: { - type: 'string', - format: 'date-time', - optional: false, nullable: false, - }, - fromUserId: { - type: 'string', - optional: false, nullable: false, - }, - fromUser: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - toRoomId: { - type: 'string', - optional: false, nullable: false, - }, - text: { - type: 'string', - optional: true, nullable: true, - }, - fileId: { - type: 'string', - optional: true, nullable: true, - }, - file: { - type: 'object', - optional: true, nullable: true, - ref: 'DriveFile', - }, - reactions: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - reaction: { - type: 'string', - optional: false, nullable: false, - }, - user: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - }, - }, - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/chat-room-invitation.ts b/packages/backend/src/models/json-schema/chat-room-invitation.ts deleted file mode 100644 index 204c959b2c..0000000000 --- a/packages/backend/src/models/json-schema/chat-room-invitation.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedChatRoomInvitationSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - createdAt: { - type: 'string', - format: 'date-time', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - }, - user: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - roomId: { - type: 'string', - optional: false, nullable: false, - }, - room: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatRoom', - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/chat-room-membership.ts b/packages/backend/src/models/json-schema/chat-room-membership.ts deleted file mode 100644 index adb73f9dde..0000000000 --- a/packages/backend/src/models/json-schema/chat-room-membership.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedChatRoomMembershipSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - createdAt: { - type: 'string', - format: 'date-time', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - }, - user: { - type: 'object', - optional: true, nullable: false, - ref: 'UserLite', - }, - roomId: { - type: 'string', - optional: false, nullable: false, - }, - room: { - type: 'object', - optional: true, nullable: false, - ref: 'ChatRoom', - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/chat-room.ts b/packages/backend/src/models/json-schema/chat-room.ts deleted file mode 100644 index e628a9baa3..0000000000 --- a/packages/backend/src/models/json-schema/chat-room.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedChatRoomSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - createdAt: { - type: 'string', - format: 'date-time', - optional: false, nullable: false, - }, - ownerId: { - type: 'string', - optional: false, nullable: false, - }, - owner: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - name: { - type: 'string', - optional: false, nullable: false, - }, - description: { - type: 'string', - optional: false, nullable: false, - }, - isMuted: { - type: 'boolean', - optional: true, nullable: false, - }, - invitationExists: { - type: 'boolean', - optional: true, nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/clip.ts b/packages/backend/src/models/json-schema/clip.ts index c4e7055cd8..7310e59013 100644 --- a/packages/backend/src/models/json-schema/clip.ts +++ b/packages/backend/src/models/json-schema/clip.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedClipSchema = { type: 'object', properties: { @@ -44,17 +39,13 @@ export const packedClipSchema = { type: 'boolean', optional: false, nullable: false, }, - favoritedCount: { - type: 'number', - optional: false, nullable: false, - }, isFavorited: { type: 'boolean', optional: true, nullable: false, }, - notesCount: { - type: 'integer', - optional: true, nullable: false, + favoritedCount: { + type: 'number', + optional: false, nullable: false, }, }, } as const; diff --git a/packages/backend/src/models/json-schema/drive-file.ts b/packages/backend/src/models/json-schema/drive-file.ts index 5ee1561c50..4359076612 100644 --- a/packages/backend/src/models/json-schema/drive-file.ts +++ b/packages/backend/src/models/json-schema/drive-file.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedDriveFileSchema = { type: 'object', properties: { @@ -20,7 +15,7 @@ export const packedDriveFileSchema = { name: { type: 'string', optional: false, nullable: false, - example: '192.jpg', + example: 'lenna.jpg', }, type: { type: 'string', @@ -74,7 +69,7 @@ export const packedDriveFileSchema = { }, url: { type: 'string', - optional: false, nullable: false, + optional: false, nullable: true, format: 'url', }, thumbnailUrl: { diff --git a/packages/backend/src/models/json-schema/drive-folder.ts b/packages/backend/src/models/json-schema/drive-folder.ts index 12012a7e12..88cb8ab4a2 100644 --- a/packages/backend/src/models/json-schema/drive-folder.ts +++ b/packages/backend/src/models/json-schema/drive-folder.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedDriveFolderSchema = { type: 'object', properties: { @@ -21,12 +16,6 @@ export const packedDriveFolderSchema = { type: 'string', optional: false, nullable: false, }, - parentId: { - type: 'string', - optional: false, nullable: true, - format: 'id', - example: 'xxxxxxxxxx', - }, foldersCount: { type: 'number', optional: true, nullable: false, @@ -35,6 +24,12 @@ export const packedDriveFolderSchema = { type: 'number', optional: true, nullable: false, }, + parentId: { + type: 'string', + optional: false, nullable: true, + format: 'id', + example: 'xxxxxxxxxx', + }, parent: { type: 'object', optional: true, nullable: true, diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index 3cd263fa37..63f56e77cb 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedEmojiSimpleSchema = { type: 'object', properties: { @@ -27,10 +22,6 @@ export const packedEmojiSimpleSchema = { type: 'string', optional: false, nullable: false, }, - localOnly: { - type: 'boolean', - optional: true, nullable: false, - }, isSensitive: { type: 'boolean', optional: true, nullable: false, @@ -104,86 +95,3 @@ export const packedEmojiDetailedSchema = { }, }, } as const; - -export const packedEmojiDetailedAdminSchema = { - type: 'object', - properties: { - id: { - type: 'string', - format: 'id', - optional: false, nullable: false, - }, - updatedAt: { - type: 'string', - format: 'date-time', - optional: false, nullable: true, - }, - name: { - type: 'string', - optional: false, nullable: false, - }, - host: { - type: 'string', - optional: false, nullable: true, - description: 'The local host is represented with `null`.', - }, - publicUrl: { - type: 'string', - optional: false, nullable: false, - }, - originalUrl: { - type: 'string', - optional: false, nullable: false, - }, - uri: { - type: 'string', - optional: false, nullable: true, - }, - type: { - type: 'string', - optional: false, nullable: true, - }, - aliases: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - format: 'id', - optional: false, nullable: false, - }, - }, - category: { - type: 'string', - optional: false, nullable: true, - }, - license: { - type: 'string', - optional: false, nullable: true, - }, - localOnly: { - type: 'boolean', - optional: false, nullable: false, - }, - isSensitive: { - type: 'boolean', - optional: false, nullable: false, - }, - roleIdsThatCanBeUsedThisEmojiAsReaction: { - type: 'array', - items: { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - optional: false, nullable: false, - }, - name: { - type: 'string', - optional: false, nullable: false, - }, - }, - }, - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 85f84952f1..42d93dfac9 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedFederationInstanceSchema = { type: 'object', properties: { @@ -45,11 +40,6 @@ export const packedFederationInstanceSchema = { type: 'boolean', optional: false, nullable: false, }, - suspensionState: { - type: 'string', - nullable: false, optional: false, - enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding', 'softwareSuspended'], - }, isBlocked: { type: 'boolean', optional: false, nullable: false, @@ -84,14 +74,6 @@ export const packedFederationInstanceSchema = { type: 'string', optional: false, nullable: true, }, - isSilenced: { - type: 'boolean', - optional: false, nullable: false, - }, - isMediaSilenced: { - type: 'boolean', - optional: false, nullable: false, - }, iconUrl: { type: 'string', optional: false, nullable: true, @@ -111,14 +93,5 @@ export const packedFederationInstanceSchema = { optional: false, nullable: true, format: 'date-time', }, - latestRequestReceivedAt: { - type: 'string', - optional: false, nullable: true, - format: 'date-time', - }, - moderationNote: { - type: 'string', - optional: true, nullable: true, - }, }, } as const; diff --git a/packages/backend/src/models/json-schema/flash.ts b/packages/backend/src/models/json-schema/flash.ts index 42b2172409..8471a138ec 100644 --- a/packages/backend/src/models/json-schema/flash.ts +++ b/packages/backend/src/models/json-schema/flash.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedFlashSchema = { type: 'object', properties: { @@ -22,16 +17,6 @@ export const packedFlashSchema = { optional: false, nullable: false, format: 'date-time', }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, title: { type: 'string', optional: false, nullable: false, @@ -44,10 +29,15 @@ export const packedFlashSchema = { type: 'string', optional: false, nullable: false, }, - visibility: { + userId: { type: 'string', optional: false, nullable: false, - enum: ['private', 'public'], + format: 'id', + }, + user: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, }, likedCount: { type: 'number', diff --git a/packages/backend/src/models/json-schema/following.ts b/packages/backend/src/models/json-schema/following.ts index c5295a5128..2bcffbfc4d 100644 --- a/packages/backend/src/models/json-schema/following.ts +++ b/packages/backend/src/models/json-schema/following.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedFollowingSchema = { type: 'object', properties: { @@ -22,20 +17,20 @@ export const packedFollowingSchema = { optional: false, nullable: false, format: 'id', }, + followee: { + type: 'object', + optional: true, nullable: false, + ref: 'UserDetailed', + }, followerId: { type: 'string', optional: false, nullable: false, format: 'id', }, - followee: { - type: 'object', - optional: true, nullable: false, - ref: 'UserDetailedNotMe', - }, follower: { type: 'object', optional: true, nullable: false, - ref: 'UserDetailedNotMe', + ref: 'UserDetailed', }, }, } as const; diff --git a/packages/backend/src/models/json-schema/gallery-post.ts b/packages/backend/src/models/json-schema/gallery-post.ts index a46d5115c2..fc503d4a64 100644 --- a/packages/backend/src/models/json-schema/gallery-post.ts +++ b/packages/backend/src/models/json-schema/gallery-post.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedGalleryPostSchema = { type: 'object', properties: { @@ -22,6 +17,14 @@ export const packedGalleryPostSchema = { optional: false, nullable: false, format: 'date-time', }, + title: { + type: 'string', + optional: false, nullable: false, + }, + description: { + type: 'string', + optional: false, nullable: true, + }, userId: { type: 'string', optional: false, nullable: false, @@ -32,14 +35,6 @@ export const packedGalleryPostSchema = { ref: 'UserLite', optional: false, nullable: false, }, - title: { - type: 'string', - optional: false, nullable: false, - }, - description: { - type: 'string', - optional: false, nullable: true, - }, fileIds: { type: 'array', optional: true, nullable: false, @@ -70,13 +65,5 @@ export const packedGalleryPostSchema = { type: 'boolean', optional: false, nullable: false, }, - likedCount: { - type: 'number', - optional: false, nullable: false, - }, - isLiked: { - type: 'boolean', - optional: true, nullable: false, - }, }, } as const; diff --git a/packages/backend/src/models/json-schema/hashtag.ts b/packages/backend/src/models/json-schema/hashtag.ts index 4fd136afed..98f8827640 100644 --- a/packages/backend/src/models/json-schema/hashtag.ts +++ b/packages/backend/src/models/json-schema/hashtag.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedHashtagSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/invite-code.ts b/packages/backend/src/models/json-schema/invite-code.ts deleted file mode 100644 index 08d1b8fd0c..0000000000 --- a/packages/backend/src/models/json-schema/invite-code.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedInviteCodeSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - code: { - type: 'string', - optional: false, nullable: false, - example: 'GR6S02ERUA5VR', - }, - expiresAt: { - type: 'string', - optional: false, nullable: true, - format: 'date-time', - }, - createdAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - createdBy: { - type: 'object', - optional: false, nullable: true, - ref: 'UserLite', - }, - usedBy: { - type: 'object', - optional: false, nullable: true, - ref: 'UserLite', - }, - usedAt: { - type: 'string', - optional: false, nullable: true, - format: 'date-time', - }, - used: { - type: 'boolean', - optional: false, nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts deleted file mode 100644 index 2cd7620af0..0000000000 --- a/packages/backend/src/models/json-schema/meta.ts +++ /dev/null @@ -1,391 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedMetaLiteSchema = { - type: 'object', - optional: false, nullable: false, - properties: { - maintainerName: { - type: 'string', - optional: false, nullable: true, - }, - maintainerEmail: { - type: 'string', - optional: false, nullable: true, - }, - version: { - type: 'string', - optional: false, nullable: false, - }, - providesTarball: { - type: 'boolean', - optional: false, nullable: false, - }, - name: { - type: 'string', - optional: false, nullable: true, - }, - shortName: { - type: 'string', - optional: false, nullable: true, - }, - uri: { - type: 'string', - optional: false, nullable: false, - format: 'url', - example: 'https://misskey.example.com', - }, - description: { - type: 'string', - optional: false, nullable: true, - }, - langs: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - }, - }, - tosUrl: { - type: 'string', - optional: false, nullable: true, - }, - repositoryUrl: { - type: 'string', - optional: false, nullable: true, - default: 'https://github.com/misskey-dev/misskey', - }, - feedbackUrl: { - type: 'string', - optional: false, nullable: true, - default: 'https://github.com/misskey-dev/misskey/issues/new', - }, - defaultDarkTheme: { - type: 'string', - optional: false, nullable: true, - }, - defaultLightTheme: { - type: 'string', - optional: false, nullable: true, - }, - disableRegistration: { - type: 'boolean', - optional: false, nullable: false, - }, - emailRequiredForSignup: { - type: 'boolean', - optional: false, nullable: false, - }, - enableHcaptcha: { - type: 'boolean', - optional: false, nullable: false, - }, - hcaptchaSiteKey: { - type: 'string', - optional: false, nullable: true, - }, - enableMcaptcha: { - type: 'boolean', - optional: false, nullable: false, - }, - mcaptchaSiteKey: { - type: 'string', - optional: false, nullable: true, - }, - mcaptchaInstanceUrl: { - type: 'string', - optional: false, nullable: true, - }, - enableRecaptcha: { - type: 'boolean', - optional: false, nullable: false, - }, - recaptchaSiteKey: { - type: 'string', - optional: false, nullable: true, - }, - enableTurnstile: { - type: 'boolean', - optional: false, nullable: false, - }, - turnstileSiteKey: { - type: 'string', - optional: false, nullable: true, - }, - enableTestcaptcha: { - type: 'boolean', - optional: false, nullable: false, - }, - googleAnalyticsMeasurementId: { - type: 'string', - optional: false, nullable: true, - }, - swPublickey: { - type: 'string', - optional: false, nullable: true, - }, - mascotImageUrl: { - type: 'string', - optional: false, nullable: false, - default: '/assets/ai.png', - }, - bannerUrl: { - type: 'string', - optional: false, nullable: true, - }, - serverErrorImageUrl: { - type: 'string', - optional: false, nullable: true, - }, - infoImageUrl: { - type: 'string', - optional: false, nullable: true, - }, - notFoundImageUrl: { - type: 'string', - optional: false, nullable: true, - }, - iconUrl: { - type: 'string', - optional: false, nullable: true, - }, - maxNoteTextLength: { - type: 'number', - optional: false, nullable: false, - }, - ads: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - url: { - type: 'string', - optional: false, nullable: false, - format: 'url', - }, - place: { - type: 'string', - optional: false, nullable: false, - }, - ratio: { - type: 'number', - optional: false, nullable: false, - }, - imageUrl: { - type: 'string', - optional: false, nullable: false, - format: 'url', - }, - dayOfWeek: { - type: 'integer', - optional: false, nullable: false, - }, - }, - }, - }, - notesPerOneAd: { - type: 'number', - optional: false, nullable: false, - default: 0, - }, - enableEmail: { - type: 'boolean', - optional: false, nullable: false, - }, - enableServiceWorker: { - type: 'boolean', - optional: false, nullable: false, - }, - translatorAvailable: { - type: 'boolean', - optional: false, nullable: false, - }, - sentryForFrontend: { - type: 'object', - optional: false, nullable: true, - properties: { - options: { - type: 'object', - optional: false, nullable: false, - properties: { - dsn: { - type: 'string', - optional: false, nullable: false, - }, - }, - additionalProperties: true, - }, - vueIntegration: { - type: 'object', - optional: true, nullable: true, - additionalProperties: true, - }, - browserTracingIntegration: { - type: 'object', - optional: true, nullable: true, - additionalProperties: true, - }, - replayIntegration: { - type: 'object', - optional: true, nullable: true, - additionalProperties: true, - }, - }, - }, - mediaProxy: { - type: 'string', - optional: false, nullable: false, - }, - enableUrlPreview: { - type: 'boolean', - optional: false, nullable: false, - }, - backgroundImageUrl: { - type: 'string', - optional: false, nullable: true, - }, - impressumUrl: { - type: 'string', - optional: false, nullable: true, - }, - logoImageUrl: { - type: 'string', - optional: false, nullable: true, - }, - privacyPolicyUrl: { - type: 'string', - optional: false, nullable: true, - }, - inquiryUrl: { - type: 'string', - optional: false, nullable: true, - }, - serverRules: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - }, - }, - themeColor: { - type: 'string', - optional: false, nullable: true, - }, - policies: { - type: 'object', - optional: false, nullable: false, - ref: 'RolePolicies', - }, - noteSearchableScope: { - type: 'string', - enum: ['local', 'global'], - optional: false, nullable: false, - default: 'local', - }, - maxFileSize: { - type: 'number', - optional: false, nullable: false, - }, - federation: { - type: 'string', - enum: ['all', 'specified', 'none'], - optional: false, nullable: false, - }, - }, -} as const; - -export const packedMetaDetailedOnlySchema = { - type: 'object', - optional: false, nullable: false, - properties: { - features: { - type: 'object', - optional: true, nullable: false, - properties: { - registration: { - type: 'boolean', - optional: false, nullable: false, - }, - emailRequiredForSignup: { - type: 'boolean', - optional: false, nullable: false, - }, - localTimeline: { - type: 'boolean', - optional: false, nullable: false, - }, - globalTimeline: { - type: 'boolean', - optional: false, nullable: false, - }, - hcaptcha: { - type: 'boolean', - optional: false, nullable: false, - }, - turnstile: { - type: 'boolean', - optional: false, nullable: false, - }, - recaptcha: { - type: 'boolean', - optional: false, nullable: false, - }, - objectStorage: { - type: 'boolean', - optional: false, nullable: false, - }, - serviceWorker: { - type: 'boolean', - optional: false, nullable: false, - }, - miauth: { - type: 'boolean', - optional: true, nullable: false, - default: true, - }, - }, - }, - proxyAccountName: { - type: 'string', - optional: false, nullable: true, - }, - requireSetup: { - type: 'boolean', - optional: false, nullable: false, - example: false, - }, - cacheRemoteFiles: { - type: 'boolean', - optional: false, nullable: false, - }, - cacheRemoteSensitiveFiles: { - type: 'boolean', - optional: false, nullable: false, - }, - }, -} as const; - -export const packedMetaDetailedSchema = { - type: 'object', - allOf: [ - { - type: 'object', - ref: 'MetaLite', - }, - { - type: 'object', - ref: 'MetaDetailedOnly', - }, - ], -} as const; diff --git a/packages/backend/src/models/json-schema/muting.ts b/packages/backend/src/models/json-schema/muting.ts index b5fab013ef..3ab99e17e7 100644 --- a/packages/backend/src/models/json-schema/muting.ts +++ b/packages/backend/src/models/json-schema/muting.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedMutingSchema = { type: 'object', properties: { @@ -30,7 +25,7 @@ export const packedMutingSchema = { mutee: { type: 'object', optional: false, nullable: false, - ref: 'UserDetailedNotMe', + ref: 'UserDetailed', }, }, } as const; diff --git a/packages/backend/src/models/json-schema/note-favorite.ts b/packages/backend/src/models/json-schema/note-favorite.ts index d2a3745f4b..d133f7367d 100644 --- a/packages/backend/src/models/json-schema/note-favorite.ts +++ b/packages/backend/src/models/json-schema/note-favorite.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedNoteFavoriteSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/note-reaction.ts b/packages/backend/src/models/json-schema/note-reaction.ts index 95658ace1f..0d8fc5449b 100644 --- a/packages/backend/src/models/json-schema/note-reaction.ts +++ b/packages/backend/src/models/json-schema/note-reaction.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedNoteReactionSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index f3901691a4..58ef425dcd 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedNoteSchema = { type: 'object', properties: { @@ -69,7 +64,6 @@ export const packedNoteSchema = { visibility: { type: 'string', optional: false, nullable: false, - enum: ['public', 'home', 'followers', 'specified'], }, mentions: { type: 'array', @@ -118,48 +112,6 @@ export const packedNoteSchema = { poll: { type: 'object', optional: true, nullable: true, - properties: { - expiresAt: { - type: 'string', - optional: true, nullable: true, - format: 'date-time', - }, - multiple: { - type: 'boolean', - optional: false, nullable: false, - }, - choices: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - isVoted: { - type: 'boolean', - optional: false, nullable: false, - }, - text: { - type: 'string', - optional: false, nullable: false, - }, - votes: { - type: 'number', - optional: false, nullable: false, - }, - }, - }, - }, - }, - }, - emojis: { - type: 'object', - optional: true, nullable: false, - additionalProperties: { - anyOf: [{ - type: 'string', - }], - }, }, channelId: { type: 'string', @@ -170,30 +122,18 @@ export const packedNoteSchema = { channel: { type: 'object', optional: true, nullable: true, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - name: { - type: 'string', - optional: false, nullable: false, - }, - color: { - type: 'string', - optional: false, nullable: false, - }, - isSensitive: { - type: 'boolean', - optional: false, nullable: false, - }, - allowRenoteToExternal: { - type: 'boolean', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: true, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + name: { + type: 'string', + optional: false, nullable: true, + }, }, }, }, @@ -204,29 +144,10 @@ export const packedNoteSchema = { reactionAcceptance: { type: 'string', optional: false, nullable: true, - enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null], - }, - reactionEmojis: { - type: 'object', - optional: false, nullable: false, - additionalProperties: { - anyOf: [{ - type: 'string', - }], - }, }, reactions: { type: 'object', optional: false, nullable: false, - additionalProperties: { - anyOf: [{ - type: 'number', - }], - }, - }, - reactionCount: { - type: 'number', - optional: false, nullable: false, }, renoteCount: { type: 'number', @@ -244,25 +165,9 @@ export const packedNoteSchema = { type: 'string', optional: true, nullable: false, }, - reactionAndUserPairCache: { - type: 'array', - optional: true, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - }, - }, - clippedCount: { - type: 'number', - optional: true, nullable: false, - }, - hasPoll: { - type: 'boolean', - optional: true, nullable: false, - }, myReaction: { - type: 'string', + type: 'object', optional: true, nullable: true, }, }, diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 6de120c8d7..e88ca61ba0 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -1,17 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ +import { notificationTypes } from '@/types.js'; -import { notificationTypes, userExportableEntities } from '@/types.js'; - -const baseSchema = { +export const packedNotificationSchema = { type: 'object', properties: { id: { type: 'string', optional: false, nullable: false, format: 'id', + example: 'xxxxxxxxxx', }, createdAt: { type: 'string', @@ -21,428 +17,46 @@ const baseSchema = { type: { type: 'string', optional: false, nullable: false, - enum: [...notificationTypes, 'reaction:grouped', 'renote:grouped'], + enum: [...notificationTypes], + }, + user: { + type: 'object', + ref: 'UserLite', + optional: true, nullable: true, + }, + userId: { + type: 'string', + optional: true, nullable: true, + format: 'id', + }, + note: { + type: 'object', + ref: 'Note', + optional: true, nullable: true, + }, + reaction: { + type: 'string', + optional: true, nullable: true, + }, + choice: { + type: 'number', + optional: true, nullable: true, + }, + invitation: { + type: 'object', + optional: true, nullable: true, + }, + body: { + type: 'string', + optional: true, nullable: true, + }, + header: { + type: 'string', + optional: true, nullable: true, + }, + icon: { + type: 'string', + optional: true, nullable: true, }, }, } as const; - -export const packedNotificationSchema = { - type: 'object', - oneOf: [{ - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['note'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['mention'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['reply'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['renote'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['quote'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['reaction'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - reaction: { - type: 'string', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['pollEnded'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['follow'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['receiveFollowRequest'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['followRequestAccepted'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - message: { - type: 'string', - optional: false, nullable: true, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['roleAssigned'], - }, - role: { - type: 'object', - ref: 'Role', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['chatRoomInvitationReceived'], - }, - invitation: { - type: 'object', - ref: 'ChatRoomInvitation', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['achievementEarned'], - }, - achievement: { - ref: 'AchievementName', - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['exportCompleted'], - }, - exportedEntity: { - type: 'string', - optional: false, nullable: false, - enum: userExportableEntities, - }, - fileId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['login'], - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['createToken'], - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['app'], - }, - body: { - type: 'string', - optional: false, nullable: false, - }, - header: { - type: 'string', - optional: false, nullable: true, - }, - icon: { - type: 'string', - optional: false, nullable: true, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['reaction:grouped'], - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - reactions: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - properties: { - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - reaction: { - type: 'string', - optional: false, nullable: false, - }, - }, - required: ['user', 'reaction'], - }, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['renote:grouped'], - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - users: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['test'], - }, - }, - }], -} as const; diff --git a/packages/backend/src/models/json-schema/page.ts b/packages/backend/src/models/json-schema/page.ts index 748d6f1245..55ba3ce7f7 100644 --- a/packages/backend/src/models/json-schema/page.ts +++ b/packages/backend/src/models/json-schema/page.ts @@ -1,110 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -const blockBaseSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - type: { - type: 'string', - optional: false, nullable: false, - }, - }, -} as const; - -const textBlockSchema = { - type: 'object', - properties: { - ...blockBaseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['text'], - }, - text: { - type: 'string', - optional: false, nullable: false, - }, - }, -} as const; - -const sectionBlockSchema = { - type: 'object', - properties: { - ...blockBaseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['section'], - }, - title: { - type: 'string', - optional: false, nullable: false, - }, - children: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'PageBlock', - selfRef: true, - }, - }, - }, -} as const; - -const imageBlockSchema = { - type: 'object', - properties: { - ...blockBaseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['image'], - }, - fileId: { - type: 'string', - optional: false, nullable: true, - }, - }, -} as const; - -const noteBlockSchema = { - type: 'object', - properties: { - ...blockBaseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['note'], - }, - detailed: { - type: 'boolean', - optional: false, nullable: false, - }, - note: { - type: 'string', - optional: false, nullable: true, - }, - }, -} as const; - -export const packedPageBlockSchema = { - type: 'object', - oneOf: [ - textBlockSchema, - sectionBlockSchema, - imageBlockSchema, - noteBlockSchema, - ], -} as const; - export const packedPageSchema = { type: 'object', properties: { @@ -124,33 +17,6 @@ export const packedPageSchema = { optional: false, nullable: false, format: 'date-time', }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - content: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'PageBlock', - }, - }, - variables: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - }, - }, title: { type: 'string', optional: false, nullable: false, @@ -163,47 +29,23 @@ export const packedPageSchema = { type: 'string', optional: false, nullable: true, }, - hideTitleWhenPinned: { - type: 'boolean', - optional: false, nullable: false, - }, - alignCenter: { - type: 'boolean', - optional: false, nullable: false, - }, - font: { - type: 'string', - optional: false, nullable: false, - }, - script: { - type: 'string', - optional: false, nullable: false, - }, - eyeCatchingImageId: { - type: 'string', - optional: false, nullable: true, - }, - eyeCatchingImage: { - type: 'object', - optional: false, nullable: true, - ref: 'DriveFile', - }, - attachedFiles: { + content: { type: 'array', optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'DriveFile', - }, }, - likedCount: { - type: 'number', + variables: { + type: 'array', optional: false, nullable: false, }, - isLiked: { - type: 'boolean', - optional: true, nullable: false, + userId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + user: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, }, }, } as const; diff --git a/packages/backend/src/models/json-schema/queue.ts b/packages/backend/src/models/json-schema/queue.ts index dad0cf57f6..7ceeda26af 100644 --- a/packages/backend/src/models/json-schema/queue.ts +++ b/packages/backend/src/models/json-schema/queue.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedQueueCountSchema = { type: 'object', properties: { @@ -28,110 +23,3 @@ export const packedQueueCountSchema = { }, }, } as const; - -// Bull.Metrics -export const packedQueueMetricsSchema = { - type: 'object', - properties: { - meta: { - type: 'object', - optional: false, nullable: false, - properties: { - count: { - type: 'number', - optional: false, nullable: false, - }, - prevTS: { - type: 'number', - optional: false, nullable: false, - }, - prevCount: { - type: 'number', - optional: false, nullable: false, - }, - }, - }, - data: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'number', - optional: false, nullable: false, - }, - }, - count: { - type: 'number', - optional: false, nullable: false, - }, - }, -} as const; - -export const packedQueueJobSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - name: { - type: 'string', - optional: false, nullable: false, - }, - data: { - type: 'object', - optional: false, nullable: false, - }, - opts: { - type: 'object', - optional: false, nullable: false, - }, - timestamp: { - type: 'number', - optional: false, nullable: false, - }, - processedOn: { - type: 'number', - optional: true, nullable: false, - }, - processedBy: { - type: 'string', - optional: true, nullable: false, - }, - finishedOn: { - type: 'number', - optional: true, nullable: false, - }, - progress: { - type: 'object', - optional: false, nullable: false, - }, - attempts: { - type: 'number', - optional: false, nullable: false, - }, - delay: { - type: 'number', - optional: false, nullable: false, - }, - failedReason: { - type: 'string', - optional: false, nullable: false, - }, - stacktrace: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - }, - }, - returnValue: { - type: 'object', - optional: false, nullable: false, - }, - isFailed: { - type: 'boolean', - optional: false, nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/renote-muting.ts b/packages/backend/src/models/json-schema/renote-muting.ts index 344d6c7c00..69ed8510da 100644 --- a/packages/backend/src/models/json-schema/renote-muting.ts +++ b/packages/backend/src/models/json-schema/renote-muting.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedRenoteMutingSchema = { type: 'object', properties: { @@ -25,7 +20,7 @@ export const packedRenoteMutingSchema = { mutee: { type: 'object', optional: false, nullable: false, - ref: 'UserDetailedNotMe', + ref: 'UserDetailed', }, }, } as const; diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts deleted file mode 100644 index cb37200384..0000000000 --- a/packages/backend/src/models/json-schema/reversi-game.ts +++ /dev/null @@ -1,243 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedReversiGameLiteSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - createdAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - startedAt: { - type: 'string', - optional: false, nullable: true, - format: 'date-time', - }, - endedAt: { - type: 'string', - optional: false, nullable: true, - format: 'date-time', - }, - isStarted: { - type: 'boolean', - optional: false, nullable: false, - }, - isEnded: { - type: 'boolean', - optional: false, nullable: false, - }, - user1Id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - user2Id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - user1: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - user2: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - winnerId: { - type: 'string', - optional: false, nullable: true, - format: 'id', - }, - winner: { - type: 'object', - optional: false, nullable: true, - ref: 'UserLite', - }, - surrenderedUserId: { - type: 'string', - optional: false, nullable: true, - format: 'id', - }, - timeoutUserId: { - type: 'string', - optional: false, nullable: true, - format: 'id', - }, - black: { - type: 'number', - optional: false, nullable: true, - }, - bw: { - type: 'string', - optional: false, nullable: false, - }, - noIrregularRules: { - type: 'boolean', - optional: false, nullable: false, - }, - isLlotheo: { - type: 'boolean', - optional: false, nullable: false, - }, - canPutEverywhere: { - type: 'boolean', - optional: false, nullable: false, - }, - loopedBoard: { - type: 'boolean', - optional: false, nullable: false, - }, - timeLimitForEachTurn: { - type: 'number', - optional: false, nullable: false, - }, - }, -} as const; - -export const packedReversiGameDetailedSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - createdAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - startedAt: { - type: 'string', - optional: false, nullable: true, - format: 'date-time', - }, - endedAt: { - type: 'string', - optional: false, nullable: true, - format: 'date-time', - }, - isStarted: { - type: 'boolean', - optional: false, nullable: false, - }, - isEnded: { - type: 'boolean', - optional: false, nullable: false, - }, - form1: { - type: 'object', - optional: false, nullable: true, - }, - form2: { - type: 'object', - optional: false, nullable: true, - }, - user1Ready: { - type: 'boolean', - optional: false, nullable: false, - }, - user2Ready: { - type: 'boolean', - optional: false, nullable: false, - }, - user1Id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - user2Id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - user1: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - user2: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - winnerId: { - type: 'string', - optional: false, nullable: true, - format: 'id', - }, - winner: { - type: 'object', - optional: false, nullable: true, - ref: 'UserLite', - }, - surrenderedUserId: { - type: 'string', - optional: false, nullable: true, - format: 'id', - }, - timeoutUserId: { - type: 'string', - optional: false, nullable: true, - format: 'id', - }, - black: { - type: 'number', - optional: false, nullable: true, - }, - bw: { - type: 'string', - optional: false, nullable: false, - }, - noIrregularRules: { - type: 'boolean', - optional: false, nullable: false, - }, - isLlotheo: { - type: 'boolean', - optional: false, nullable: false, - }, - canPutEverywhere: { - type: 'boolean', - optional: false, nullable: false, - }, - loopedBoard: { - type: 'boolean', - optional: false, nullable: false, - }, - timeLimitForEachTurn: { - type: 'number', - optional: false, nullable: false, - }, - logs: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'number', - }, - }, - }, - map: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - }, - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts deleted file mode 100644 index 8bd01c92a3..0000000000 --- a/packages/backend/src/models/json-schema/role.ts +++ /dev/null @@ -1,449 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedRoleCondFormulaLogicsSchema = { - type: 'object', - properties: { - id: { - type: 'string', optional: false, - }, - type: { - type: 'string', - nullable: false, optional: false, - enum: ['and', 'or'], - }, - values: { - type: 'array', - nullable: false, optional: false, - items: { - ref: 'RoleCondFormulaValue', - }, - }, - }, -} as const; - -export const packedRoleCondFormulaValueNot = { - type: 'object', - properties: { - id: { - type: 'string', optional: false, - }, - type: { - type: 'string', - nullable: false, optional: false, - enum: ['not'], - }, - value: { - type: 'object', - optional: false, - ref: 'RoleCondFormulaValue', - }, - }, -} as const; - -export const packedRoleCondFormulaValueIsLocalOrRemoteSchema = { - type: 'object', - properties: { - id: { - type: 'string', optional: false, - }, - type: { - type: 'string', - nullable: false, optional: false, - enum: ['isLocal', 'isRemote'], - }, - }, -} as const; - -export const packedRoleCondFormulaValueUserSettingBooleanSchema = { - type: 'object', - properties: { - id: { - type: 'string', optional: false, - }, - type: { - type: 'string', - nullable: false, optional: false, - enum: ['isSuspended', 'isLocked', 'isBot', 'isCat', 'isExplorable'], - }, - }, -} as const; - -export const packedRoleCondFormulaValueAssignedRoleSchema = { - type: 'object', - properties: { - id: { - type: 'string', optional: false, - }, - type: { - type: 'string', - nullable: false, optional: false, - enum: ['roleAssignedTo'], - }, - roleId: { - type: 'string', - nullable: false, optional: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - }, -} as const; - -export const packedRoleCondFormulaValueCreatedSchema = { - type: 'object', - properties: { - id: { - type: 'string', optional: false, - }, - type: { - type: 'string', - nullable: false, optional: false, - enum: [ - 'createdLessThan', - 'createdMoreThan', - ], - }, - sec: { - type: 'number', - nullable: false, optional: false, - }, - }, -} as const; - -export const packedRoleCondFormulaFollowersOrFollowingOrNotesSchema = { - type: 'object', - properties: { - id: { - type: 'string', optional: false, - }, - type: { - type: 'string', - nullable: false, optional: false, - enum: [ - 'followersLessThanOrEq', - 'followersMoreThanOrEq', - 'followingLessThanOrEq', - 'followingMoreThanOrEq', - 'notesLessThanOrEq', - 'notesMoreThanOrEq', - ], - }, - value: { - type: 'number', - nullable: false, optional: false, - }, - }, -} as const; - -export const packedRoleCondFormulaValueSchema = { - type: 'object', - oneOf: [ - { - ref: 'RoleCondFormulaLogics', - }, - { - ref: 'RoleCondFormulaValueNot', - }, - { - ref: 'RoleCondFormulaValueIsLocalOrRemote', - }, - { - ref: 'RoleCondFormulaValueUserSettingBooleanSchema', - }, - { - ref: 'RoleCondFormulaValueAssignedRole', - }, - { - ref: 'RoleCondFormulaValueCreated', - }, - { - ref: 'RoleCondFormulaFollowersOrFollowingOrNotes', - }, - ], -} as const; - -export const packedRolePoliciesSchema = { - type: 'object', - optional: false, nullable: false, - properties: { - gtlAvailable: { - type: 'boolean', - optional: false, nullable: false, - }, - ltlAvailable: { - type: 'boolean', - optional: false, nullable: false, - }, - canPublicNote: { - type: 'boolean', - optional: false, nullable: false, - }, - mentionLimit: { - type: 'integer', - optional: false, nullable: false, - }, - canInvite: { - type: 'boolean', - optional: false, nullable: false, - }, - inviteLimit: { - type: 'integer', - optional: false, nullable: false, - }, - inviteLimitCycle: { - type: 'integer', - optional: false, nullable: false, - }, - inviteExpirationTime: { - type: 'integer', - optional: false, nullable: false, - }, - canManageCustomEmojis: { - type: 'boolean', - optional: false, nullable: false, - }, - canManageAvatarDecorations: { - type: 'boolean', - optional: false, nullable: false, - }, - canSearchNotes: { - type: 'boolean', - optional: false, nullable: false, - }, - canUseTranslator: { - type: 'boolean', - optional: false, nullable: false, - }, - canHideAds: { - type: 'boolean', - optional: false, nullable: false, - }, - driveCapacityMb: { - type: 'integer', - optional: false, nullable: false, - }, - maxFileSizeMb: { - type: 'integer', - optional: false, nullable: false, - }, - uploadableFileTypes: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - }, - }, - alwaysMarkNsfw: { - type: 'boolean', - optional: false, nullable: false, - }, - canUpdateBioMedia: { - type: 'boolean', - optional: false, nullable: false, - }, - pinLimit: { - type: 'integer', - optional: false, nullable: false, - }, - antennaLimit: { - type: 'integer', - optional: false, nullable: false, - }, - wordMuteLimit: { - type: 'integer', - optional: false, nullable: false, - }, - webhookLimit: { - type: 'integer', - optional: false, nullable: false, - }, - clipLimit: { - type: 'integer', - optional: false, nullable: false, - }, - noteEachClipsLimit: { - type: 'integer', - optional: false, nullable: false, - }, - userListLimit: { - type: 'integer', - optional: false, nullable: false, - }, - userEachUserListsLimit: { - type: 'integer', - optional: false, nullable: false, - }, - rateLimitFactor: { - type: 'integer', - optional: false, nullable: false, - }, - avatarDecorationLimit: { - type: 'integer', - optional: false, nullable: false, - }, - canImportAntennas: { - type: 'boolean', - optional: false, nullable: false, - }, - canImportBlocking: { - type: 'boolean', - optional: false, nullable: false, - }, - canImportFollowing: { - type: 'boolean', - optional: false, nullable: false, - }, - canImportMuting: { - type: 'boolean', - optional: false, nullable: false, - }, - canImportUserLists: { - type: 'boolean', - optional: false, nullable: false, - }, - chatAvailability: { - type: 'string', - optional: false, nullable: false, - enum: ['available', 'readonly', 'unavailable'], - }, - }, -} as const; - -export const packedRoleLiteSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - name: { - type: 'string', - optional: false, nullable: false, - example: 'New Role', - }, - color: { - type: 'string', - optional: false, nullable: true, - example: '#000000', - }, - iconUrl: { - type: 'string', - optional: false, nullable: true, - }, - description: { - type: 'string', - optional: false, nullable: false, - }, - isModerator: { - type: 'boolean', - optional: false, nullable: false, - example: false, - }, - isAdministrator: { - type: 'boolean', - optional: false, nullable: false, - example: false, - }, - displayOrder: { - type: 'integer', - optional: false, nullable: false, - example: 0, - }, - }, -} as const; - -export const packedRoleSchema = { - type: 'object', - allOf: [ - { - type: 'object', - ref: 'RoleLite', - }, - { - type: 'object', - properties: { - createdAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - updatedAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - target: { - type: 'string', - optional: false, nullable: false, - enum: ['manual', 'conditional'], - }, - condFormula: { - type: 'object', - optional: false, nullable: false, - ref: 'RoleCondFormulaValue', - }, - isPublic: { - type: 'boolean', - optional: false, nullable: false, - example: false, - }, - isExplorable: { - type: 'boolean', - optional: false, nullable: false, - example: false, - }, - asBadge: { - type: 'boolean', - optional: false, nullable: false, - example: false, - }, - preserveAssignmentOnMoveAccount: { - type: 'boolean', - optional: false, nullable: false, - example: false, - }, - canEditMembersByModerator: { - type: 'boolean', - optional: false, nullable: false, - example: false, - }, - policies: { - type: 'object', - optional: false, nullable: false, - additionalProperties: { - anyOf: [{ - type: 'object', - properties: { - value: { - oneOf: [ - { - type: 'integer', - }, - { - type: 'boolean', - }, - ], - }, - priority: { - type: 'integer', - }, - useDefault: { - type: 'boolean', - }, - }, - }], - }, - }, - usersCount: { - type: 'integer', - optional: false, nullable: false, - }, - }, - }, - ], -} as const; diff --git a/packages/backend/src/models/json-schema/signin.ts b/packages/backend/src/models/json-schema/signin.ts deleted file mode 100644 index 45732a742b..0000000000 --- a/packages/backend/src/models/json-schema/signin.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedSigninSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - createdAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - ip: { - type: 'string', - optional: false, nullable: false, - }, - headers: { - type: 'object', - optional: false, nullable: false, - }, - success: { - type: 'boolean', - optional: false, nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/system-webhook.ts b/packages/backend/src/models/json-schema/system-webhook.ts deleted file mode 100644 index d83065a743..0000000000 --- a/packages/backend/src/models/json-schema/system-webhook.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { systemWebhookEventTypes } from '@/models/SystemWebhook.js'; - -export const packedSystemWebhookSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - isActive: { - type: 'boolean', - optional: false, nullable: false, - }, - updatedAt: { - type: 'string', - format: 'date-time', - optional: false, nullable: false, - }, - latestSentAt: { - type: 'string', - format: 'date-time', - optional: false, nullable: true, - }, - latestStatus: { - type: 'number', - optional: false, nullable: true, - }, - name: { - type: 'string', - optional: false, nullable: false, - }, - on: { - type: 'array', - items: { - type: 'string', - optional: false, nullable: false, - enum: systemWebhookEventTypes, - }, - }, - url: { - type: 'string', - optional: false, nullable: false, - }, - secret: { - type: 'string', - optional: false, nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/user-list.ts b/packages/backend/src/models/json-schema/user-list.ts index dc9af25602..1e620516e4 100644 --- a/packages/backend/src/models/json-schema/user-list.ts +++ b/packages/backend/src/models/json-schema/user-list.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedUserListSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 2b5f706ff9..f9a20ac398 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -1,42 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const notificationRecieveConfig = { - type: 'object', - oneOf: [ - { - type: 'object', - nullable: false, - properties: { - type: { - type: 'string', - nullable: false, - enum: ['all', 'following', 'follower', 'mutualFollow', 'followingOrFollower', 'never'], - }, - }, - required: ['type'], - }, - { - type: 'object', - nullable: false, - properties: { - type: { - type: 'string', - nullable: false, - enum: ['list'], - }, - userListId: { - type: 'string', - format: 'misskey:id', - }, - }, - required: ['type', 'userListId'], - }, - ], -} as const; - export const packedUserLiteSchema = { type: 'object', properties: { @@ -71,41 +32,15 @@ export const packedUserLiteSchema = { type: 'string', nullable: true, optional: false, }, - avatarDecorations: { - type: 'array', - nullable: false, optional: false, - items: { - type: 'object', - nullable: false, optional: false, - properties: { - id: { - type: 'string', - nullable: false, optional: false, - format: 'id', - }, - angle: { - type: 'number', - nullable: false, optional: true, - }, - flipH: { - type: 'boolean', - nullable: false, optional: true, - }, - url: { - type: 'string', - format: 'url', - nullable: false, optional: false, - }, - offsetX: { - type: 'number', - nullable: false, optional: true, - }, - offsetY: { - type: 'number', - nullable: false, optional: true, - }, - }, - }, + isAdmin: { + type: 'boolean', + nullable: false, optional: true, + default: false, + }, + isModerator: { + type: 'boolean', + nullable: false, optional: true, + default: false, }, isBot: { type: 'boolean', @@ -115,82 +50,12 @@ export const packedUserLiteSchema = { type: 'boolean', nullable: false, optional: true, }, - requireSigninToViewContents: { - type: 'boolean', - nullable: false, optional: true, - }, - makeNotesFollowersOnlyBefore: { - type: 'number', - nullable: true, optional: true, - }, - makeNotesHiddenBefore: { - type: 'number', - nullable: true, optional: true, - }, - instance: { - type: 'object', - nullable: false, optional: true, - properties: { - name: { - type: 'string', - nullable: true, optional: false, - }, - softwareName: { - type: 'string', - nullable: true, optional: false, - }, - softwareVersion: { - type: 'string', - nullable: true, optional: false, - }, - iconUrl: { - type: 'string', - nullable: true, optional: false, - }, - faviconUrl: { - type: 'string', - nullable: true, optional: false, - }, - themeColor: { - type: 'string', - nullable: true, optional: false, - }, - }, - }, - emojis: { - type: 'object', - nullable: false, optional: false, - additionalProperties: { - type: 'string', - }, - }, onlineStatus: { type: 'string', - nullable: false, optional: false, + format: 'url', + nullable: true, optional: false, enum: ['unknown', 'online', 'active', 'offline'], }, - badgeRoles: { - type: 'array', - nullable: false, optional: true, - items: { - type: 'object', - nullable: false, optional: false, - properties: { - name: { - type: 'string', - nullable: false, optional: false, - }, - iconUrl: { - type: 'string', - nullable: true, optional: false, - }, - displayOrder: { - type: 'number', - nullable: false, optional: false, - }, - }, - }, - }, }, } as const; @@ -207,18 +72,21 @@ export const packedUserDetailedNotMeOnlySchema = { format: 'uri', nullable: true, optional: false, }, - movedTo: { + movedToUri: { type: 'string', format: 'uri', - nullable: true, optional: false, + nullable: true, + optional: false, }, alsoKnownAs: { type: 'array', - nullable: true, optional: false, + nullable: true, + optional: false, items: { type: 'string', format: 'id', - nullable: false, optional: false, + nullable: false, + optional: false, }, }, createdAt: { @@ -296,15 +164,6 @@ export const packedUserDetailedNotMeOnlySchema = { }, }, }, - verifiedLinks: { - type: 'array', - nullable: false, optional: false, - items: { - type: 'string', - nullable: false, optional: false, - format: 'url', - }, - }, followersCount: { type: 'number', nullable: false, optional: false, @@ -348,57 +207,20 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: false, }, - followingVisibility: { - type: 'string', - nullable: false, optional: false, - enum: ['public', 'followers', 'private'], - }, - followersVisibility: { - type: 'string', - nullable: false, optional: false, - enum: ['public', 'followers', 'private'], - }, - chatScope: { - type: 'string', - nullable: false, optional: false, - enum: ['everyone', 'following', 'followers', 'mutual', 'none'], - }, - canChat: { - type: 'boolean', - nullable: false, optional: false, - }, - roles: { - type: 'array', - nullable: false, optional: false, - items: { - type: 'object', - nullable: false, optional: false, - ref: 'RoleLite', - }, - }, - followedMessage: { - type: 'string', - nullable: true, optional: true, - }, - memo: { - type: 'string', - nullable: true, optional: false, - }, - moderationNote: { - type: 'string', - nullable: false, optional: true, - }, twoFactorEnabled: { type: 'boolean', - nullable: false, optional: true, + nullable: false, optional: false, + default: false, }, usePasswordLessLogin: { type: 'boolean', - nullable: false, optional: true, + nullable: false, optional: false, + default: false, }, securityKeys: { type: 'boolean', - nullable: false, optional: true, + nullable: false, optional: false, + default: false, }, //#region relations isFollowing: { @@ -433,14 +255,9 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: true, }, - notify: { + memo: { type: 'string', nullable: false, optional: true, - enum: ['normal', 'none'], - }, - withReplies: { - type: 'boolean', - nullable: false, optional: true, }, //#endregion }, @@ -459,41 +276,29 @@ export const packedMeDetailedOnlySchema = { nullable: true, optional: false, format: 'id', }, - followedMessage: { - type: 'string', - nullable: true, optional: false, - }, - isModerator: { - type: 'boolean', - nullable: true, optional: false, - }, - isAdmin: { - type: 'boolean', - nullable: true, optional: false, - }, injectFeaturedNote: { type: 'boolean', - nullable: false, optional: false, + nullable: true, optional: false, }, receiveAnnouncementEmail: { type: 'boolean', - nullable: false, optional: false, + nullable: true, optional: false, }, alwaysMarkNsfw: { type: 'boolean', - nullable: false, optional: false, + nullable: true, optional: false, }, autoSensitive: { type: 'boolean', - nullable: false, optional: false, + nullable: true, optional: false, }, carefulBot: { type: 'boolean', - nullable: false, optional: false, + nullable: true, optional: false, }, autoAcceptFollowed: { type: 'boolean', - nullable: false, optional: false, + nullable: true, optional: false, }, noCrawle: { type: 'boolean', @@ -511,11 +316,6 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: false, optional: false, }, - twoFactorBackupCodesStock: { - type: 'string', - enum: ['full', 'partial', 'none'], - nullable: false, optional: false, - }, hideOnlineStatus: { type: 'boolean', nullable: false, optional: false, @@ -532,27 +332,10 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: false, optional: false, }, - unreadAnnouncements: { - type: 'array', - nullable: false, optional: false, - items: { - type: 'object', - nullable: false, optional: false, - ref: 'Announcement', - }, - }, hasUnreadAntenna: { type: 'boolean', nullable: false, optional: false, }, - hasUnreadChannel: { - type: 'boolean', - nullable: false, optional: false, - }, - hasUnreadChatMessages: { - type: 'boolean', - nullable: false, optional: false, - }, hasUnreadNotification: { type: 'boolean', nullable: false, optional: false, @@ -561,10 +344,6 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: false, optional: false, }, - unreadNotificationsCount: { - type: 'number', - nullable: false, optional: false, - }, mutedWords: { type: 'array', nullable: false, optional: false, @@ -577,18 +356,6 @@ export const packedMeDetailedOnlySchema = { }, }, }, - hardMutedWords: { - type: 'array', - nullable: false, optional: false, - items: { - type: 'array', - nullable: false, optional: false, - items: { - type: 'string', - nullable: false, optional: false, - }, - }, - }, mutedInstances: { type: 'array', nullable: true, optional: false, @@ -597,66 +364,22 @@ export const packedMeDetailedOnlySchema = { nullable: false, optional: false, }, }, - notificationRecieveConfig: { - type: 'object', - nullable: false, optional: false, - properties: { - note: { optional: true, ...notificationRecieveConfig }, - follow: { optional: true, ...notificationRecieveConfig }, - mention: { optional: true, ...notificationRecieveConfig }, - reply: { optional: true, ...notificationRecieveConfig }, - renote: { optional: true, ...notificationRecieveConfig }, - quote: { optional: true, ...notificationRecieveConfig }, - reaction: { optional: true, ...notificationRecieveConfig }, - pollEnded: { optional: true, ...notificationRecieveConfig }, - receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, - followRequestAccepted: { optional: true, ...notificationRecieveConfig }, - roleAssigned: { optional: true, ...notificationRecieveConfig }, - chatRoomInvitationReceived: { optional: true, ...notificationRecieveConfig }, - achievementEarned: { optional: true, ...notificationRecieveConfig }, - app: { optional: true, ...notificationRecieveConfig }, - test: { optional: true, ...notificationRecieveConfig }, - }, - }, - emailNotificationTypes: { + mutingNotificationTypes: { type: 'array', - nullable: false, optional: false, + nullable: true, optional: false, items: { type: 'string', nullable: false, optional: false, }, }, - achievements: { + emailNotificationTypes: { type: 'array', - nullable: false, optional: false, + nullable: true, optional: false, items: { - ref: 'Achievement', + type: 'string', + nullable: false, optional: false, }, }, - loggedInDays: { - type: 'number', - nullable: false, optional: false, - }, - policies: { - type: 'object', - nullable: false, optional: false, - ref: 'RolePolicies', - }, - twoFactorEnabled: { - type: 'boolean', - nullable: false, optional: false, - default: false, - }, - usePasswordLessLogin: { - type: 'boolean', - nullable: false, optional: false, - default: false, - }, - securityKeys: { - type: 'boolean', - nullable: false, optional: false, - default: false, - }, //#region secrets email: { type: 'string', @@ -672,23 +395,6 @@ export const packedMeDetailedOnlySchema = { items: { type: 'object', nullable: false, optional: false, - properties: { - id: { - type: 'string', - nullable: false, optional: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - name: { - type: 'string', - nullable: false, optional: false, - }, - lastUsed: { - type: 'string', - nullable: false, optional: false, - format: 'date-time', - }, - }, }, }, //#endregion diff --git a/packages/backend/src/models/util/id.ts b/packages/backend/src/models/util/id.ts deleted file mode 100644 index 2d742702c7..0000000000 --- a/packages/backend/src/models/util/id.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const id = () => ({ - type: 'varchar' as const, - length: 32, -}); diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index b06895fcc9..488979c409 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -1,169 +1,107 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - // https://github.com/typeorm/typeorm/issues/2400 import pg from 'pg'; -import { DataSource, Logger, type QueryRunner } from 'typeorm'; +pg.types.setTypeParser(20, Number); + +import { DataSource, Logger } from 'typeorm'; import * as highlight from 'cli-highlight'; import { entities as charts } from '@/core/chart/entities.js'; + +import { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; +import { AccessToken } from '@/models/entities/AccessToken.js'; +import { Ad } from '@/models/entities/Ad.js'; +import { Announcement } from '@/models/entities/Announcement.js'; +import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js'; +import { Antenna } from '@/models/entities/Antenna.js'; +import { App } from '@/models/entities/App.js'; +import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js'; +import { AuthSession } from '@/models/entities/AuthSession.js'; +import { Blocking } from '@/models/entities/Blocking.js'; +import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js'; +import { ChannelFavorite } from '@/models/entities/ChannelFavorite.js'; +import { Clip } from '@/models/entities/Clip.js'; +import { ClipNote } from '@/models/entities/ClipNote.js'; +import { ClipFavorite } from '@/models/entities/ClipFavorite.js'; +import { DriveFile } from '@/models/entities/DriveFile.js'; +import { DriveFolder } from '@/models/entities/DriveFolder.js'; +import { Emoji } from '@/models/entities/Emoji.js'; +import { Following } from '@/models/entities/Following.js'; +import { FollowRequest } from '@/models/entities/FollowRequest.js'; +import { GalleryLike } from '@/models/entities/GalleryLike.js'; +import { GalleryPost } from '@/models/entities/GalleryPost.js'; +import { Hashtag } from '@/models/entities/Hashtag.js'; +import { Instance } from '@/models/entities/Instance.js'; +import { Meta } from '@/models/entities/Meta.js'; +import { ModerationLog } from '@/models/entities/ModerationLog.js'; +import { MutedNote } from '@/models/entities/MutedNote.js'; +import { Muting } from '@/models/entities/Muting.js'; +import { RenoteMuting } from '@/models/entities/RenoteMuting.js'; +import { Note } from '@/models/entities/Note.js'; +import { NoteFavorite } from '@/models/entities/NoteFavorite.js'; +import { NoteReaction } from '@/models/entities/NoteReaction.js'; +import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js'; +import { NoteUnread } from '@/models/entities/NoteUnread.js'; +import { Page } from '@/models/entities/Page.js'; +import { PageLike } from '@/models/entities/PageLike.js'; +import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js'; +import { Poll } from '@/models/entities/Poll.js'; +import { PollVote } from '@/models/entities/PollVote.js'; +import { PromoNote } from '@/models/entities/PromoNote.js'; +import { PromoRead } from '@/models/entities/PromoRead.js'; +import { RegistrationTicket } from '@/models/entities/RegistrationTicket.js'; +import { RegistryItem } from '@/models/entities/RegistryItem.js'; +import { Relay } from '@/models/entities/Relay.js'; +import { Signin } from '@/models/entities/Signin.js'; +import { SwSubscription } from '@/models/entities/SwSubscription.js'; +import { UsedUsername } from '@/models/entities/UsedUsername.js'; +import { User } from '@/models/entities/User.js'; +import { UserIp } from '@/models/entities/UserIp.js'; +import { UserKeypair } from '@/models/entities/UserKeypair.js'; +import { UserList } from '@/models/entities/UserList.js'; +import { UserListFavorite } from '@/models/entities/UserListFavorite.js'; +import { UserListJoining } from '@/models/entities/UserListJoining.js'; +import { UserNotePining } from '@/models/entities/UserNotePining.js'; +import { UserPending } from '@/models/entities/UserPending.js'; +import { UserProfile } from '@/models/entities/UserProfile.js'; +import { UserPublickey } from '@/models/entities/UserPublickey.js'; +import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js'; +import { Webhook } from '@/models/entities/Webhook.js'; +import { Channel } from '@/models/entities/Channel.js'; +import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js'; +import { Role } from '@/models/entities/Role.js'; +import { RoleAssignment } from '@/models/entities/RoleAssignment.js'; +import { Flash } from '@/models/entities/Flash.js'; +import { FlashLike } from '@/models/entities/FlashLike.js'; +import { UserMemo } from '@/models/entities/UserMemo.js'; + import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; -import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; -import { MiAccessToken } from '@/models/AccessToken.js'; -import { MiAd } from '@/models/Ad.js'; -import { MiAnnouncement } from '@/models/Announcement.js'; -import { MiAnnouncementRead } from '@/models/AnnouncementRead.js'; -import { MiAntenna } from '@/models/Antenna.js'; -import { MiApp } from '@/models/App.js'; -import { MiAvatarDecoration } from '@/models/AvatarDecoration.js'; -import { MiAuthSession } from '@/models/AuthSession.js'; -import { MiBlocking } from '@/models/Blocking.js'; -import { MiChannelFollowing } from '@/models/ChannelFollowing.js'; -import { MiChannelFavorite } from '@/models/ChannelFavorite.js'; -import { MiClip } from '@/models/Clip.js'; -import { MiClipNote } from '@/models/ClipNote.js'; -import { MiClipFavorite } from '@/models/ClipFavorite.js'; -import { MiDriveFile } from '@/models/DriveFile.js'; -import { MiDriveFolder } from '@/models/DriveFolder.js'; -import { MiEmoji } from '@/models/Emoji.js'; -import { MiFollowing } from '@/models/Following.js'; -import { MiFollowRequest } from '@/models/FollowRequest.js'; -import { MiGalleryLike } from '@/models/GalleryLike.js'; -import { MiGalleryPost } from '@/models/GalleryPost.js'; -import { MiHashtag } from '@/models/Hashtag.js'; -import { MiInstance } from '@/models/Instance.js'; -import { MiMeta } from '@/models/Meta.js'; -import { MiModerationLog } from '@/models/ModerationLog.js'; -import { MiMuting } from '@/models/Muting.js'; -import { MiRenoteMuting } from '@/models/RenoteMuting.js'; -import { MiNote } from '@/models/Note.js'; -import { MiNoteFavorite } from '@/models/NoteFavorite.js'; -import { MiNoteReaction } from '@/models/NoteReaction.js'; -import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js'; -import { MiPage } from '@/models/Page.js'; -import { MiPageLike } from '@/models/PageLike.js'; -import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js'; -import { MiPoll } from '@/models/Poll.js'; -import { MiPollVote } from '@/models/PollVote.js'; -import { MiPromoNote } from '@/models/PromoNote.js'; -import { MiPromoRead } from '@/models/PromoRead.js'; -import { MiRegistrationTicket } from '@/models/RegistrationTicket.js'; -import { MiRegistryItem } from '@/models/RegistryItem.js'; -import { MiRelay } from '@/models/Relay.js'; -import { MiSignin } from '@/models/Signin.js'; -import { MiSwSubscription } from '@/models/SwSubscription.js'; -import { MiUsedUsername } from '@/models/UsedUsername.js'; -import { MiUser } from '@/models/User.js'; -import { MiUserIp } from '@/models/UserIp.js'; -import { MiUserKeypair } from '@/models/UserKeypair.js'; -import { MiUserList } from '@/models/UserList.js'; -import { MiUserListFavorite } from '@/models/UserListFavorite.js'; -import { MiUserListMembership } from '@/models/UserListMembership.js'; -import { MiUserNotePining } from '@/models/UserNotePining.js'; -import { MiUserPending } from '@/models/UserPending.js'; -import { MiUserProfile } from '@/models/UserProfile.js'; -import { MiUserPublickey } from '@/models/UserPublickey.js'; -import { MiUserSecurityKey } from '@/models/UserSecurityKey.js'; -import { MiWebhook } from '@/models/Webhook.js'; -import { MiSystemWebhook } from '@/models/SystemWebhook.js'; -import { MiChannel } from '@/models/Channel.js'; -import { MiRetentionAggregation } from '@/models/RetentionAggregation.js'; -import { MiRole } from '@/models/Role.js'; -import { MiRoleAssignment } from '@/models/RoleAssignment.js'; -import { MiFlash } from '@/models/Flash.js'; -import { MiFlashLike } from '@/models/FlashLike.js'; -import { MiUserMemo } from '@/models/UserMemo.js'; -import { MiChatMessage } from '@/models/ChatMessage.js'; -import { MiChatRoom } from '@/models/ChatRoom.js'; -import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js'; -import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js'; -import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; -import { MiReversiGame } from '@/models/ReversiGame.js'; -import { MiChatApproval } from '@/models/ChatApproval.js'; -import { MiSystemAccount } from '@/models/SystemAccount.js'; - -pg.types.setTypeParser(20, Number); - export const dbLogger = new MisskeyLogger('db'); -const sqlLogger = dbLogger.createSubLogger('sql', 'gray'); - -export type LoggerProps = { - disableQueryTruncation?: boolean; - enableQueryParamLogging?: boolean; - printReplicationMode?: boolean, -}; - -function highlightSql(sql: string) { - return highlight.highlight(sql, { - language: 'sql', ignoreIllegals: true, - }); -} - -function truncateSql(sql: string) { - return sql.length > 100 ? `${sql.substring(0, 100)}...` : sql; -} - -function stringifyParameter(param: any) { - if (param instanceof Date) { - return param.toISOString(); - } else { - return param; - } -} +const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); class MyCustomLogger implements Logger { - constructor(private props: LoggerProps = {}) { + @bindThis + private highlight(sql: string) { + return highlight.highlight(sql, { + language: 'sql', ignoreIllegals: true, + }); } @bindThis - private transformQueryLog(sql: string, opts?: { - prefix?: string; - }) { - let modded = opts?.prefix ? opts.prefix + sql : sql; - if (!this.props.disableQueryTruncation) { - modded = truncateSql(modded); - } - - return highlightSql(modded); + public logQuery(query: string, parameters?: any[]) { + sqlLogger.info(this.highlight(query).substring(0, 100)); } @bindThis - private transformParameters(parameters?: any[]) { - if (this.props.enableQueryParamLogging && parameters && parameters.length > 0) { - return parameters.map(stringifyParameter); - } - - return undefined; + public logQueryError(error: string, query: string, parameters?: any[]) { + sqlLogger.error(this.highlight(query)); } @bindThis - public logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) { - const prefix = (this.props.printReplicationMode && queryRunner) - ? `[${queryRunner.getReplicationMode()}] ` - : undefined; - sqlLogger.info(this.transformQueryLog(query, { prefix }), this.transformParameters(parameters)); - } - - @bindThis - public logQueryError(error: string, query: string, parameters?: any[], queryRunner?: QueryRunner) { - const prefix = (this.props.printReplicationMode && queryRunner) - ? `[${queryRunner.getReplicationMode()}] ` - : undefined; - sqlLogger.error(this.transformQueryLog(query, { prefix }), this.transformParameters(parameters)); - } - - @bindThis - public logQuerySlow(time: number, query: string, parameters?: any[], queryRunner?: QueryRunner) { - const prefix = (this.props.printReplicationMode && queryRunner) - ? `[${queryRunner.getReplicationMode()}] ` - : undefined; - sqlLogger.warn(this.transformQueryLog(query, { prefix }), this.transformParameters(parameters)); + public logQuerySlow(time: number, query: string, parameters?: any[]) { + sqlLogger.warn(this.highlight(query)); } @bindThis @@ -183,80 +121,72 @@ class MyCustomLogger implements Logger { } export const entities = [ - MiAnnouncement, - MiAnnouncementRead, - MiMeta, - MiInstance, - MiApp, - MiAvatarDecoration, - MiAuthSession, - MiAccessToken, - MiUser, - MiUserProfile, - MiUserKeypair, - MiUserPublickey, - MiUserList, - MiUserListFavorite, - MiUserListMembership, - MiUserNotePining, - MiUserSecurityKey, - MiUsedUsername, - MiFollowing, - MiFollowRequest, - MiMuting, - MiRenoteMuting, - MiBlocking, - MiNote, - MiNoteFavorite, - MiNoteReaction, - MiNoteThreadMuting, - MiPage, - MiPageLike, - MiGalleryPost, - MiGalleryLike, - MiDriveFile, - MiDriveFolder, - MiPoll, - MiPollVote, - MiEmoji, - MiHashtag, - MiSwSubscription, - MiSystemAccount, - MiAbuseUserReport, - MiAbuseReportNotificationRecipient, - MiRegistrationTicket, - MiSignin, - MiModerationLog, - MiClip, - MiClipNote, - MiClipFavorite, - MiAntenna, - MiPromoNote, - MiPromoRead, - MiRelay, - MiChannel, - MiChannelFollowing, - MiChannelFavorite, - MiRegistryItem, - MiAd, - MiPasswordResetRequest, - MiUserPending, - MiWebhook, - MiSystemWebhook, - MiUserIp, - MiRetentionAggregation, - MiRole, - MiRoleAssignment, - MiFlash, - MiFlashLike, - MiUserMemo, - MiChatMessage, - MiChatRoom, - MiChatRoomMembership, - MiChatRoomInvitation, - MiChatApproval, - MiBubbleGameRecord, - MiReversiGame, + Announcement, + AnnouncementRead, + Meta, + Instance, + App, + AuthSession, + AccessToken, + User, + UserProfile, + UserKeypair, + UserPublickey, + UserList, + UserListFavorite, + UserListJoining, + UserNotePining, + UserSecurityKey, + UsedUsername, + AttestationChallenge, + Following, + FollowRequest, + Muting, + RenoteMuting, + Blocking, + Note, + NoteFavorite, + NoteReaction, + NoteThreadMuting, + NoteUnread, + Page, + PageLike, + GalleryPost, + GalleryLike, + DriveFile, + DriveFolder, + Poll, + PollVote, + Emoji, + Hashtag, + SwSubscription, + AbuseUserReport, + RegistrationTicket, + Signin, + ModerationLog, + Clip, + ClipNote, + ClipFavorite, + Antenna, + PromoNote, + PromoRead, + Relay, + MutedNote, + Channel, + ChannelFollowing, + ChannelFavorite, + RegistryItem, + Ad, + PasswordResetRequest, + UserPending, + Webhook, + UserIp, + RetentionAggregation, + Role, + RoleAssignment, + Flash, + FlashLike, + UserMemo, ...charts, ]; @@ -274,24 +204,22 @@ export function createPostgresDataSource(config: Config) { statement_timeout: 1000 * 10, ...config.db.extra, }, - ...(config.dbReplications ? { - replication: { - master: { - host: config.db.host, - port: config.db.port, - username: config.db.user, - password: config.db.pass, - database: config.db.db, - }, - slaves: config.dbSlaves!.map(rep => ({ - host: rep.host, - port: rep.port, - username: rep.user, - password: rep.pass, - database: rep.db, - })), + replication: config.dbReplications ? { + master: { + host: config.db.host, + port: config.db.port, + username: config.db.user, + password: config.db.pass, + database: config.db.db, }, - } : {}), + slaves: config.dbSlaves!.map(rep => ({ + host: rep.host, + port: rep.port, + username: rep.user, + password: rep.pass, + database: rep.db, + })), + } : undefined, synchronize: process.env.NODE_ENV === 'test', dropSchema: process.env.NODE_ENV === 'test', cache: !config.db.disableCache && process.env.NODE_ENV !== 'test' ? { // dbをcloseしても何故かredisのコネクションが内部的に残り続けるようで、テストの際に支障が出るため無効にする(キャッシュも含めてテストしたいため本当は有効にしたいが...) @@ -299,20 +227,14 @@ export function createPostgresDataSource(config: Config) { options: { host: config.redis.host, port: config.redis.port, - family: config.redis.family ?? 0, + family: config.redis.family == null ? 0 : config.redis.family, password: config.redis.pass, keyPrefix: `${config.redis.prefix}:query:`, db: config.redis.db ?? 0, }, } : false, logging: log, - logger: log - ? new MyCustomLogger({ - disableQueryTruncation: config.logging?.sql?.disableQueryTruncation, - enableQueryParamLogging: config.logging?.sql?.enableQueryParamLogging, - printReplicationMode: !!config.dbReplications, - }) - : undefined, + logger: log ? new MyCustomLogger() : undefined, maxQueryExecutionTime: 300, entities: entities, migrations: ['../../migration/*.js'], diff --git a/packages/backend/src/queue/QueueLoggerService.ts b/packages/backend/src/queue/QueueLoggerService.ts index 65869afd46..648af893c2 100644 --- a/packages/backend/src/queue/QueueLoggerService.ts +++ b/packages/backend/src/queue/QueueLoggerService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 9044285bf6..e1c6b93d9b 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -1,21 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; import { GlobalModule } from '@/GlobalModule.js'; -import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueProcessorService } from './QueueProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; -import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; -import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; +import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js'; import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; -import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; @@ -27,7 +19,6 @@ import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmo import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js'; import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; -import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js'; import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js'; import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js'; import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js'; @@ -53,12 +44,10 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor ResyncChartsProcessorService, CleanChartsProcessorService, CheckExpiredMutingsProcessorService, - BakeBufferedReactionsProcessorService, CleanProcessorService, DeleteDriveFilesProcessorService, ExportCustomEmojisProcessorService, ExportNotesProcessorService, - ExportClipsProcessorService, ExportFavoritesProcessorService, ExportFollowingProcessorService, ExportMutingProcessorService, @@ -75,14 +64,11 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor DeleteFileProcessorService, CleanRemoteFilesProcessorService, RelationshipProcessorService, - UserWebhookDeliverProcessorService, - SystemWebhookDeliverProcessorService, + WebhookDeliverProcessorService, EndedPollNotificationProcessorService, DeliverProcessorService, InboxProcessorService, AggregateRetentionProcessorService, - CheckExpiredMutingsProcessorService, - CheckModeratorsActivityProcessorService, QueueProcessorService, ], exports: [ diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index c98ebcdcd9..42f9c1af7d 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -1,25 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Bull from 'bullmq'; -import * as Sentry from '@sentry/node'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; -import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; -import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; +import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js'; import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; -import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js'; import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js'; import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js'; @@ -40,11 +31,10 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; -import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; -import { QUEUE, baseWorkerOptions } from './const.js'; +import { QUEUE, baseQueueOptions } from './const.js'; // ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019 function httpRelatedBackoff(attemptsMade: number) { @@ -67,7 +57,7 @@ function getJobInfo(job: Bull.Job | undefined, increment = false): string { // onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする const currentAttempts = job.attemptsMade + (increment ? 1 : 0); - const maxAttempts = job.opts.attempts ?? 0; + const maxAttempts = job.opts ? job.opts.attempts : 0; return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`; } @@ -79,8 +69,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private dbQueueWorker: Bull.Worker; private deliverQueueWorker: Bull.Worker; private inboxQueueWorker: Bull.Worker; - private userWebhookDeliverQueueWorker: Bull.Worker; - private systemWebhookDeliverQueueWorker: Bull.Worker; + private webhookDeliverQueueWorker: Bull.Worker; private relationshipQueueWorker: Bull.Worker; private objectStorageQueueWorker: Bull.Worker; private endedPollNotificationQueueWorker: Bull.Worker; @@ -90,15 +79,13 @@ export class QueueProcessorService implements OnApplicationShutdown { private config: Config, private queueLoggerService: QueueLoggerService, - private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService, - private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService, + private webhookDeliverProcessorService: WebhookDeliverProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, private deliverProcessorService: DeliverProcessorService, private inboxProcessorService: InboxProcessorService, private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService, private exportNotesProcessorService: ExportNotesProcessorService, - private exportClipsProcessorService: ExportClipsProcessorService, private exportFavoritesProcessorService: ExportFavoritesProcessorService, private exportFollowingProcessorService: ExportFollowingProcessorService, private exportMutingProcessorService: ExportMutingProcessorService, @@ -120,402 +107,219 @@ export class QueueProcessorService implements OnApplicationShutdown { private cleanChartsProcessorService: CleanChartsProcessorService, private aggregateRetentionProcessorService: AggregateRetentionProcessorService, private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService, - private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService, - private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService, private cleanProcessorService: CleanProcessorService, ) { this.logger = this.queueLoggerService.logger; - function renderError(e?: Error) { - // 何故かeがundefinedで来ることがある - if (!e) return '?'; - - if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError') { - return `${e.name}: ${e.message}`; + function renderError(e: Error): any { + if (e) { // 何故かeがundefinedで来ることがある + return { + stack: e.stack, + message: e.message, + name: e.name, + }; + } else { + return { + stack: '?', + message: '?', + name: '?', + }; } - - return { - stack: e.stack, - message: e.message, - name: e.name, - }; - } - - function renderJob(job?: Bull.Job) { - if (!job) return '?'; - - return { - name: job.name || undefined, - info: getJobInfo(job), - failedReason: job.failedReason || undefined, - data: job.data, - }; } //#region system - { - const processer = (job: Bull.Job) => { - switch (job.name) { - case 'tickCharts': return this.tickChartsProcessorService.process(); - case 'resyncCharts': return this.resyncChartsProcessorService.process(); - case 'cleanCharts': return this.cleanChartsProcessorService.process(); - case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); - case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); - case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process(); - case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process(); - case 'clean': return this.cleanProcessorService.process(); - default: throw new Error(`unrecognized job type ${job.name} for system`); - } - }; + this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => { + switch (job.name) { + case 'tickCharts': return this.tickChartsProcessorService.process(); + case 'resyncCharts': return this.resyncChartsProcessorService.process(); + case 'cleanCharts': return this.cleanChartsProcessorService.process(); + case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); + case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); + case 'clean': return this.cleanProcessorService.process(); + default: throw new Error(`unrecognized job type ${job.name} for system`); + } + }, { + ...baseQueueOptions(this.config, QUEUE.SYSTEM), + autorun: false, + }); - this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job)); - } else { - return processer(job); - } - }, { - ...baseWorkerOptions(this.config, QUEUE.SYSTEM), - autorun: false, - }); + const systemLogger = this.logger.createSubLogger('system'); - const logger = this.logger.createSubLogger('system'); - - this.systemQueueWorker - .on('active', (job) => logger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err: Error) => { - logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } + this.systemQueueWorker + .on('active', (job) => systemLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => systemLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`)); //#endregion //#region db - { - const processer = (job: Bull.Job) => { - switch (job.name) { - case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job); - case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job); - case 'exportNotes': return this.exportNotesProcessorService.process(job); - case 'exportClips': return this.exportClipsProcessorService.process(job); - case 'exportFavorites': return this.exportFavoritesProcessorService.process(job); - case 'exportFollowing': return this.exportFollowingProcessorService.process(job); - case 'exportMuting': return this.exportMutingProcessorService.process(job); - case 'exportBlocking': return this.exportBlockingProcessorService.process(job); - case 'exportUserLists': return this.exportUserListsProcessorService.process(job); - case 'exportAntennas': return this.exportAntennasProcessorService.process(job); - case 'importFollowing': return this.importFollowingProcessorService.process(job); - case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job); - case 'importMuting': return this.importMutingProcessorService.process(job); - case 'importBlocking': return this.importBlockingProcessorService.process(job); - case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job); - case 'importUserLists': return this.importUserListsProcessorService.process(job); - case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job); - case 'importAntennas': return this.importAntennasProcessorService.process(job); - case 'deleteAccount': return this.deleteAccountProcessorService.process(job); - default: throw new Error(`unrecognized job type ${job.name} for db`); - } - }; + this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => { + switch (job.name) { + case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job); + case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job); + case 'exportNotes': return this.exportNotesProcessorService.process(job); + case 'exportFavorites': return this.exportFavoritesProcessorService.process(job); + case 'exportFollowing': return this.exportFollowingProcessorService.process(job); + case 'exportMuting': return this.exportMutingProcessorService.process(job); + case 'exportBlocking': return this.exportBlockingProcessorService.process(job); + case 'exportUserLists': return this.exportUserListsProcessorService.process(job); + case 'exportAntennas': return this.exportAntennasProcessorService.process(job); + case 'importFollowing': return this.importFollowingProcessorService.process(job); + case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job); + case 'importMuting': return this.importMutingProcessorService.process(job); + case 'importBlocking': return this.importBlockingProcessorService.process(job); + case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job); + case 'importUserLists': return this.importUserListsProcessorService.process(job); + case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job); + case 'importAntennas': return this.importAntennasProcessorService.process(job); + case 'deleteAccount': return this.deleteAccountProcessorService.process(job); + default: throw new Error(`unrecognized job type ${job.name} for db`); + } + }, { + ...baseQueueOptions(this.config, QUEUE.DB), + autorun: false, + }); - this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job)); - } else { - return processer(job); - } - }, { - ...baseWorkerOptions(this.config, QUEUE.DB), - autorun: false, - }); + const dbLogger = this.logger.createSubLogger('db'); - const logger = this.logger.createSubLogger('db'); - - this.dbQueueWorker - .on('active', (job) => logger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => { - logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } + this.dbQueueWorker + .on('active', (job) => dbLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => dbLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`)); //#endregion //#region deliver - { - this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job)); - } else { - return this.deliverProcessorService.process(job); - } - }, { - ...baseWorkerOptions(this.config, QUEUE.DELIVER), - autorun: false, - concurrency: this.config.deliverJobConcurrency ?? 128, - limiter: { - max: this.config.deliverJobPerSec ?? 128, - duration: 1000, - }, - settings: { - backoffStrategy: httpRelatedBackoff, - }, - }); + this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.DELIVER), + autorun: false, + concurrency: this.config.deliverJobConcurrency ?? 128, + limiter: { + max: this.config.deliverJobPerSec ?? 128, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, + }); - const logger = this.logger.createSubLogger('deliver'); + const deliverLogger = this.logger.createSubLogger('deliver'); - this.deliverQueueWorker - .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => { - logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } + this.deliverQueueWorker + .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) + .on('error', (err: Error) => deliverLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`)); //#endregion //#region inbox - { - this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job)); - } else { - return this.inboxProcessorService.process(job); - } - }, { - ...baseWorkerOptions(this.config, QUEUE.INBOX), - autorun: false, - concurrency: this.config.inboxJobConcurrency ?? 16, - limiter: { - max: this.config.inboxJobPerSec ?? 32, - duration: 1000, - }, - settings: { - backoffStrategy: httpRelatedBackoff, - }, - }); + this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.INBOX), + autorun: false, + concurrency: this.config.inboxJobConcurrency ?? 16, + limiter: { + max: this.config.inboxJobPerSec ?? 16, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, + }); - const logger = this.logger.createSubLogger('inbox'); + const inboxLogger = this.logger.createSubLogger('inbox'); - this.inboxQueueWorker - .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) - .on('failed', (job, err) => { - logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) }); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } + this.inboxQueueWorker + .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) + .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) + .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => inboxLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`)); //#endregion - //#region user-webhook deliver - { - this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job)); - } else { - return this.userWebhookDeliverProcessorService.process(job); - } - }, { - ...baseWorkerOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER), - autorun: false, - concurrency: 64, - limiter: { - max: 64, - duration: 1000, - }, - settings: { - backoffStrategy: httpRelatedBackoff, - }, - }); + //#region webhook deliver + this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => this.webhookDeliverProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER), + autorun: false, + concurrency: 64, + limiter: { + max: 64, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, + }); - const logger = this.logger.createSubLogger('user-webhook'); + const webhookLogger = this.logger.createSubLogger('webhook'); - this.userWebhookDeliverQueueWorker - .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => { - logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } - //#endregion - - //#region system-webhook deliver - { - this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: SystemWebhookDeliver' }, () => this.systemWebhookDeliverProcessorService.process(job)); - } else { - return this.systemWebhookDeliverProcessorService.process(job); - } - }, { - ...baseWorkerOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER), - autorun: false, - concurrency: 16, - limiter: { - max: 16, - duration: 1000, - }, - settings: { - backoffStrategy: httpRelatedBackoff, - }, - }); - - const logger = this.logger.createSubLogger('system-webhook'); - - this.systemWebhookDeliverQueueWorker - .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => { - logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } + this.webhookDeliverQueueWorker + .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) + .on('error', (err: Error) => webhookLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`)); //#endregion //#region relationship - { - const processer = (job: Bull.Job) => { - switch (job.name) { - case 'follow': return this.relationshipProcessorService.processFollow(job); - case 'unfollow': return this.relationshipProcessorService.processUnfollow(job); - case 'block': return this.relationshipProcessorService.processBlock(job); - case 'unblock': return this.relationshipProcessorService.processUnblock(job); - default: throw new Error(`unrecognized job type ${job.name} for relationship`); - } - }; + this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => { + switch (job.name) { + case 'follow': return this.relationshipProcessorService.processFollow(job); + case 'unfollow': return this.relationshipProcessorService.processUnfollow(job); + case 'block': return this.relationshipProcessorService.processBlock(job); + case 'unblock': return this.relationshipProcessorService.processUnblock(job); + default: throw new Error(`unrecognized job type ${job.name} for relationship`); + } + }, { + ...baseQueueOptions(this.config, QUEUE.RELATIONSHIP), + autorun: false, + concurrency: this.config.relashionshipJobConcurrency ?? 16, + limiter: { + max: this.config.relashionshipJobPerSec ?? 64, + duration: 1000, + }, + }); - this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job)); - } else { - return processer(job); - } - }, { - ...baseWorkerOptions(this.config, QUEUE.RELATIONSHIP), - autorun: false, - concurrency: this.config.relationshipJobConcurrency ?? 16, - limiter: { - max: this.config.relationshipJobPerSec ?? 64, - duration: 1000, - }, - }); - - const logger = this.logger.createSubLogger('relationship'); - - this.relationshipQueueWorker - .on('active', (job) => logger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => { - logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } + const relationshipLogger = this.logger.createSubLogger('relationship'); + + this.relationshipQueueWorker + .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => relationshipLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`)); //#endregion //#region object storage - { - const processer = (job: Bull.Job) => { - switch (job.name) { - case 'deleteFile': return this.deleteFileProcessorService.process(job); - case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job); - default: throw new Error(`unrecognized job type ${job.name} for objectStorage`); - } - }; + this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => { + switch (job.name) { + case 'deleteFile': return this.deleteFileProcessorService.process(job); + case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job); + default: throw new Error(`unrecognized job type ${job.name} for objectStorage`); + } + }, { + ...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE), + autorun: false, + concurrency: 16, + }); - this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job)); - } else { - return processer(job); - } - }, { - ...baseWorkerOptions(this.config, QUEUE.OBJECT_STORAGE), - autorun: false, - concurrency: 16, - }); + const objectStorageLogger = this.logger.createSubLogger('objectStorage'); - const logger = this.logger.createSubLogger('objectStorage'); - - this.objectStorageQueueWorker - .on('active', (job) => logger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => { - logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } + this.objectStorageQueueWorker + .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => objectStorageLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`)); //#endregion //#region ended poll notification - { - this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job)); - } else { - return this.endedPollNotificationProcessorService.process(job); - } - }, { - ...baseWorkerOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION), - autorun: false, - }); - } + this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => this.endedPollNotificationProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION), + autorun: false, + }); //#endregion } @@ -526,8 +330,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.dbQueueWorker.run(), this.deliverQueueWorker.run(), this.inboxQueueWorker.run(), - this.userWebhookDeliverQueueWorker.run(), - this.systemWebhookDeliverQueueWorker.run(), + this.webhookDeliverQueueWorker.run(), this.relationshipQueueWorker.run(), this.objectStorageQueueWorker.run(), this.endedPollNotificationQueueWorker.run(), @@ -541,8 +344,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.dbQueueWorker.close(), this.deliverQueueWorker.close(), this.inboxQueueWorker.close(), - this.userWebhookDeliverQueueWorker.close(), - this.systemWebhookDeliverQueueWorker.close(), + this.webhookDeliverQueueWorker.close(), this.relationshipQueueWorker.close(), this.objectStorageQueueWorker.close(), this.endedPollNotificationQueueWorker.close(), diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts index 7e146a7e03..d240fe70e0 100644 --- a/packages/backend/src/queue/const.ts +++ b/packages/backend/src/queue/const.ts @@ -1,9 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { MetricsTime } from 'bullmq'; import { Config } from '@/config.js'; import type * as Bull from 'bullmq'; @@ -15,25 +9,18 @@ export const QUEUE = { DB: 'db', RELATIONSHIP: 'relationship', OBJECT_STORAGE: 'objectStorage', - USER_WEBHOOK_DELIVER: 'userWebhookDeliver', - SYSTEM_WEBHOOK_DELIVER: 'systemWebhookDeliver', + WEBHOOK_DELIVER: 'webhookDeliver', }; export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions { return { connection: { - ...config.redisForJobQueue, - keyPrefix: undefined, + port: config.redisForJobQueue.port, + host: config.redisForJobQueue.host, + family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family, + password: config.redisForJobQueue.pass, + db: config.redisForJobQueue.db ?? 0, }, prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue:${queueName}` : `queue:${queueName}`, }; } - -export function baseWorkerOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.WorkerOptions { - return { - ...baseQueueOptions(config, queueName), - metrics: { - maxDataPoints: MetricsTime.ONE_WEEK, - }, - }; -} diff --git a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts index 4769cccabf..600ce0828f 100644 --- a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts +++ b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts @@ -1,14 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import type { RetentionAggregationsRepository, UsersRepository } from '@/models/_.js'; +import type { RetentionAggregationsRepository, UsersRepository } from '@/models/index.js'; import { deepClone } from '@/misc/clone.js'; import { IdService } from '@/core/IdService.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; @@ -20,6 +16,9 @@ export class AggregateRetentionProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -47,13 +46,13 @@ export class AggregateRetentionProcessorService { // 今日登録したユーザーを全て取得 const targetUsers = await this.usersRepository.findBy({ host: IsNull(), - id: MoreThan(this.idService.gen(Date.now() - (1000 * 60 * 60 * 24))), + createdAt: MoreThan(new Date(Date.now() - (1000 * 60 * 60 * 24))), }); const targetUserIds = targetUsers.map(u => u.id); try { await this.retentionAggregationsRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), createdAt: now, updatedAt: now, dateKey, diff --git a/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts deleted file mode 100644 index d49c99f694..0000000000 --- a/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import type Logger from '@/logger.js'; -import { bindThis } from '@/decorators.js'; -import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type * as Bull from 'bullmq'; -import { MiMeta } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; - -@Injectable() -export class BakeBufferedReactionsProcessorService { - private logger: Logger; - - constructor( - @Inject(DI.meta) - private meta: MiMeta, - - private reactionsBufferingService: ReactionsBufferingService, - private queueLoggerService: QueueLoggerService, - ) { - this.logger = this.queueLoggerService.logger.createSubLogger('bake-buffered-reactions'); - } - - @bindThis - public async process(): Promise { - if (!this.meta.enableReactionsBuffering) { - this.logger.info('Reactions buffering is disabled. Skipping...'); - return; - } - - this.logger.info('Baking buffered reactions...'); - - await this.reactionsBufferingService.bake(); - - this.logger.succ('All buffered reactions baked.'); - } -} diff --git a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts index 448fc9c763..c4ee212bab 100644 --- a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts @@ -1,12 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { MutingsRepository } from '@/models/_.js'; +import type { MutingsRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { UserMutingService } from '@/core/UserMutingService.js'; @@ -18,6 +14,9 @@ export class CheckExpiredMutingsProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts deleted file mode 100644 index c9fe4fca73..0000000000 --- a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts +++ /dev/null @@ -1,282 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; -import type Logger from '@/logger.js'; -import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { EmailService } from '@/core/EmailService.js'; -import { MiUser, type UserProfilesRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { AnnouncementService } from '@/core/AnnouncementService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; - -// モデレーターが不在と判断する日付の閾値 -const MODERATOR_INACTIVITY_LIMIT_DAYS = 7; -// 警告通知やログ出力を行う残日数の閾値 -const MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS = 2; -// 期限から6時間ごとに通知を行う -const MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS = 6; -const ONE_HOUR_MILLI_SEC = 1000 * 60 * 60; -const ONE_DAY_MILLI_SEC = ONE_HOUR_MILLI_SEC * 24; - -export type ModeratorInactivityEvaluationResult = { - isModeratorsInactive: boolean; - inactiveModerators: MiUser[]; - remainingTime: ModeratorInactivityRemainingTime; -}; - -export type ModeratorInactivityRemainingTime = { - time: number; - asHours: number; - asDays: number; -}; - -function generateModeratorInactivityMail(remainingTime: ModeratorInactivityRemainingTime) { - const subject = 'Moderator Inactivity Warning / モデレーター不在の通知'; - - const timeVariant = remainingTime.asDays === 0 ? `${remainingTime.asHours} hours` : `${remainingTime.asDays} days`; - const timeVariantJa = remainingTime.asDays === 0 ? `${remainingTime.asHours} 時間` : `${remainingTime.asDays} 日間`; - const message = [ - 'To Moderators,', - '', - `A moderator has been inactive for a period of time. If there are ${timeVariant} of inactivity left, it will switch to invitation only.`, - 'If you do not wish to move to invitation only, you must log into Misskey and update your last active date and time.', - '', - '---------------', - '', - 'To モデレーター各位', - '', - `モデレーターが一定期間活動していないようです。あと${timeVariantJa}活動していない状態が続くと招待制に切り替わります。`, - '招待制に切り替わることを望まない場合は、Misskeyにログインして最終アクティブ日時を更新してください。', - '', - ]; - - const html = message.join('
'); - const text = message.join('\n'); - - return { - subject, - html, - text, - }; -} - -function generateInvitationOnlyChangedMail() { - const subject = 'Change to Invitation-Only / 招待制に変更されました'; - - const message = [ - 'To Moderators,', - '', - `Changed to invitation only because no moderator activity was detected for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days.`, - 'To cancel the invitation only, you need to access the control panel.', - '', - '---------------', - '', - 'To モデレーター各位', - '', - `モデレーターの活動が${MODERATOR_INACTIVITY_LIMIT_DAYS}日間検出されなかったため、招待制に変更されました。`, - '招待制を解除するには、コントロールパネルにアクセスする必要があります。', - '', - ]; - - const html = message.join('
'); - const text = message.join('\n'); - - return { - subject, - html, - text, - }; -} - -@Injectable() -export class CheckModeratorsActivityProcessorService { - private logger: Logger; - - constructor( - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - private metaService: MetaService, - private roleService: RoleService, - private emailService: EmailService, - private announcementService: AnnouncementService, - private systemWebhookService: SystemWebhookService, - private queueLoggerService: QueueLoggerService, - ) { - this.logger = this.queueLoggerService.logger.createSubLogger('check-moderators-activity'); - } - - @bindThis - public async process(): Promise { - this.logger.info('start.'); - - const meta = await this.metaService.fetch(false); - if (!meta.disableRegistration) { - await this.processImpl(); - } else { - this.logger.info('is already invitation only.'); - } - - this.logger.succ('finish.'); - } - - @bindThis - private async processImpl() { - const evaluateResult = await this.evaluateModeratorsInactiveDays(); - if (evaluateResult.isModeratorsInactive) { - this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will move to invitation only.`); - - await this.changeToInvitationOnly(); - await this.notifyChangeToInvitationOnly(); - } else { - const remainingTime = evaluateResult.remainingTime; - if (remainingTime.asDays <= MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS) { - const timeVariant = remainingTime.asDays === 0 ? `${remainingTime.asHours} hours` : `${remainingTime.asDays} days`; - this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${timeVariant}, it will switch to invitation only.`); - - if (remainingTime.asHours % MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS === 0) { - // ジョブの実行頻度と同等だと通知が多すぎるため期限から6時間ごとに通知する - // つまり、のこり2日を切ったら6時間ごとに通知が送られる - await this.notifyInactiveModeratorsWarning(remainingTime); - } - } - } - } - - /** - * モデレーターが不在であるかどうかを確認する。trueの場合はモデレーターが不在である。 - * isModerator, isAdministrator, isRootのいずれかがtrueのユーザを対象に、 - * {@link MiUser.lastActiveDate}の値が実行日時の{@link MODERATOR_INACTIVITY_LIMIT_DAYS}日前よりも古いユーザがいるかどうかを確認する。 - * {@link MiUser.lastActiveDate}がnullの場合は、そのユーザは確認の対象外とする。 - * - * ----- - * - * ### サンプルパターン - * - 実行日時: 2022-01-30 12:00:00 - * - 判定基準: 2022-01-23 12:00:00(実行日時の{@link MODERATOR_INACTIVITY_LIMIT_DAYS}日前) - * - * #### パターン① - * - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト - * - モデレータB: lastActiveDate = 2022-01-23 12:00:00 ※セーフ(判定基準と同値なのでギリギリ残り0日) - * - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日) - * - モデレータD: lastActiveDate = null - * - * この場合、モデレータBのアクティビティのみ判定基準日よりも古くないため、モデレーターが在席と判断される。 - * - * #### パターン② - * - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト - * - モデレータB: lastActiveDate = 2022-01-22 12:00:00 ※アウト(残り-1日) - * - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日) - * - モデレータD: lastActiveDate = null - * - * この場合、モデレータA, B, Cのアクティビティは判定基準日よりも古いため、モデレーターが不在と判断される。 - */ - @bindThis - public async evaluateModeratorsInactiveDays(): Promise { - const today = new Date(); - const inactivePeriod = new Date(today); - inactivePeriod.setDate(today.getDate() - MODERATOR_INACTIVITY_LIMIT_DAYS); - - const moderators = await this.fetchModerators() - .then(it => it.filter(it => it.lastActiveDate != null)); - const inactiveModerators = moderators - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .filter(it => it.lastActiveDate!.getTime() < inactivePeriod.getTime()); - - // 残りの猶予を示したいので、最終アクティブ日時が一番若いモデレータの日数を基準に猶予を計算する - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const newestLastActiveDate = new Date(Math.max(...moderators.map(it => it.lastActiveDate!.getTime()))); - const remainingTime = newestLastActiveDate.getTime() - inactivePeriod.getTime(); - const remainingTimeAsDays = Math.floor(remainingTime / ONE_DAY_MILLI_SEC); - const remainingTimeAsHours = Math.floor((remainingTime / ONE_HOUR_MILLI_SEC)); - - return { - isModeratorsInactive: inactiveModerators.length === moderators.length, - inactiveModerators, - remainingTime: { - time: remainingTime, - asHours: remainingTimeAsHours, - asDays: remainingTimeAsDays, - }, - }; - } - - @bindThis - private async changeToInvitationOnly() { - await this.metaService.update({ disableRegistration: true }); - } - - @bindThis - public async notifyInactiveModeratorsWarning(remainingTime: ModeratorInactivityRemainingTime) { - // -- モデレータへのメール送信 - - const moderators = await this.fetchModerators(); - const moderatorProfiles = await this.userProfilesRepository - .findBy({ userId: In(moderators.map(it => it.id)) }) - .then(it => new Map(it.map(it => [it.userId, it]))); - - const mail = generateModeratorInactivityMail(remainingTime); - for (const moderator of moderators) { - const profile = moderatorProfiles.get(moderator.id); - if (profile && profile.email && profile.emailVerified) { - this.emailService.sendEmail(profile.email, mail.subject, mail.html, mail.text); - } - } - - // -- SystemWebhook - - return this.systemWebhookService.enqueueSystemWebhook( - 'inactiveModeratorsWarning', - { remainingTime: remainingTime }, - ); - } - - @bindThis - public async notifyChangeToInvitationOnly() { - // -- モデレータへのメールとお知らせ(個人向け)送信 - - const moderators = await this.fetchModerators(); - const moderatorProfiles = await this.userProfilesRepository - .findBy({ userId: In(moderators.map(it => it.id)) }) - .then(it => new Map(it.map(it => [it.userId, it]))); - - const mail = generateInvitationOnlyChangedMail(); - for (const moderator of moderators) { - this.announcementService.create({ - title: mail.subject, - text: mail.text, - forExistingUsers: true, - needConfirmationToRead: true, - userId: moderator.id, - }); - - const profile = moderatorProfiles.get(moderator.id); - if (profile && profile.email && profile.emailVerified) { - this.emailService.sendEmail(profile.email, mail.subject, mail.html, mail.text); - } - } - - // -- SystemWebhook - - return this.systemWebhookService.enqueueSystemWebhook( - 'inactiveModeratorsInvitationOnlyChanged', - {}, - ); - } - - @bindThis - private async fetchModerators() { - // TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する - return this.roleService.getModerators({ - includeAdmins: true, - includeRoot: true, - excludeExpire: true, - }); - } -} diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts index 8c5faa8d07..22d7c1b4fb 100644 --- a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts @@ -1,9 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import FederationChart from '@/core/chart/charts/federation.js'; import NotesChart from '@/core/chart/charts/notes.js'; @@ -26,6 +23,9 @@ export class CleanChartsProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + private federationChart: FederationChart, private notesChart: NotesChart, private usersChart: UsersChart, @@ -48,19 +48,20 @@ export class CleanChartsProcessorService { public async process(): Promise { this.logger.info('Clean charts...'); - // DBへの同時接続を避けるためにPromise.allを使わずひとつずつ実行する - await this.federationChart.clean(); - await this.notesChart.clean(); - await this.usersChart.clean(); - await this.activeUsersChart.clean(); - await this.instanceChart.clean(); - await this.perUserNotesChart.clean(); - await this.perUserPvChart.clean(); - await this.driveChart.clean(); - await this.perUserReactionsChart.clean(); - await this.perUserFollowingChart.clean(); - await this.perUserDriveChart.clean(); - await this.apRequestChart.clean(); + await Promise.all([ + this.federationChart.clean(), + this.notesChart.clean(), + this.usersChart.clean(), + this.activeUsersChart.clean(), + this.instanceChart.clean(), + this.perUserNotesChart.clean(), + this.perUserPvChart.clean(), + this.driveChart.clean(), + this.perUserReactionsChart.clean(), + this.perUserFollowingChart.clean(), + this.perUserDriveChart.clean(), + this.apRequestChart.clean(), + ]); this.logger.succ('All charts successfully cleaned.'); } diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index a26b69cd2b..cefa6da5e9 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -1,17 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { In, LessThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AntennasRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/_.js'; +import type { AntennasRepository, MutedNotesRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; -import type { Config } from '@/config.js'; -import { ReversiService } from '@/core/ReversiService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -26,6 +20,9 @@ export class CleanProcessorService { @Inject(DI.userIpsRepository) private userIpsRepository: UserIpsRepository, + @Inject(DI.mutedNotesRepository) + private mutedNotesRepository: MutedNotesRepository, + @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, @@ -33,7 +30,6 @@ export class CleanProcessorService { private roleAssignmentsRepository: RoleAssignmentsRepository, private queueLoggerService: QueueLoggerService, - private reversiService: ReversiService, private idService: IdService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('clean'); @@ -47,14 +43,22 @@ export class CleanProcessorService { createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))), }); - // 使われてないアンテナを停止 - if (this.config.deactivateAntennaThreshold > 0) { - this.antennasRepository.update({ - lastUsedAt: LessThan(new Date(Date.now() - this.config.deactivateAntennaThreshold)), - }, { - isActive: false, - }); - } + this.mutedNotesRepository.delete({ + id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))), + reason: 'word', + }); + + this.mutedNotesRepository.delete({ + id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))), + reason: 'word', + }); + + // 7日以上使われてないアンテナを停止 + this.antennasRepository.update({ + lastUsedAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 7))), + }, { + isActive: false, + }); const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign') .where('assign.expiresAt IS NOT NULL') @@ -67,8 +71,6 @@ export class CleanProcessorService { }); } - this.reversiService.cleanOutdatedGames(); - this.logger.succ('Cleaned.'); } } diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts index 728fc9e72b..c54bf59ae4 100644 --- a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts @@ -1,12 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { bindThis } from '@/decorators.js'; @@ -18,6 +14,9 @@ export class CleanRemoteFilesProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -32,7 +31,7 @@ export class CleanRemoteFilesProcessorService { this.logger.info('Deleting cached remote files...'); let deletedCount = 0; - let cursor: MiDriveFile['id'] | null = null; + let cursor: any = null; while (true) { const files = await this.driveFilesRepository.find({ @@ -52,7 +51,7 @@ export class CleanRemoteFilesProcessorService { break; } - cursor = files.at(-1)?.id ?? null; + cursor = files[files.length - 1].id; await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true))); @@ -63,7 +62,7 @@ export class CleanRemoteFilesProcessorService { isLink: false, }); - job.updateProgress(100 / total * deletedCount); + job.updateProgress(deletedCount / total); } this.logger.succ('All cached remote files has been deleted.'); diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index 14a53e0c42..39dd801af0 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -1,19 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiNote } from '@/models/Note.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { Note } from '@/models/entities/Note.js'; import { EmailService } from '@/core/EmailService.js'; import { bindThis } from '@/decorators.js'; -import { SearchService } from '@/core/SearchService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbUserDeleteJobData } from '../types.js'; @@ -23,6 +18,9 @@ export class DeleteAccountProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -38,7 +36,6 @@ export class DeleteAccountProcessorService { private driveService: DriveService, private emailService: EmailService, private queueLoggerService: QueueLoggerService, - private searchService: SearchService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('delete-account'); } @@ -53,7 +50,7 @@ export class DeleteAccountProcessorService { } { // Delete notes - let cursor: MiNote['id'] | null = null; + let cursor: Note['id'] | null = null; while (true) { const notes = await this.notesRepository.find({ @@ -65,26 +62,22 @@ export class DeleteAccountProcessorService { order: { id: 1, }, - }) as MiNote[]; + }) as Note[]; if (notes.length === 0) { break; } - cursor = notes.at(-1)?.id ?? null; + cursor = notes[notes.length - 1].id; await this.notesRepository.delete(notes.map(note => note.id)); - - for (const note of notes) { - await this.searchService.unindexNote(note); - } } this.logger.succ('All of notes deleted'); } { // Delete files - let cursor: MiDriveFile['id'] | null = null; + let cursor: DriveFile['id'] | null = null; while (true) { const files = await this.driveFilesRepository.find({ @@ -96,13 +89,13 @@ export class DeleteAccountProcessorService { order: { id: 1, }, - }) as MiDriveFile[]; + }) as DriveFile[]; if (files.length === 0) { break; } - cursor = files.at(-1)?.id ?? null; + cursor = files[files.length - 1].id; for (const file of files) { await this.driveService.deleteFileSync(file); diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts index 291fa4a6d8..6772c5dc76 100644 --- a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts @@ -1,12 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, DriveFilesRepository, MiDriveFile } from '@/models/_.js'; +import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { bindThis } from '@/decorators.js'; @@ -19,6 +15,9 @@ export class DeleteDriveFilesProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -41,7 +40,7 @@ export class DeleteDriveFilesProcessorService { } let deletedCount = 0; - let cursor: MiDriveFile['id'] | null = null; + let cursor: any = null; while (true) { const files = await this.driveFilesRepository.find({ @@ -60,7 +59,7 @@ export class DeleteDriveFilesProcessorService { break; } - cursor = files.at(-1)?.id ?? null; + cursor = files[files.length - 1].id; for (const file of files) { await this.driveService.deleteFileSync(file); diff --git a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts index fc1dd93ce7..edf87bd921 100644 --- a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts @@ -1,9 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { bindThis } from '@/decorators.js'; @@ -16,6 +13,9 @@ export class DeleteFileProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + private driveService: DriveService, private queueLoggerService: QueueLoggerService, ) { diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 391ccdac05..406e9df850 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -1,19 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import * as Bull from 'bullmq'; -import { Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { InstancesRepository, MiMeta } from '@/models/_.js'; +import type { DriveFilesRepository, InstancesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; +import { MetaService } from '@/core/MetaService.js'; import { ApRequestService } from '@/core/activitypub/ApRequestService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; import { MemorySingleCache } from '@/misc/cache.js'; -import type { MiInstance } from '@/models/Instance.js'; +import type { Instance } from '@/models/entities/Instance.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import FederationChart from '@/core/chart/charts/federation.js'; @@ -26,16 +22,20 @@ import type { DeliverJobData } from '../types.js'; @Injectable() export class DeliverProcessorService { private logger: Logger; - private suspendedHostsCache: MemorySingleCache; + private suspendedHostsCache: MemorySingleCache; private latest: string | null; constructor( - @Inject(DI.meta) - private meta: MiMeta, + @Inject(DI.config) + private config: Config, @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private metaService: MetaService, private utilityService: UtilityService, private federatedInstanceService: FederatedInstanceService, private fetchInstanceMetadataService: FetchInstanceMetadataService, @@ -46,14 +46,16 @@ export class DeliverProcessorService { private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('deliver'); - this.suspendedHostsCache = new MemorySingleCache(1000 * 60 * 60); // 1h + this.suspendedHostsCache = new MemorySingleCache(1000 * 60 * 60); } @bindThis public async process(job: Bull.Job): Promise { const { host } = new URL(job.data.to); - if (!this.utilityService.isFederationAllowedUri(job.data.to)) { + // ブロックしてたら中断 + const meta = await this.metaService.fetch(); + if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.toPuny(host))) { return 'skip (blocked)'; } @@ -62,7 +64,7 @@ export class DeliverProcessorService { if (suspendedHosts == null) { suspendedHosts = await this.instancesRepository.find({ where: { - suspensionState: Not('none'), + isSuspended: true, }, }); this.suspendedHostsCache.set(suspendedHosts); @@ -71,81 +73,52 @@ export class DeliverProcessorService { return 'skip (suspended)'; } - const i = await (this.meta.enableStatsForFederatedInstances - ? this.federatedInstanceService.fetchOrRegister(host) - : this.federatedInstanceService.fetch(host)); - - // suspend server by software - if (i != null && this.utilityService.isDeliverSuspendedSoftware(i)) { - return 'skip (software suspended)'; - } - try { - await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest); - - this.apRequestChart.deliverSucc(); - this.federationChart.deliverd(host, true); - - // Update instance stats - process.nextTick(async () => { - if (i == null) return; + await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content); + // Update stats + this.federatedInstanceService.fetch(host).then(i => { if (i.isNotResponding) { this.federatedInstanceService.update(i.id, { isNotResponding: false, - notRespondingSince: null, }); } - if (this.meta.enableStatsForFederatedInstances) { - this.fetchInstanceMetadataService.fetchInstanceMetadata(i); - } + this.fetchInstanceMetadataService.fetchInstanceMetadata(i); + this.apRequestChart.deliverSucc(); + this.federationChart.deliverd(i.host, true); - if (this.meta.enableChartsForFederatedInstances) { + if (meta.enableChartsForFederatedInstances) { this.instanceChart.requestSent(i.host, true); } }); return 'Success'; } catch (res) { - this.apRequestChart.deliverFail(); - this.federationChart.deliverd(host, false); - - // Update instance stats - this.federatedInstanceService.fetchOrRegister(host).then(i => { + // Update stats + this.federatedInstanceService.fetch(host).then(i => { if (!i.isNotResponding) { this.federatedInstanceService.update(i.id, { isNotResponding: true, - notRespondingSince: new Date(), - }); - } else if (i.notRespondingSince) { - // 1週間以上不通ならサスペンド - if (i.suspensionState === 'none' && i.notRespondingSince.getTime() <= Date.now() - 1000 * 60 * 60 * 24 * 7) { - this.federatedInstanceService.update(i.id, { - suspensionState: 'autoSuspendedForNotResponding', - }); - } - } else { - // isNotRespondingがtrueでnotRespondingSinceがnullの場合はnotRespondingSinceをセット - // notRespondingSinceは新たな機能なので、それ以前のデータにはnotRespondingSinceがない場合がある - this.federatedInstanceService.update(i.id, { - notRespondingSince: new Date(), }); } - if (this.meta.enableChartsForFederatedInstances) { + this.apRequestChart.deliverFail(); + this.federationChart.deliverd(i.host, false); + + if (meta.enableChartsForFederatedInstances) { this.instanceChart.requestSent(i.host, false); } }); if (res instanceof StatusError) { // 4xx - if (!res.isRetryable) { + if (res.isClientError) { // 相手が閉鎖していることを明示しているため、配送停止する if (job.data.isSharedInbox && res.statusCode === 410) { - this.federatedInstanceService.fetchOrRegister(host).then(i => { + this.federatedInstanceService.fetch(host).then(i => { this.federatedInstanceService.update(i.id, { - suspensionState: 'goneSuspended', + isSuspended: true, }); }); throw new Bull.UnrecoverableError(`${host} is gone`); diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts index 34180e5f2b..21501592f2 100644 --- a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts +++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { PollVotesRepository, NotesRepository } from '@/models/_.js'; +import type { PollVotesRepository, NotesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; -import { CacheService } from '@/core/CacheService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; @@ -19,13 +14,15 @@ export class EndedPollNotificationProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @Inject(DI.pollVotesRepository) private pollVotesRepository: PollVotesRepository, - private cacheService: CacheService, private notificationService: NotificationService, private queueLoggerService: QueueLoggerService, ) { @@ -49,12 +46,9 @@ export class EndedPollNotificationProcessorService { const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])]; for (const userId of userIds) { - const profile = await this.cacheService.userProfileCache.fetch(userId); - if (profile.userHost === null) { - this.notificationService.createNotification(userId, 'pollEnded', { - noteId: note.id, - }); - } + this.notificationService.createNotification(userId, 'pollEnded', { + noteId: note.id, + }); } } } diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts index 053ba99005..ac52325c8d 100644 --- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts @@ -1,21 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { format as DateFormat } from 'date-fns'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AntennasRepository, UsersRepository, UserListMembershipsRepository, MiUser } from '@/models/_.js'; +import type { AntennasRepository, UsersRepository, UserListJoiningsRepository, User } from '@/models/index.js'; +import type { Config } from '@/config.js'; import Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { bindThis } from '@/decorators.js'; import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { NotificationService } from '@/core/NotificationService.js'; -import { ExportedAntenna } from '@/queue/processors/ImportAntennasProcessorService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type { DBExportAntennasData } from '../types.js'; import type * as Bull from 'bullmq'; @@ -25,19 +19,21 @@ export class ExportAntennasProcessorService { private logger: Logger; constructor ( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.antennasRepository) private antennsRepository: AntennasRepository, - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, - + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + private driveService: DriveService, private utilityService: UtilityService, private queueLoggerService: QueueLoggerService, - private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-antennas'); } @@ -66,11 +62,11 @@ export class ExportAntennasProcessorService { const antennas = await this.antennsRepository.findBy({ userId: job.data.user.id }); write('['); for (const [index, antenna] of antennas.entries()) { - let users: MiUser[] | undefined; + let users: User[] | undefined; if (antenna.userListId !== null) { - const memberships = await this.userListMembershipsRepository.findBy({ userListId: antenna.userListId }); + const joinings = await this.userListJoiningsRepository.findBy({ userListId: antenna.userListId }); users = await this.usersRepository.findBy({ - id: In(memberships.map(j => j.userId)), + id: In(joinings.map(j => j.userId)), }); } write(JSON.stringify({ @@ -83,12 +79,10 @@ export class ExportAntennasProcessorService { return this.utilityService.getFullApAccount(u.username, u.host); // acct }) : null, caseSensitive: antenna.caseSensitive, - localOnly: antenna.localOnly, - excludeBots: antenna.excludeBots, withReplies: antenna.withReplies, withFile: antenna.withFile, - excludeNotesInSensitiveChannel: antenna.excludeNotesInSensitiveChannel, - } satisfies Required)); + notify: antenna.notify, + })); if (antennas.length - 1 !== index) { write(', '); } @@ -99,11 +93,6 @@ export class ExportAntennasProcessorService { const fileName = 'antennas-' + DateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); this.logger.succ('Exported to: ' + driveFile.id); - - this.notificationService.createNotification(user.id, 'exportCompleted', { - exportedEntity: 'antenna', - fileId: driveFile.id, - }); } finally { cleanup(); } diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts index ecc439db69..eb758e162d 100644 --- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts @@ -1,19 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, BlockingsRepository, MiBlocking } from '@/models/_.js'; +import type { UsersRepository, BlockingsRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -24,6 +19,9 @@ export class ExportBlockingProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -31,7 +29,6 @@ export class ExportBlockingProcessorService { private blockingsRepository: BlockingsRepository, private utilityService: UtilityService, - private notificationService: NotificationService, private driveService: DriveService, private queueLoggerService: QueueLoggerService, ) { @@ -56,7 +53,7 @@ export class ExportBlockingProcessorService { const stream = fs.createWriteStream(path, { flags: 'a' }); let exportedCount = 0; - let cursor: MiBlocking['id'] | null = null; + let cursor: any = null; while (true) { const blockings = await this.blockingsRepository.find({ @@ -75,7 +72,7 @@ export class ExportBlockingProcessorService { break; } - cursor = blockings.at(-1)?.id ?? null; + cursor = blockings[blockings.length - 1].id; for (const block of blockings) { const u = await this.usersRepository.findOneBy({ id: block.blockeeId }); @@ -111,11 +108,6 @@ export class ExportBlockingProcessorService { const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); this.logger.succ(`Exported to: ${driveFile.id}`); - - this.notificationService.createNotification(user.id, 'exportCompleted', { - exportedEntity: 'blocking', - fileId: driveFile.id, - }); } finally { cleanup(); } diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts deleted file mode 100644 index 583ddbb745..0000000000 --- a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts +++ /dev/null @@ -1,213 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as fs from 'node:fs'; -import { Writable } from 'node:stream'; -import { Inject, Injectable, StreamableFile } from '@nestjs/common'; -import { MoreThan } from 'typeorm'; -import { format as dateFormat } from 'date-fns'; -import { DI } from '@/di-symbols.js'; -import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js'; -import type Logger from '@/logger.js'; -import { DriveService } from '@/core/DriveService.js'; -import { createTemp } from '@/misc/create-temp.js'; -import type { MiPoll } from '@/models/Poll.js'; -import type { MiNote } from '@/models/Note.js'; -import { bindThis } from '@/decorators.js'; -import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import { Packed } from '@/misc/json-schema.js'; -import { IdService } from '@/core/IdService.js'; -import { NotificationService } from '@/core/NotificationService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type * as Bull from 'bullmq'; -import type { DbJobDataWithUser } from '../types.js'; - -@Injectable() -export class ExportClipsProcessorService { - private logger: Logger; - - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.pollsRepository) - private pollsRepository: PollsRepository, - - @Inject(DI.clipsRepository) - private clipsRepository: ClipsRepository, - - @Inject(DI.clipNotesRepository) - private clipNotesRepository: ClipNotesRepository, - - private driveService: DriveService, - private queueLoggerService: QueueLoggerService, - private idService: IdService, - private notificationService: NotificationService, - ) { - this.logger = this.queueLoggerService.logger.createSubLogger('export-clips'); - } - - @bindThis - public async process(job: Bull.Job): Promise { - this.logger.info(`Exporting clips of ${job.data.user.id} ...`); - - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); - if (user == null) { - return; - } - - // Create temp file - const [path, cleanup] = await createTemp(); - - this.logger.info(`Temp file is ${path}`); - - try { - const stream = Writable.toWeb(fs.createWriteStream(path, { flags: 'a' })); - const writer = stream.getWriter(); - writer.closed.catch(this.logger.error); - - await writer.write('['); - - await this.processClips(writer, user, job); - - await writer.write(']'); - await writer.close(); - - this.logger.succ(`Exported to: ${path}`); - - const fileName = 'clips-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; - const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); - - this.logger.succ(`Exported to: ${driveFile.id}`); - - this.notificationService.createNotification(user.id, 'exportCompleted', { - exportedEntity: 'clip', - fileId: driveFile.id, - }); - } finally { - cleanup(); - } - } - - async processClips(writer: WritableStreamDefaultWriter, user: MiUser, job: Bull.Job) { - let exportedClipsCount = 0; - let cursor: MiClip['id'] | null = null; - - while (true) { - const clips = await this.clipsRepository.find({ - where: { - userId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }); - - if (clips.length === 0) { - job.updateProgress(100); - break; - } - - cursor = clips.at(-1)?.id ?? null; - - for (const clip of clips) { - // Stringify but remove the last `]}` - const content = JSON.stringify(this.serializeClip(clip)).slice(0, -2); - const isFirst = exportedClipsCount === 0; - await writer.write(isFirst ? content : ',\n' + content); - - await this.processClipNotes(writer, clip.id); - - await writer.write(']}'); - exportedClipsCount++; - } - - const total = await this.clipsRepository.countBy({ - userId: user.id, - }); - - job.updateProgress(exportedClipsCount / total); - } - } - - async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise { - let exportedClipNotesCount = 0; - let cursor: MiClipNote['id'] | null = null; - - while (true) { - const clipNotes = await this.clipNotesRepository.find({ - where: { - clipId, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - relations: ['note', 'note.user'], - }) as (MiClipNote & { note: MiNote & { user: MiUser } })[]; - - if (clipNotes.length === 0) { - break; - } - - cursor = clipNotes.at(-1)?.id ?? null; - - for (const clipNote of clipNotes) { - let poll: MiPoll | undefined; - if (clipNote.note.hasPoll) { - poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id }); - } - const content = JSON.stringify(this.serializeClipNote(clipNote, poll)); - const isFirst = exportedClipNotesCount === 0; - await writer.write(isFirst ? content : ',\n' + content); - - exportedClipNotesCount++; - } - } - } - - private serializeClip(clip: MiClip): Record { - return { - id: clip.id, - name: clip.name, - description: clip.description, - lastClippedAt: clip.lastClippedAt?.toISOString(), - clipNotes: [], - }; - } - - private serializeClipNote(clip: MiClipNote & { note: MiNote & { user: MiUser } }, poll: MiPoll | undefined): Record { - return { - id: clip.id, - createdAt: this.idService.parse(clip.id).date.toISOString(), - note: { - id: clip.note.id, - text: clip.note.text, - createdAt: this.idService.parse(clip.note.id).date.toISOString(), - fileIds: clip.note.fileIds, - replyId: clip.note.replyId, - renoteId: clip.note.renoteId, - poll: poll, - cw: clip.note.cw, - visibility: clip.note.visibility, - visibleUserIds: clip.note.visibleUserIds, - localOnly: clip.note.localOnly, - reactionAcceptance: clip.note.reactionAcceptance, - uri: clip.note.uri, - url: clip.note.url, - user: { - id: clip.note.user.id, - name: clip.note.user.name, - username: clip.note.user.username, - host: clip.note.user.host, - uri: clip.note.user.uri, - }, - }, - }; - } -} diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index e237cd4975..3203d9f3e5 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; @@ -10,13 +5,12 @@ import { format as dateFormat } from 'date-fns'; import mime from 'mime-types'; import archiver from 'archiver'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository, UsersRepository } from '@/models/_.js'; +import type { EmojisRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp, createTempDir } from '@/misc/create-temp.js'; import { DownloadService } from '@/core/DownloadService.js'; -import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -38,7 +32,6 @@ export class ExportCustomEmojisProcessorService { private driveService: DriveService, private downloadService: DownloadService, private queueLoggerService: QueueLoggerService, - private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-custom-emojis'); } @@ -136,12 +129,6 @@ export class ExportCustomEmojisProcessorService { const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true }); this.logger.succ(`Exported to: ${driveFile.id}`); - - this.notificationService.createNotification(user.id, 'exportCompleted', { - exportedEntity: 'customEmoji', - fileId: driveFile.id, - }); - cleanup(); archiveCleanup(); resolve(); diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts index b81feece01..76c38a6b86 100644 --- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts @@ -1,22 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import type { MiNoteFavorite, NoteFavoritesRepository, PollsRepository, MiUser, UsersRepository } from '@/models/_.js'; +import type { NoteFavorite, NoteFavoritesRepository, NotesRepository, PollsRepository, User, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; -import type { MiPoll } from '@/models/Poll.js'; -import type { MiNote } from '@/models/Note.js'; +import type { Poll } from '@/models/entities/Poll.js'; +import type { Note } from '@/models/entities/Note.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; -import { NotificationService } from '@/core/NotificationService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; @@ -26,19 +20,23 @@ export class ExportFavoritesProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + @Inject(DI.noteFavoritesRepository) private noteFavoritesRepository: NoteFavoritesRepository, private driveService: DriveService, private queueLoggerService: QueueLoggerService, - private idService: IdService, - private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-favorites'); } @@ -76,7 +74,7 @@ export class ExportFavoritesProcessorService { await write('['); let exportedFavoritesCount = 0; - let cursor: MiNoteFavorite['id'] | null = null; + let cursor: NoteFavorite['id'] | null = null; while (true) { const favorites = await this.noteFavoritesRepository.find({ @@ -89,21 +87,21 @@ export class ExportFavoritesProcessorService { id: 1, }, relations: ['note', 'note.user'], - }) as (MiNoteFavorite & { note: MiNote & { user: MiUser } })[]; + }) as (NoteFavorite & { note: Note & { user: User } })[]; if (favorites.length === 0) { job.updateProgress(100); break; } - cursor = favorites.at(-1)?.id ?? null; + cursor = favorites[favorites.length - 1].id; for (const favorite of favorites) { - let poll: MiPoll | undefined; + let poll: Poll | undefined; if (favorite.note.hasPoll) { poll = await this.pollsRepository.findOneByOrFail({ noteId: favorite.note.id }); } - const content = JSON.stringify(this.serialize(favorite, poll)); + const content = JSON.stringify(serialize(favorite, poll)); const isFirst = exportedFavoritesCount === 0; await write(isFirst ? content : ',\n' + content); exportedFavoritesCount++; @@ -125,43 +123,38 @@ export class ExportFavoritesProcessorService { const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); this.logger.succ(`Exported to: ${driveFile.id}`); - - this.notificationService.createNotification(user.id, 'exportCompleted', { - exportedEntity: 'favorite', - fileId: driveFile.id, - }); } finally { cleanup(); } } - - private serialize(favorite: MiNoteFavorite & { note: MiNote & { user: MiUser } }, poll: MiPoll | null = null): Record { - return { - id: favorite.id, - createdAt: this.idService.parse(favorite.id).date.toISOString(), - note: { - id: favorite.note.id, - text: favorite.note.text, - createdAt: this.idService.parse(favorite.note.id).date.toISOString(), - fileIds: favorite.note.fileIds, - replyId: favorite.note.replyId, - renoteId: favorite.note.renoteId, - poll: poll, - cw: favorite.note.cw, - visibility: favorite.note.visibility, - visibleUserIds: favorite.note.visibleUserIds, - localOnly: favorite.note.localOnly, - reactionAcceptance: favorite.note.reactionAcceptance, - uri: favorite.note.uri, - url: favorite.note.url, - user: { - id: favorite.note.user.id, - name: favorite.note.user.name, - username: favorite.note.user.username, - host: favorite.note.user.host, - uri: favorite.note.user.uri, - }, - }, - }; - } +} + +function serialize(favorite: NoteFavorite & { note: Note & { user: User } }, poll: Poll | null = null): Record { + return { + id: favorite.id, + createdAt: favorite.createdAt, + note: { + id: favorite.note.id, + text: favorite.note.text, + createdAt: favorite.note.createdAt, + fileIds: favorite.note.fileIds, + replyId: favorite.note.replyId, + renoteId: favorite.note.renoteId, + poll: poll, + cw: favorite.note.cw, + visibility: favorite.note.visibility, + visibleUserIds: favorite.note.visibleUserIds, + localOnly: favorite.note.localOnly, + reactionAcceptance: favorite.note.reactionAcceptance, + uri: favorite.note.uri, + url: favorite.note.url, + user: { + id: favorite.note.user.id, + name: favorite.note.user.name, + username: favorite.note.user.username, + host: favorite.note.user.host, + uri: favorite.note.user.uri, + }, + }, + }; } diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts index 91c39cb758..8726cb1402 100644 --- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts @@ -1,20 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { In, MoreThan, Not } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, FollowingsRepository, MutingsRepository } from '@/models/_.js'; +import type { UsersRepository, FollowingsRepository, MutingsRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; -import type { MiFollowing } from '@/models/Following.js'; +import type { Following } from '@/models/entities/Following.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -25,6 +20,9 @@ export class ExportFollowingProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -37,7 +35,6 @@ export class ExportFollowingProcessorService { private utilityService: UtilityService, private driveService: DriveService, private queueLoggerService: QueueLoggerService, - private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-following'); } @@ -59,7 +56,7 @@ export class ExportFollowingProcessorService { try { const stream = fs.createWriteStream(path, { flags: 'a' }); - let cursor: MiFollowing['id'] | null = null; + let cursor: Following['id'] | null = null; const mutings = job.data.excludeMuting ? await this.mutingsRepository.findBy({ muterId: user.id, @@ -76,13 +73,13 @@ export class ExportFollowingProcessorService { order: { id: 1, }, - }) as MiFollowing[]; + }) as Following[]; if (followings.length === 0) { break; } - cursor = followings.at(-1)?.id ?? null; + cursor = followings[followings.length - 1].id; for (const following of followings) { const u = await this.usersRepository.findOneBy({ id: following.followeeId }); @@ -94,8 +91,7 @@ export class ExportFollowingProcessorService { continue; } - const userAcct = this.utilityService.getFullApAccount(u.username, u.host); - const content = `${userAcct},withReplies=${following.withReplies}`; + const content = this.utilityService.getFullApAccount(u.username, u.host); await new Promise((res, rej) => { stream.write(content + '\n', err => { if (err) { @@ -116,11 +112,6 @@ export class ExportFollowingProcessorService { const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); this.logger.succ(`Exported to: ${driveFile.id}`); - - this.notificationService.createNotification(user.id, 'exportCompleted', { - exportedEntity: 'following', - fileId: driveFile.id, - }); } finally { cleanup(); } diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts index f9867ade29..0f11a9e843 100644 --- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts @@ -1,19 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import type { MutingsRepository, UsersRepository, MiMuting } from '@/models/_.js'; +import type { MutingsRepository, UsersRepository, BlockingsRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -24,16 +19,21 @@ export class ExportMutingProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, private utilityService: UtilityService, private driveService: DriveService, private queueLoggerService: QueueLoggerService, - private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-muting'); } @@ -56,7 +56,7 @@ export class ExportMutingProcessorService { const stream = fs.createWriteStream(path, { flags: 'a' }); let exportedCount = 0; - let cursor: MiMuting['id'] | null = null; + let cursor: any = null; while (true) { const mutes = await this.mutingsRepository.find({ @@ -76,7 +76,7 @@ export class ExportMutingProcessorService { break; } - cursor = mutes.at(-1)?.id ?? null; + cursor = mutes[mutes.length - 1].id; for (const mute of mutes) { const u = await this.usersRepository.findOneBy({ id: mute.muteeId }); @@ -112,11 +112,6 @@ export class ExportMutingProcessorService { const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); this.logger.succ(`Exported to: ${driveFile.id}`); - - this.notificationService.createNotification(user.id, 'exportCompleted', { - exportedEntity: 'muting', - fileId: driveFile.id, - }); } finally { cleanup(); } diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index 9e2b678219..24fb331883 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -1,105 +1,28 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ReadableStream, TextEncoderStream } from 'node:stream/web'; +import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js'; +import type { NotesRepository, PollsRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; -import type { MiPoll } from '@/models/Poll.js'; -import type { MiNote } from '@/models/Note.js'; +import type { Poll } from '@/models/entities/Poll.js'; +import type { Note } from '@/models/entities/Note.js'; import { bindThis } from '@/decorators.js'; -import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import { Packed } from '@/misc/json-schema.js'; -import { IdService } from '@/core/IdService.js'; -import { NotificationService } from '@/core/NotificationService.js'; -import { JsonArrayStream } from '@/misc/JsonArrayStream.js'; -import { FileWriterStream } from '@/misc/FileWriterStream.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; -class NoteStream extends ReadableStream> { - constructor( - job: Bull.Job, - notesRepository: NotesRepository, - pollsRepository: PollsRepository, - driveFileEntityService: DriveFileEntityService, - idService: IdService, - userId: string, - ) { - let exportedNotesCount = 0; - let cursor: MiNote['id'] | null = null; - - const serialize = ( - note: MiNote, - poll: MiPoll | null, - files: Packed<'DriveFile'>[], - ): Record => { - return { - id: note.id, - text: note.text, - createdAt: idService.parse(note.id).date.toISOString(), - fileIds: note.fileIds, - files: files, - replyId: note.replyId, - renoteId: note.renoteId, - poll: poll, - cw: note.cw, - visibility: note.visibility, - visibleUserIds: note.visibleUserIds, - localOnly: note.localOnly, - reactionAcceptance: note.reactionAcceptance, - }; - }; - - super({ - async pull(controller): Promise { - const notes = await notesRepository.find({ - where: { - userId, - ...(cursor !== null ? { id: MoreThan(cursor) } : {}), - }, - take: 100, // 100件ずつ取得 - order: { id: 1 }, - }); - - if (notes.length === 0) { - job.updateProgress(100); - controller.close(); - } - - cursor = notes.at(-1)?.id ?? null; - - for (const note of notes) { - const poll = note.hasPoll - ? await pollsRepository.findOneByOrFail({ noteId: note.id }) // N+1 - : null; - const files = await driveFileEntityService.packManyByIds(note.fileIds); // N+1 - const content = serialize(note, poll, files); - - controller.enqueue(content); - exportedNotesCount++; - } - - const total = await notesRepository.countBy({ userId }); - job.updateProgress(exportedNotesCount / total); - }, - }); - } -} - @Injectable() export class ExportNotesProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -111,9 +34,6 @@ export class ExportNotesProcessorService { private driveService: DriveService, private queueLoggerService: QueueLoggerService, - private driveFileEntityService: DriveFileEntityService, - private idService: IdService, - private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-notes'); } @@ -133,32 +53,91 @@ export class ExportNotesProcessorService { this.logger.info(`Temp file is ${path}`); try { - // メモリが足りなくならないようにストリームで処理する - await new NoteStream( - job, - this.notesRepository, - this.pollsRepository, - this.driveFileEntityService, - this.idService, - user.id, - ) - .pipeThrough(new JsonArrayStream()) - .pipeThrough(new TextEncoderStream()) - .pipeTo(new FileWriterStream(path)); + const stream = fs.createWriteStream(path, { flags: 'a' }); + const write = (text: string): Promise => { + return new Promise((res, rej) => { + stream.write(text, err => { + if (err) { + this.logger.error(err); + rej(err); + } else { + res(); + } + }); + }); + }; + + await write('['); + + let exportedNotesCount = 0; + let cursor: Note['id'] | null = null; + + while (true) { + const notes = await this.notesRepository.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }) as Note[]; + + if (notes.length === 0) { + job.updateProgress(100); + break; + } + + cursor = notes[notes.length - 1].id; + + for (const note of notes) { + let poll: Poll | undefined; + if (note.hasPoll) { + poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id }); + } + const content = JSON.stringify(serialize(note, poll)); + const isFirst = exportedNotesCount === 0; + await write(isFirst ? content : ',\n' + content); + exportedNotesCount++; + } + + const total = await this.notesRepository.countBy({ + userId: user.id, + }); + + job.updateProgress(exportedNotesCount / total); + } + + await write(']'); + + stream.end(); this.logger.succ(`Exported to: ${path}`); const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); this.logger.succ(`Exported to: ${driveFile.id}`); - - this.notificationService.createNotification(user.id, 'exportCompleted', { - exportedEntity: 'note', - fileId: driveFile.id, - }); } finally { cleanup(); } } } + +function serialize(note: Note, poll: Poll | null = null): Record { + return { + id: note.id, + text: note.text, + createdAt: note.createdAt, + fileIds: note.fileIds, + replyId: note.replyId, + renoteId: note.renoteId, + poll: poll, + cw: note.cw, + visibility: note.visibility, + visibleUserIds: note.visibleUserIds, + localOnly: note.localOnly, + reactionAcceptance: note.reactionAcceptance, + }; +} diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts index 733e75f65f..ec63358053 100644 --- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -1,19 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import type { UserListMembershipsRepository, UserListsRepository, UsersRepository } from '@/models/_.js'; +import type { UserListJoiningsRepository, UserListsRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -24,19 +19,21 @@ export class ExportUserListsProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, private utilityService: UtilityService, private driveService: DriveService, private queueLoggerService: QueueLoggerService, - private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-user-lists'); } @@ -63,16 +60,14 @@ export class ExportUserListsProcessorService { const stream = fs.createWriteStream(path, { flags: 'a' }); for (const list of lists) { - const memberships = await this.userListMembershipsRepository.findBy({ userListId: list.id }); + const joinings = await this.userListJoiningsRepository.findBy({ userListId: list.id }); const users = await this.usersRepository.findBy({ - id: In(memberships.map(j => j.userId)), + id: In(joinings.map(j => j.userId)), }); - const usersWithReplies = new Set(memberships.filter(m => m.withReplies).map(m => m.userId)); for (const u of users) { const acct = this.utilityService.getFullApAccount(u.username, u.host); - // 3rd column and later will be key=value pairs - const content = `${list.name},${acct},withReplies=${usersWithReplies.has(u.id)}`; + const content = `${list.name},${acct}`; await new Promise((res, rej) => { stream.write(content + '\n', err => { if (err) { @@ -93,11 +88,6 @@ export class ExportUserListsProcessorService { const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); this.logger.succ(`Exported to: ${driveFile.id}`); - - this.notificationService.createNotification(user.id, 'exportCompleted', { - exportedEntity: 'userList', - fileId: driveFile.id, - }); } finally { cleanup(); } diff --git a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts index 4c7f2d09bb..0c09f2796f 100644 --- a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts @@ -1,33 +1,27 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable, Inject } from '@nestjs/common'; import _Ajv from 'ajv'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import Logger from '@/logger.js'; -import type { AntennasRepository } from '@/models/_.js'; +import type { AntennasRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { Schema, SchemaType } from '@/misc/json-schema.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import { DBAntennaImportJobData } from '../types.js'; import type * as Bull from 'bullmq'; const Ajv = _Ajv.default; -const exportedAntennaSchema = { +const validate = new Ajv().compile({ type: 'object', properties: { name: { type: 'string', minLength: 1, maxLength: 100 }, - src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'users_blacklist'] }, - userListAccts: { - type: 'array', + src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, + userListAccts: { + type: 'array', items: { type: 'string', - }, + }, nullable: true, }, keywords: { type: 'array', items: { @@ -44,18 +38,12 @@ const exportedAntennaSchema = { type: 'string', } }, caseSensitive: { type: 'boolean' }, - localOnly: { type: 'boolean' }, - excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, - excludeNotesInSensitiveChannel: { type: 'boolean' }, + notify: { type: 'boolean' }, }, - required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'], -} as const satisfies Schema; - -export type ExportedAntenna = SchemaType; - -const validate = new Ajv().compile(exportedAntennaSchema); + required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], +}); @Injectable() export class ImportAntennasProcessorService { @@ -82,8 +70,9 @@ export class ImportAntennasProcessorService { this.logger.warn('Validation Failed'); continue; } - const result = await this.antennasRepository.insertOne({ - id: this.idService.gen(now.getTime()), + const result = await this.antennasRepository.insert({ + id: this.idService.genId(), + createdAt: now, lastUsedAt: now, userId: job.data.user.id, name: antenna.name, @@ -93,12 +82,10 @@ export class ImportAntennasProcessorService { excludeKeywords: antenna.excludeKeywords, users: (antenna.src === 'list' && antenna.userListAccts !== null ? antenna.userListAccts : antenna.users).filter(Boolean), caseSensitive: antenna.caseSensitive, - localOnly: antenna.localOnly, - excludeBots: antenna.excludeBots, withReplies: antenna.withReplies, withFile: antenna.withFile, - excludeNotesInSensitiveChannel: antenna.excludeNotesInSensitiveChannel, - }); + notify: antenna.notify, + }).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0])); this.logger.succ('Antenna created: ' + result.id); this.globalEventService.publishInternalEvent('antennaCreated', result); } diff --git a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts index b78229c648..2f1a9e5b03 100644 --- a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, DriveFilesRepository } from '@/models/_.js'; +import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 95fe0a2c6a..d862567871 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -1,14 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; -import { ZipReader } from 'slacc'; -import { IsNull } from 'typeorm'; +import { DataSource } from 'typeorm'; +import unzipper from 'unzipper'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository, DriveFilesRepository } from '@/models/_.js'; +import type { EmojisRepository, DriveFilesRepository, UsersRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { createTempDir } from '@/misc/create-temp.js'; @@ -25,6 +21,15 @@ export class ImportCustomEmojisProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -67,9 +72,9 @@ export class ImportCustomEmojisProcessorService { } const outputPath = path + '/emojis'; - try { - this.logger.succ(`Unzipping to ${outputPath}`); - ZipReader.withDestinationPath(outputPath).viaBuffer(await fs.promises.readFile(destPath)); + const unzipStream = fs.createReadStream(destPath); + const extractor = unzipper.Extract({ path: outputPath }); + extractor.on('close', async () => { const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8'); const meta = JSON.parse(metaRaw); @@ -87,46 +92,31 @@ export class ImportCustomEmojisProcessorService { const emojiPath = outputPath + '/' + record.fileName; await this.emojisRepository.delete({ name: emojiInfo.name, - host: IsNull(), }); - - try { - const driveFile = await this.driveService.addFile({ - user: null, - path: emojiPath, - name: record.fileName, - force: true, - }); - await this.customEmojiService.add({ - originalUrl: driveFile.url, - publicUrl: driveFile.webpublicUrl ?? driveFile.url, - fileType: driveFile.webpublicType ?? driveFile.type, - name: emojiInfo.name, - category: emojiInfo.category, - host: null, - aliases: emojiInfo.aliases, - license: emojiInfo.license, - isSensitive: emojiInfo.isSensitive, - localOnly: emojiInfo.localOnly, - roleIdsThatCanBeUsedThisEmojiAsReaction: [], - }); - } catch (e) { - if (e instanceof Error || typeof e === 'string') { - this.logger.error(`couldn't import ${emojiPath} for ${emojiInfo.name}: ${e}`); - } - continue; - } + const driveFile = await this.driveService.addFile({ + user: null, + path: emojiPath, + name: record.fileName, + force: true, + }); + await this.customEmojiService.add({ + name: emojiInfo.name, + category: emojiInfo.category, + host: null, + aliases: emojiInfo.aliases, + driveFile, + license: emojiInfo.license, + isSensitive: emojiInfo.isSensitive, + localOnly: emojiInfo.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: [], + }); } cleanup(); - + this.logger.succ('Imported'); - } catch (e) { - if (e instanceof Error || typeof e === 'string') { - this.logger.error(e); - } - cleanup(); - throw e; - } + }); + unzipStream.pipe(extractor); + this.logger.succ(`Unzipping to ${outputPath}`); } } diff --git a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts index 03663d3b06..15bee9672e 100644 --- a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, DriveFilesRepository } from '@/models/_.js'; +import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; @@ -56,7 +51,7 @@ export class ImportFollowingProcessorService { const csv = await this.downloadService.downloadTextFile(file.url); const targets = csv.trim().split('\n'); - this.queueService.createImportFollowingToDbJob({ id: user.id }, targets, job.data.withReplies); + this.queueService.createImportFollowingToDbJob({ id: user.id }, targets); this.logger.succ('Import jobs created'); } @@ -67,19 +62,8 @@ export class ImportFollowingProcessorService { const user = job.data.user; try { - const parts = line.split(','); - const acct = parts[0].trim(); + const acct = line.split(',')[0].trim(); const { username, host } = Acct.parse(acct); - let withReplies: boolean | null = null; - - for (const keyValue of parts.slice(2)) { - const [key, value] = keyValue.split('='); - switch (key) { - case 'withReplies': - withReplies = value === 'true'; - break; - } - } if (!host) return; @@ -104,9 +88,9 @@ export class ImportFollowingProcessorService { // skip myself if (target.id === job.data.user.id) return; - this.logger.info(`Follow ${target.id} ${job.data.withReplies ? 'with replies' : 'without replies'} ...`); + this.logger.info(`Follow ${target.id} ...`); - await this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: withReplies ?? job.data.withReplies }]); + this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true }]); } catch (e) { this.logger.warn(`Error: ${e}`); } diff --git a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts index ec9d2b6c4c..723935cd31 100644 --- a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts @@ -1,12 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, DriveFilesRepository } from '@/models/_.js'; +import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; @@ -23,6 +19,9 @@ export class ImportMutingProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index bf061a1f78..824ee8157a 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -1,12 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, DriveFilesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; +import type { UsersRepository, DriveFilesRepository, UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; @@ -24,6 +20,9 @@ export class ImportUserListsProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -33,8 +32,8 @@ export class ImportUserListsProcessorService { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, private utilityService: UtilityService, private idService: IdService, @@ -70,19 +69,8 @@ export class ImportUserListsProcessorService { linenum++; try { - const parts = line.split(','); - const listName = parts[0].trim(); - const { username, host } = Acct.parse(parts[1].trim()); - let withReplies = false; - - for (const keyValue of parts.slice(2)) { - const [key, value] = keyValue.split('='); - switch (key) { - case 'withReplies': - withReplies = value === 'true'; - break; - } - } + const listName = line.split(',')[0].trim(); + const { username, host } = Acct.parse(line.split(',')[1].trim()); let list = await this.userListsRepository.findOneBy({ userId: user.id, @@ -90,11 +78,12 @@ export class ImportUserListsProcessorService { }); if (list == null) { - list = await this.userListsRepository.insertOne({ - id: this.idService.gen(), + list = await this.userListsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), userId: user.id, name: listName, - }); + }).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); } let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({ @@ -109,11 +98,9 @@ export class ImportUserListsProcessorService { target = await this.remoteUserResolveService.resolveUser(username, host); } - if (await this.userListMembershipsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue; + if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue; - await this.userListService.addMember(target, list, user, { - withReplies: withReplies, - }); + this.userListService.push(target, list!, user); } catch (e) { this.logger.warn(`Error in line:${linenum} ${e}`); } diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 079e014da8..ce1d7aaa1b 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -1,56 +1,45 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { URL } from 'node:url'; -import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import httpSignature from '@peertube/http-signature'; import * as Bull from 'bullmq'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; +import { MetaService } from '@/core/MetaService.js'; +import { ApRequestService } from '@/core/activitypub/ApRequestService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import FederationChart from '@/core/chart/charts/federation.js'; import { getApId } from '@/core/activitypub/type.js'; -import type { IActivity } from '@/core/activitypub/type.js'; -import type { MiRemoteUser } from '@/models/User.js'; -import type { MiUserPublickey } from '@/models/UserPublickey.js'; +import type { RemoteUser } from '@/models/entities/User.js'; +import type { UserPublickey } from '@/models/entities/UserPublickey.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; -import { JsonLdService } from '@/core/activitypub/JsonLdService.js'; +import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js'; import { ApInboxService } from '@/core/activitypub/ApInboxService.js'; import { bindThis } from '@/decorators.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { CollapsedQueue } from '@/misc/collapsed-queue.js'; -import { MiNote } from '@/models/Note.js'; -import { MiMeta } from '@/models/Meta.js'; -import { DI } from '@/di-symbols.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type { InboxJobData } from '../types.js'; -type UpdateInstanceJob = { - latestRequestReceivedAt: Date, - shouldUnsuspend: boolean, -}; - @Injectable() -export class InboxProcessorService implements OnApplicationShutdown { +export class InboxProcessorService { private logger: Logger; - private updateInstanceQueue: CollapsedQueue; constructor( - @Inject(DI.meta) - private meta: MiMeta, + @Inject(DI.config) + private config: Config, private utilityService: UtilityService, + private metaService: MetaService, private apInboxService: ApInboxService, private federatedInstanceService: FederatedInstanceService, private fetchInstanceMetadataService: FetchInstanceMetadataService, - private jsonLdService: JsonLdService, + private ldSignatureService: LdSignatureService, + private apRequestService: ApRequestService, private apPersonService: ApPersonService, private apDbResolverService: ApDbResolverService, private instanceChart: InstanceChart, @@ -59,13 +48,12 @@ export class InboxProcessorService implements OnApplicationShutdown { private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('inbox'); - this.updateInstanceQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseUpdateInstanceJobs, this.performUpdateInstance); } @bindThis public async process(job: Bull.Job): Promise { const signature = job.data.signature; // HTTP-signature - let activity = job.data.activity; + const activity = job.data.activity; //#region Log const info = Object.assign({}, activity); @@ -75,7 +63,9 @@ export class InboxProcessorService implements OnApplicationShutdown { const host = this.utilityService.toPuny(new URL(signature.keyId).hostname); - if (!this.utilityService.isFederationAllowedHost(host)) { + // ブロックしてたら中断 + const meta = await this.metaService.fetch(); + if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) { return `Blocked request: ${host}`; } @@ -86,8 +76,8 @@ export class InboxProcessorService implements OnApplicationShutdown { // HTTP-Signature keyIdを元にDBから取得 let authUser: { - user: MiRemoteUser; - key: MiUserPublickey | null; + user: RemoteUser; + key: UserPublickey | null; } | null = await this.apDbResolverService.getAuthUserFromKeyId(signature.keyId); // keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得 @@ -97,22 +87,22 @@ export class InboxProcessorService implements OnApplicationShutdown { } catch (err) { // 対象が4xxならスキップ if (err instanceof StatusError) { - if (!err.isRetryable) { + if (err.isClientError) { throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`); } - throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`); + throw new Error(`Error in actor ${activity.actor} - ${err.statusCode ?? err}`); } } } // それでもわからなければ終了 if (authUser == null) { - throw new Bull.UnrecoverableError(`skip: failed to resolve user ${getApId(activity.actor)}`); + throw new Bull.UnrecoverableError('skip: failed to resolve user'); } // publicKey がなくても終了 if (authUser.key == null) { - throw new Bull.UnrecoverableError(`skip: failed to resolve user publicKey ${getApId(activity.actor)}`); + throw new Bull.UnrecoverableError('skip: failed to resolve user publicKey'); } // HTTP-Signatureの検証 @@ -121,21 +111,20 @@ export class InboxProcessorService implements OnApplicationShutdown { // また、signatureのsignerは、activity.actorと一致する必要がある if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { // 一致しなくても、でもLD-Signatureがありそうならそっちも見る - const ldSignature = activity.signature; - if (ldSignature) { - if (ldSignature.type !== 'RsaSignature2017') { - throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${ldSignature.type}`); + if (activity.signature) { + if (activity.signature.type !== 'RsaSignature2017') { + throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`); } - // ldSignature.creator: https://example.oom/users/user#main-key + // activity.signature.creator: https://example.oom/users/user#main-key // みたいになっててUserを引っ張れば公開キーも入ることを期待する - if (ldSignature.creator) { - const candicate = ldSignature.creator.replace(/#.*/, ''); + if (activity.signature.creator) { + const candicate = activity.signature.creator.replace(/#.*/, ''); await this.apPersonService.resolvePerson(candicate).catch(() => null); } // keyIdからLD-Signatureのユーザーを取得 - authUser = await this.apDbResolverService.getAuthUserFromKeyId(ldSignature.creator); + authUser = await this.apDbResolverService.getAuthUserFromKeyId(activity.signature.creator); if (authUser == null) { throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした'); } @@ -144,38 +133,21 @@ export class InboxProcessorService implements OnApplicationShutdown { throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした'); } - const jsonLd = this.jsonLdService.use(); - // LD-Signature検証 - const verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); + const ldSignature = this.ldSignatureService.use(); + const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); if (!verified) { throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました'); } - // アクティビティを正規化 - delete activity.signature; - try { - activity = await jsonLd.compact(activity) as IActivity; - } catch (e) { - throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${e}`); - } - // TODO: 元のアクティビティと非互換な形に正規化される場合は転送をスキップする - // https://github.com/mastodon/mastodon/blob/664b0ca/app/services/activitypub/process_collection_service.rb#L24-L29 - activity.signature = ldSignature; - - //#region Log - const compactedInfo = Object.assign({}, activity); - delete compactedInfo['@context']; - this.logger.debug(`compacted: ${JSON.stringify(compactedInfo, null, 2)}`); - //#endregion - // もう一度actorチェック if (authUser.user.uri !== activity.actor) { throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`); } + // ブロックしてたら中断 const ldHost = this.utilityService.extractDbHost(authUser.user.uri); - if (!this.utilityService.isFederationAllowedHost(ldHost)) { + if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) { throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`); } } else { @@ -190,86 +162,27 @@ export class InboxProcessorService implements OnApplicationShutdown { if (signerHost !== activityIdHost) { throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`); } - } else { - throw new Bull.UnrecoverableError('skip: activity id is not a string'); } - this.apRequestChart.inbox(); - this.federationChart.inbox(authUser.user.host); - - // Update instance stats - process.nextTick(async () => { - const i = await (this.meta.enableStatsForFederatedInstances - ? this.federatedInstanceService.fetchOrRegister(authUser.user.host) - : this.federatedInstanceService.fetch(authUser.user.host)); - - if (i == null) return; - - this.updateInstanceQueue.enqueue(i.id, { + // Update stats + this.federatedInstanceService.fetch(authUser.user.host).then(i => { + this.federatedInstanceService.update(i.id, { latestRequestReceivedAt: new Date(), - shouldUnsuspend: i.suspensionState === 'autoSuspendedForNotResponding', + isNotResponding: false, }); - if (this.meta.enableChartsForFederatedInstances) { + this.fetchInstanceMetadataService.fetchInstanceMetadata(i); + + this.apRequestChart.inbox(); + this.federationChart.inbox(i.host); + + if (meta.enableChartsForFederatedInstances) { this.instanceChart.requestReceived(i.host); } - - this.fetchInstanceMetadataService.fetchInstanceMetadata(i); }); // アクティビティを処理 - try { - const result = await this.apInboxService.performActivity(authUser.user, activity); - if (result && !result.startsWith('ok')) { - this.logger.warn(`inbox activity ignored (maybe): id=${activity.id} reason=${result}`); - return result; - } - } catch (e) { - if (e instanceof IdentifiableError) { - if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') { - return 'blocked notes with prohibited words'; - } - if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') { - return 'actor has been suspended'; - } - if (e.id === 'd450b8a9-48e4-4dab-ae36-f4db763fda7c') { // invalid Note - return e.message; - } - } - throw e; - } + await this.apInboxService.performActivity(authUser.user, activity); return 'ok'; } - - @bindThis - public collapseUpdateInstanceJobs(oldJob: UpdateInstanceJob, newJob: UpdateInstanceJob) { - const latestRequestReceivedAt = oldJob.latestRequestReceivedAt < newJob.latestRequestReceivedAt - ? newJob.latestRequestReceivedAt - : oldJob.latestRequestReceivedAt; - const shouldUnsuspend = oldJob.shouldUnsuspend || newJob.shouldUnsuspend; - return { - latestRequestReceivedAt, - shouldUnsuspend, - }; - } - - @bindThis - public async performUpdateInstance(id: string, job: UpdateInstanceJob) { - await this.federatedInstanceService.update(id, { - latestRequestReceivedAt: new Date(), - isNotResponding: false, - // もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる - suspensionState: job.shouldUnsuspend ? 'none' : undefined, - }); - } - - @bindThis - public async dispose(): Promise { - await this.updateInstanceQueue.performAllNow(); - } - - @bindThis - async onApplicationShutdown(signal?: string) { - await this.dispose(); - } } diff --git a/packages/backend/src/queue/processors/RelationshipProcessorService.ts b/packages/backend/src/queue/processors/RelationshipProcessorService.ts index 408b02fb38..722260d948 100644 --- a/packages/backend/src/queue/processors/RelationshipProcessorService.ts +++ b/packages/backend/src/queue/processors/RelationshipProcessorService.ts @@ -1,21 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; +import type * as Bull from 'bullmq'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; -import type { UsersRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { MiLocalUser, MiRemoteUser } from '@/models/User.js'; -import { RelationshipJobData } from '../types.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type * as Bull from 'bullmq'; +import { RelationshipJobData } from '../types.js'; +import type { UsersRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { LocalUser, RemoteUser } from '@/models/entities/User.js'; @Injectable() export class RelationshipProcessorService { @@ -34,12 +29,8 @@ export class RelationshipProcessorService { @bindThis public async processFollow(job: Bull.Job): Promise { - this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id} ${job.data.withReplies ? "with replies" : "without replies"}`); - await this.userFollowingService.follow(job.data.from, job.data.to, { - requestId: job.data.requestId, - silent: job.data.silent, - withReplies: job.data.withReplies, - }); + this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id}`); + await this.userFollowingService.follow(job.data.from, job.data.to, job.data.requestId, job.data.silent); return 'ok'; } @@ -49,7 +40,7 @@ export class RelationshipProcessorService { const [follower, followee] = await Promise.all([ this.usersRepository.findOneByOrFail({ id: job.data.from.id }), this.usersRepository.findOneByOrFail({ id: job.data.to.id }), - ]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser]; + ]) as [LocalUser | RemoteUser, LocalUser | RemoteUser]; await this.userFollowingService.unfollow(follower, followee, job.data.silent); return 'ok'; } diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts index 0c47fdedb3..eab8e1e68d 100644 --- a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts @@ -1,13 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; +import FederationChart from '@/core/chart/charts/federation.js'; import NotesChart from '@/core/chart/charts/notes.js'; import UsersChart from '@/core/chart/charts/users.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +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 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 * as Bull from 'bullmq'; @@ -17,9 +22,21 @@ export class ResyncChartsProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + + private federationChart: FederationChart, private notesChart: NotesChart, private usersChart: UsersChart, + private activeUsersChart: ActiveUsersChart, + private instanceChart: InstanceChart, + private perUserNotesChart: PerUserNotesChart, private driveChart: DriveChart, + private perUserReactionsChart: PerUserReactionsChart, + private perUserFollowingChart: PerUserFollowingChart, + private perUserDriveChart: PerUserDriveChart, + private apRequestChart: ApRequestChart, + private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('resync-charts'); @@ -29,12 +46,13 @@ export class ResyncChartsProcessorService { public async process(): Promise { this.logger.info('Resync charts...'); - // DBへの同時接続を避けるためにPromise.allを使わずひとつずつ実行する // TODO: ユーザーごとのチャートも更新する // TODO: インスタンスごとのチャートも更新する - await this.driveChart.resync(); - await this.notesChart.resync(); - await this.usersChart.resync(); + await Promise.all([ + this.driveChart.resync(), + this.notesChart.resync(), + this.usersChart.resync(), + ]); this.logger.succ('All charts successfully resynced.'); } diff --git a/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts deleted file mode 100644 index f6bef52684..0000000000 --- a/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import * as Bull from 'bullmq'; -import { DI } from '@/di-symbols.js'; -import type { SystemWebhooksRepository } from '@/models/_.js'; -import type { Config } from '@/config.js'; -import type Logger from '@/logger.js'; -import { HttpRequestService } from '@/core/HttpRequestService.js'; -import { StatusError } from '@/misc/status-error.js'; -import { bindThis } from '@/decorators.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import { SystemWebhookDeliverJobData } from '../types.js'; - -@Injectable() -export class SystemWebhookDeliverProcessorService { - private logger: Logger; - - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.systemWebhooksRepository) - private systemWebhooksRepository: SystemWebhooksRepository, - - private httpRequestService: HttpRequestService, - private queueLoggerService: QueueLoggerService, - ) { - this.logger = this.queueLoggerService.logger.createSubLogger('webhook'); - } - - @bindThis - public async process(job: Bull.Job): Promise { - try { - this.logger.debug(`delivering ${job.data.webhookId}`); - - const res = await this.httpRequestService.send(job.data.to, { - method: 'POST', - headers: { - 'User-Agent': 'Misskey-Hooks', - 'X-Misskey-Host': this.config.host, - 'X-Misskey-Hook-Id': job.data.webhookId, - 'X-Misskey-Hook-Secret': job.data.secret, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - server: this.config.url, - hookId: job.data.webhookId, - eventId: job.data.eventId, - createdAt: job.data.createdAt, - type: job.data.type, - body: job.data.content, - }), - }); - - this.systemWebhooksRepository.update({ id: job.data.webhookId }, { - latestSentAt: new Date(), - latestStatus: res.status, - }); - - return 'Success'; - } catch (res) { - this.logger.error(res as Error); - - this.systemWebhooksRepository.update({ id: job.data.webhookId }, { - latestSentAt: new Date(), - latestStatus: res instanceof StatusError ? res.statusCode : 1, - }); - - if (res instanceof StatusError) { - // 4xx - if (!res.isRetryable) { - throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); - } - - // 5xx etc. - throw new Error(`${res.statusCode} ${res.statusMessage}`); - } else { - // DNS error, socket error, timeout ... - throw res; - } - } - } -} diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts index fc8856a271..f1696bf567 100644 --- a/packages/backend/src/queue/processors/TickChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts @@ -1,9 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import FederationChart from '@/core/chart/charts/federation.js'; import NotesChart from '@/core/chart/charts/notes.js'; @@ -26,6 +23,9 @@ export class TickChartsProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + private federationChart: FederationChart, private notesChart: NotesChart, private usersChart: UsersChart, @@ -48,19 +48,20 @@ export class TickChartsProcessorService { public async process(): Promise { this.logger.info('Tick charts...'); - // DBへの同時接続を避けるためにPromise.allを使わずひとつずつ実行する - await this.federationChart.tick(false); - await this.notesChart.tick(false); - await this.usersChart.tick(false); - await this.activeUsersChart.tick(false); - await this.instanceChart.tick(false); - await this.perUserNotesChart.tick(false); - await this.perUserPvChart.tick(false); - await this.driveChart.tick(false); - await this.perUserReactionsChart.tick(false); - await this.perUserFollowingChart.tick(false); - await this.perUserDriveChart.tick(false); - await this.apRequestChart.tick(false); + await Promise.all([ + this.federationChart.tick(false), + this.notesChart.tick(false), + this.usersChart.tick(false), + this.activeUsersChart.tick(false), + this.instanceChart.tick(false), + this.perUserNotesChart.tick(false), + this.perUserPvChart.tick(false), + this.driveChart.tick(false), + this.perUserReactionsChart.tick(false), + this.perUserFollowingChart.tick(false), + this.perUserDriveChart.tick(false), + this.apRequestChart.tick(false), + ]); this.logger.succ('All charts successfully ticked.'); } diff --git a/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts similarity index 83% rename from packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts rename to packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts index 9ec630ef70..8b40c16749 100644 --- a/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts @@ -1,22 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; -import type { WebhooksRepository } from '@/models/_.js'; +import type { WebhooksRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { StatusError } from '@/misc/status-error.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import { UserWebhookDeliverJobData } from '../types.js'; +import type { WebhookDeliverJobData } from '../types.js'; @Injectable() -export class UserWebhookDeliverProcessorService { +export class WebhookDeliverProcessorService { private logger: Logger; constructor( @@ -33,10 +28,10 @@ export class UserWebhookDeliverProcessorService { } @bindThis - public async process(job: Bull.Job): Promise { + public async process(job: Bull.Job): Promise { try { this.logger.debug(`delivering ${job.data.webhookId}`); - + const res = await this.httpRequestService.send(job.data.to, { method: 'POST', headers: { @@ -47,7 +42,6 @@ export class UserWebhookDeliverProcessorService { 'Content-Type': 'application/json', }, body: JSON.stringify({ - server: this.config.url, hookId: job.data.webhookId, userId: job.data.userId, eventId: job.data.eventId, @@ -56,25 +50,25 @@ export class UserWebhookDeliverProcessorService { body: job.data.content, }), }); - + this.webhooksRepository.update({ id: job.data.webhookId }, { latestSentAt: new Date(), latestStatus: res.status, }); - + return 'Success'; } catch (res) { this.webhooksRepository.update({ id: job.data.webhookId }, { latestSentAt: new Date(), latestStatus: res instanceof StatusError ? res.statusCode : 1, }); - + if (res instanceof StatusError) { // 4xx - if (!res.isRetryable) { + if (res.isClientError) { throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); } - + // 5xx etc. throw new Error(`${res.statusCode} ${res.statusMessage}`); } else { diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 757daea88b..776dd3aa12 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -1,26 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiNote } from '@/models/Note.js'; -import type { SystemWebhookEventType } from '@/models/SystemWebhook.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { User } from '@/models/entities/User.js'; +import type { Webhook } from '@/models/entities/Webhook.js'; import type { IActivity } from '@/core/activitypub/type.js'; -import type { SystemWebhookPayload } from '@/core/SystemWebhookService.js'; -import type { UserWebhookPayload } from '@/core/UserWebhookService.js'; import type httpSignature from '@peertube/http-signature'; export type DeliverJobData = { /** Actor */ user: ThinUser; /** Activity */ - content: string; - /** Digest header */ - digest: string; + content: unknown; /** inbox URL to deliver */ to: string; /** whether it is sharedInbox */ @@ -37,8 +27,7 @@ export type RelationshipJobData = { to: ThinUser; silent?: boolean; requestId?: string; - withReplies?: boolean; -}; +} export type DbJobData = DbJobMap[T]; @@ -61,11 +50,11 @@ export type DbJobMap = { importUserLists: DbUserImportJobData; importCustomEmojis: DbUserImportJobData; deleteAccount: DbUserDeleteJobData; -}; +} export type DbJobDataWithUser = { user: ThinUser; -}; +} export type DbExportFollowingData = { user: ThinUser; @@ -75,7 +64,7 @@ export type DbExportFollowingData = { export type DBExportAntennasData = { user: ThinUser -}; +} export type DbUserDeleteJobData = { user: ThinUser; @@ -84,19 +73,17 @@ export type DbUserDeleteJobData = { export type DbUserImportJobData = { user: ThinUser; - fileId: MiDriveFile['id']; - withReplies?: boolean; + fileId: DriveFile['id']; }; export type DBAntennaImportJobData = { user: ThinUser, antenna: Antenna -}; +} export type DbUserImportToDbJobData = { user: ThinUser; target: string; - withReplies?: boolean; }; export type ObjectStorageJobData = ObjectStorageFileJobData | Record; @@ -106,24 +93,14 @@ export type ObjectStorageFileJobData = { }; export type EndedPollNotificationJobData = { - noteId: MiNote['id']; + noteId: Note['id']; }; -export type SystemWebhookDeliverJobData = { - type: T; - content: SystemWebhookPayload; - webhookId: MiWebhook['id']; - to: string; - secret: string; - createdAt: number; - eventId: string; -}; - -export type UserWebhookDeliverJobData = { - type: T; - content: UserWebhookPayload; - webhookId: MiWebhook['id']; - userId: MiUser['id']; +export type WebhookDeliverJobData = { + type: string; + content: unknown; + webhookId: Webhook['id']; + userId: User['id']; to: string; secret: string; createdAt: number; @@ -131,5 +108,5 @@ export type UserWebhookDeliverJobData { - if (isRenote(note) && !isQuote(note)) { + private async packActivity(note: Note): Promise { + if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) { const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId }); return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note); } @@ -107,72 +93,16 @@ export class ActivityPubServerService { @bindThis private inbox(request: FastifyRequest, reply: FastifyReply) { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - let signature; try { - signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'host', 'date'], authorizationHeaderName: 'signature' }); + signature = httpSignature.parseRequest(request.raw, { 'headers': [] }); } catch (e) { reply.code(401); return; } - if (signature.params.headers.indexOf('host') === -1 - || request.headers.host !== this.config.host) { - // Host not specified or not match. - reply.code(401); - return; - } - - if (signature.params.headers.indexOf('digest') === -1) { - // Digest not found. - reply.code(401); - } else { - const digest = request.headers.digest; - - if (typeof digest !== 'string') { - // Huh? - reply.code(401); - return; - } - - const re = /^([a-zA-Z0-9\-]+)=(.+)$/; - const match = digest.match(re); - - if (match == null) { - // Invalid digest - reply.code(401); - return; - } - - const algo = match[1].toUpperCase(); - const digestValue = match[2]; - - if (algo !== 'SHA-256') { - // Unsupported digest algorithm - reply.code(401); - return; - } - - if (request.rawBody == null) { - // Bad request - reply.code(400); - return; - } - - const hash = crypto.createHash('sha256').update(request.rawBody).digest('base64'); - - if (hash !== digestValue) { - // Invalid digest - reply.code(401); - return; - } - } - + // TODO: request.bodyのバリデーション? this.queueService.inbox(request.body as IActivity, signature); reply.code(202); @@ -183,11 +113,6 @@ export class ActivityPubServerService { request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>, reply: FastifyReply, ) { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - const userId = request.params.user; const cursor = request.query.cursor; @@ -211,11 +136,11 @@ export class ActivityPubServerService { //#region Check ff visibility const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.followersVisibility === 'private') { + if (profile.ffVisibility === 'private') { reply.code(403); reply.header('Cache-Control', 'public, max-age=30'); return; - } else if (profile.followersVisibility === 'followers') { + } else if (profile.ffVisibility === 'followers') { reply.code(403); reply.header('Cache-Control', 'public, max-age=30'); return; @@ -228,7 +153,7 @@ export class ActivityPubServerService { if (page) { const query = { followeeId: user.id, - } as FindOptionsWhere; + } as FindOptionsWhere; // カーソルが指定されている場合 if (cursor) { @@ -256,7 +181,7 @@ export class ActivityPubServerService { undefined, inStock ? `${partOf}?${url.query({ page: 'true', - cursor: followings.at(-1)!.id, + cursor: followings[followings.length - 1].id, })}` : undefined, ); @@ -264,11 +189,7 @@ export class ActivityPubServerService { return (this.apRendererService.addContext(rendered)); } else { // index page - const rendered = this.apRendererService.renderOrderedCollection( - partOf, - user.followersCount, - `${partOf}?page=true`, - ); + const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`); reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); @@ -280,11 +201,6 @@ export class ActivityPubServerService { request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>, reply: FastifyReply, ) { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - const userId = request.params.user; const cursor = request.query.cursor; @@ -308,11 +224,11 @@ export class ActivityPubServerService { //#region Check ff visibility const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.followingVisibility === 'private') { + if (profile.ffVisibility === 'private') { reply.code(403); reply.header('Cache-Control', 'public, max-age=30'); return; - } else if (profile.followingVisibility === 'followers') { + } else if (profile.ffVisibility === 'followers') { reply.code(403); reply.header('Cache-Control', 'public, max-age=30'); return; @@ -325,7 +241,7 @@ export class ActivityPubServerService { if (page) { const query = { followerId: user.id, - } as FindOptionsWhere; + } as FindOptionsWhere; // カーソルが指定されている場合 if (cursor) { @@ -353,7 +269,7 @@ export class ActivityPubServerService { undefined, inStock ? `${partOf}?${url.query({ page: 'true', - cursor: followings.at(-1)!.id, + cursor: followings[followings.length - 1].id, })}` : undefined, ); @@ -361,11 +277,7 @@ export class ActivityPubServerService { return (this.apRendererService.addContext(rendered)); } else { // index page - const rendered = this.apRendererService.renderOrderedCollection( - partOf, - user.followingCount, - `${partOf}?page=true`, - ); + const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`); reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); @@ -374,11 +286,6 @@ export class ActivityPubServerService { @bindThis private async featured(request: FastifyRequest<{ Params: { user: string; }; }>, reply: FastifyReply) { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - const userId = request.params.user; const user = await this.usersRepository.findOneBy({ @@ -396,18 +303,14 @@ export class ActivityPubServerService { order: { id: 'DESC' }, }); - const pinnedNotes = (await Promise.all(pinings.map(pining => - this.notesRepository.findOneByOrFail({ id: pining.noteId })))) - .filter(note => !note.localOnly && ['public', 'home'].includes(note.visibility)); + const pinnedNotes = await Promise.all(pinings.map(pining => + this.notesRepository.findOneByOrFail({ id: pining.noteId }))); const renderedNotes = await Promise.all(pinnedNotes.map(note => this.apRendererService.renderNote(note))); const rendered = this.apRendererService.renderOrderedCollection( `${this.config.url}/users/${userId}/collections/featured`, - renderedNotes.length, - undefined, - undefined, - renderedNotes, + renderedNotes.length, undefined, undefined, renderedNotes, ); reply.header('Cache-Control', 'public, max-age=180'); @@ -423,11 +326,6 @@ export class ActivityPubServerService { }>, reply: FastifyReply, ) { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - const userId = request.params.user; const sinceId = request.query.since_id; @@ -463,28 +361,15 @@ export class ActivityPubServerService { const partOf = `${this.config.url}/users/${userId}/outbox`; if (page) { - const notes = this.meta.enableFanoutTimeline ? await this.fanoutTimelineEndpointService.getMiNotes({ - sinceId: sinceId ?? null, - untilId: untilId ?? null, - limit: limit, - allowPartial: false, // Possibly true? IDK it's OK for ordered collection. - me: null, - redisTimelines: [ - `userTimeline:${user.id}`, - `userTimelineWithReplies:${user.id}`, - ], - useDbFallback: true, - ignoreAuthorFromMute: true, - excludePureRenotes: false, - noteFilter: (note) => { - if (note.visibility !== 'home' && note.visibility !== 'public') return false; - if (note.localOnly) return false; - return true; - }, - dbFallback: async (untilId, sinceId, limit) => { - return await this.getUserNotesFromDb(sinceId, untilId, limit, user.id); - }, - }) : await this.getUserNotesFromDb(sinceId ?? null, untilId ?? null, limit, user.id); + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId) + .andWhere('note.userId = :userId', { userId: user.id }) + .andWhere(new Brackets(qb => { qb + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); + })) + .andWhere('note.localOnly = FALSE'); + + const notes = await query.take(limit).getMany(); if (sinceId) notes.reverse(); @@ -502,7 +387,7 @@ export class ActivityPubServerService { })}` : undefined, notes.length ? `${partOf}?${url.query({ page: 'true', - until_id: notes.at(-1)!.id, + until_id: notes[notes.length - 1].id, })}` : undefined, ); @@ -510,9 +395,7 @@ export class ActivityPubServerService { return (this.apRendererService.addContext(rendered)); } else { // index page - const rendered = this.apRendererService.renderOrderedCollection( - partOf, - user.notesCount, + const rendered = this.apRendererService.renderOrderedCollection(partOf, user.notesCount, `${partOf}?page=true`, `${partOf}?page=true&since_id=000000000000000000000000`, ); @@ -523,49 +406,21 @@ export class ActivityPubServerService { } @bindThis - private async getUserNotesFromDb(untilId: string | null, sinceId: string | null, limit: number, userId: MiUser['id']) { - return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId) - .andWhere('note.userId = :userId', { userId }) - .andWhere(new Brackets(qb => { - qb - .where('note.visibility = \'public\'') - .orWhere('note.visibility = \'home\''); - })) - .andWhere('note.localOnly = FALSE') - .limit(limit) - .getMany(); - } - - @bindThis - private async userInfo(request: FastifyRequest, reply: FastifyReply, user: MiUser | null) { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - + private async userInfo(request: FastifyRequest, reply: FastifyReply, user: User | null) { if (user == null) { reply.code(404); return; } - // リモートだったらリダイレクト - if (user.host != null) { - if (user.uri == null || this.utilityService.isSelfHost(user.host)) { - reply.code(500); - return; - } - reply.redirect(user.uri, 301); - return; - } - reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser))); + return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as LocalUser))); } @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { - fastify.addConstraintStrategy({ + // addConstraintStrategy の型定義がおかしいため + (fastify.addConstraintStrategy as any)({ name: 'apOrHtml', storage() { const store = {} as any; @@ -580,33 +435,14 @@ export class ActivityPubServerService { }, deriveConstraint(request: IncomingMessage) { const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]); - if (accepted === false) return null; - return accepted !== 'html' ? 'ap' : 'html'; + const isAp = typeof accepted === 'string' && !accepted.match(/html/); + return isAp ? 'ap' : 'html'; }, }); - const almostDefaultJsonParser: FastifyBodyParser = function (request, rawBody, done) { - if (rawBody.length === 0) { - const err = new Error('Body cannot be empty!') as any; - err.statusCode = 400; - return done(err); - } - - try { - const json = secureJson.parse(rawBody.toString('utf8'), null, { - protoAction: 'ignore', - constructorAction: 'ignore', - }); - done(null, json); - } catch (err: any) { - err.statusCode = 400; - return done(err); - } - }; - fastify.register(fastifyAccepts); - fastify.addContentTypeParser('application/activity+json', { parseAs: 'buffer' }, almostDefaultJsonParser); - fastify.addContentTypeParser('application/ld+json', { parseAs: 'buffer' }, almostDefaultJsonParser); + fastify.addContentTypeParser('application/activity+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore')); + fastify.addContentTypeParser('application/ld+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore')); fastify.addHook('onRequest', (request, reply, done) => { reply.header('Access-Control-Allow-Headers', 'Accept'); @@ -618,18 +454,13 @@ export class ActivityPubServerService { //#region Routing // inbox (limit: 64kb) - fastify.post('/inbox', { config: { rawBody: true }, bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply)); - fastify.post('/users/:user/inbox', { config: { rawBody: true }, bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply)); + fastify.post('/inbox', { bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply)); + fastify.post('/users/:user/inbox', { bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply)); // note fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { vary(reply.raw, 'Accept'); - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - const note = await this.notesRepository.findOneBy({ id: request.params.note, visibility: In(['public', 'home']), @@ -660,11 +491,6 @@ export class ActivityPubServerService { fastify.get<{ Params: { note: string; } }>('/notes/:note/activity', async (request, reply) => { vary(reply.raw, 'Accept'); - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - const note = await this.notesRepository.findOneBy({ id: request.params.note, userHost: IsNull(), @@ -705,11 +531,6 @@ export class ActivityPubServerService { // publickey fastify.get<{ Params: { user: string; } }>('/users/:user/publickey', async (request, reply) => { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - const userId = request.params.user; const user = await this.usersRepository.findOneBy({ @@ -735,36 +556,21 @@ export class ActivityPubServerService { }); fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { - vary(reply.raw, 'Accept'); - - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - const userId = request.params.user; const user = await this.usersRepository.findOneBy({ id: userId, + host: IsNull(), isSuspended: false, }); return await this.userInfo(request, reply, user); }); - fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { - vary(reply.raw, 'Accept'); - - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - - const acct = Acct.parse(request.params.acct); - + fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { const user = await this.usersRepository.findOneBy({ - usernameLower: acct.username.toLowerCase(), - host: acct.host ?? IsNull(), + usernameLower: request.params.user.toLowerCase(), + host: IsNull(), isSuspended: false, }); @@ -774,11 +580,6 @@ export class ActivityPubServerService { // emoji fastify.get<{ Params: { emoji: string; } }>('/emojis/:emoji', async (request, reply) => { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - const emoji = await this.emojisRepository.findOneBy({ host: IsNull(), name: request.params.emoji, @@ -796,11 +597,6 @@ export class ActivityPubServerService { // like fastify.get<{ Params: { like: string; } }>('/likes/:like', async (request, reply) => { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - const reaction = await this.noteReactionsRepository.findOneBy({ id: request.params.like }); if (reaction == null) { @@ -822,11 +618,6 @@ export class ActivityPubServerService { // follow fastify.get<{ Params: { follower: string; followee: string; } }>('/follows/:follower/:followee', async (request, reply) => { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - // This may be used before the follow is completed, so we do not // check if the following exists. @@ -839,7 +630,7 @@ export class ActivityPubServerService { id: request.params.followee, host: Not(IsNull()), }), - ]) as [MiLocalUser | MiRemoteUser | null, MiLocalUser | MiRemoteUser | null]; + ]) as [LocalUser | RemoteUser | null, LocalUser | RemoteUser | null]; if (follower == null || followee == null) { reply.code(404); @@ -852,12 +643,7 @@ export class ActivityPubServerService { }); // follow - fastify.get<{ Params: { followRequestId: string; } }>('/follows/:followRequestId', async (request, reply) => { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - + fastify.get<{ Params: { followRequestId: string ; } }>('/follows/:followRequestId', async (request, reply) => { // This may be used before the follow is completed, so we do not // check if the following exists and only check if the follow request exists. @@ -879,7 +665,7 @@ export class ActivityPubServerService { id: followRequest.followeeId, host: Not(IsNull()), }), - ]) as [MiLocalUser | MiRemoteUser | null, MiLocalUser | MiRemoteUser | null]; + ]) as [LocalUser | RemoteUser | null, LocalUser | RemoteUser | null]; if (follower == null || followee == null) { reply.code(404); diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 772c37094c..98329ddffa 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -1,17 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import { Inject, Injectable } from '@nestjs/common'; import rename from 'rename'; -import sharp from 'sharp'; -import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; import type { Config } from '@/config.js'; -import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js'; +import type { DriveFile, DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { createTemp } from '@/misc/create-temp.js'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; @@ -25,10 +18,11 @@ import { contentDisposition } from '@/misc/content-disposition.js'; import { FileInfoService } from '@/core/FileInfoService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; -import { isMimeImage } from '@/misc/is-mime-image.js'; -import { correctFilename } from '@/misc/correct-filename.js'; -import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; +import { isMimeImage } from '@/misc/is-mime-image.js'; +import sharp from 'sharp'; +import { sharpBmp } from 'sharp-read-bmp'; +import { correctFilename } from '@/misc/correct-filename.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -53,7 +47,7 @@ export class FileServerService { private internalStorageService: InternalStorageService, private loggerService: LoggerService, ) { - this.logger = this.loggerService.getLogger('server', 'gray'); + this.logger = this.loggerService.getLogger('server', 'gray', false); //this.createServer = this.createServer.bind(this); } @@ -62,29 +56,23 @@ export class FileServerService { public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.addHook('onRequest', (request, reply, done) => { reply.header('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\''); - if (process.env.NODE_ENV === 'development') { - reply.header('Access-Control-Allow-Origin', '*'); - } done(); }); - fastify.register((fastify, options, done) => { - fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); - fastify.get('/files/app-default.jpg', (request, reply) => { - const file = fs.createReadStream(`${_dirname}/assets/dummy.png`); - reply.header('Content-Type', 'image/jpeg'); - reply.header('Cache-Control', 'max-age=31536000, immutable'); - return reply.send(file); - }); + fastify.get('/files/app-default.jpg', (request, reply) => { + const file = fs.createReadStream(`${_dirname}/assets/dummy.png`); + reply.header('Content-Type', 'image/jpeg'); + reply.header('Cache-Control', 'max-age=31536000, immutable'); + return reply.send(file); + }); - fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => { - return await this.sendDriveFile(request, reply) - .catch(err => this.errorHandler(request, reply, err)); - }); - fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => { - return await reply.redirect(`${this.config.url}/files/${request.params.key}`, 301); - }); - done(); + fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => { + return await this.sendDriveFile(request, reply) + .catch(err => this.errorHandler(request, reply, err)); + }); + fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => { + return await this.sendDriveFile(request, reply) + .catch(err => this.errorHandler(request, reply, err)); }); fastify.get<{ @@ -147,12 +135,12 @@ export class FileServerService { url.searchParams.set('static', '1'); file.cleanup(); - return await reply.redirect(url.toString(), 301); + return await reply.redirect(301, url.toString()); } else if (file.mime.startsWith('video/')) { const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url); if (externalThumbnail) { file.cleanup(); - return await reply.redirect(externalThumbnail, 301); + return await reply.redirect(301, externalThumbnail); } image = await this.videoProcessingService.generateVideoThumbnail(file.path); @@ -167,41 +155,16 @@ export class FileServerService { url.searchParams.set('url', file.url); file.cleanup(); - return await reply.redirect(url.toString(), 301); + return await reply.redirect(301, url.toString()); } } if (!image) { - if (request.headers.range && file.file.size > 0) { - const range = request.headers.range as string; - const parts = range.replace(/bytes=/, '').split('-'); - const start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; - if (end > file.file.size) { - end = file.file.size - 1; - } - const chunksize = end - start + 1; - - image = { - data: fs.createReadStream(file.path, { - start, - end, - }), - ext: file.ext, - type: file.mime, - }; - - reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); - reply.header('Accept-Ranges', 'bytes'); - reply.header('Content-Length', chunksize); - reply.code(206); - } else { - image = { - data: fs.createReadStream(file.path), - ext: file.ext, - type: file.mime, - }; - } + image = { + data: fs.createReadStream(file.path), + ext: file.ext, + type: file.mime, + }; } if ('pipe' in image.data && typeof image.data.pipe === 'function') { @@ -214,13 +177,11 @@ export class FileServerService { } reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream'); - reply.header('Content-Length', file.file.size); - reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Content-Disposition', contentDisposition( 'inline', - correctFilename(file.filename, image.ext), - ), + correctFilename(file.filename, image.ext) + ) ); return image.data; } @@ -234,54 +195,11 @@ export class FileServerService { reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream'); reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Content-Disposition', contentDisposition('inline', filename)); - - if (request.headers.range && file.file.size > 0) { - const range = request.headers.range as string; - const parts = range.replace(/bytes=/, '').split('-'); - const start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; - if (end > file.file.size) { - end = file.file.size - 1; - } - const chunksize = end - start + 1; - const fileStream = fs.createReadStream(file.path, { - start, - end, - }); - reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); - reply.header('Accept-Ranges', 'bytes'); - reply.header('Content-Length', chunksize); - reply.code(206); - return fileStream; - } - return fs.createReadStream(file.path); } else { reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream'); - reply.header('Content-Length', file.file.size); reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Content-Disposition', contentDisposition('inline', file.filename)); - - if (request.headers.range && file.file.size > 0) { - const range = request.headers.range as string; - const parts = range.replace(/bytes=/, '').split('-'); - const start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; - if (end > file.file.size) { - end = file.file.size - 1; - } - const chunksize = end - start + 1; - const fileStream = fs.createReadStream(file.path, { - start, - end, - }); - reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); - reply.header('Accept-Ranges', 'bytes'); - reply.header('Content-Length', chunksize); - reply.code(206); - return fileStream; - } - return fs.createReadStream(file.path); } } catch (e) { @@ -314,17 +232,11 @@ export class FileServerService { } return await reply.redirect( - url.toString(), 301, + url.toString(), ); } - if (!request.headers['user-agent']) { - throw new StatusError('User-Agent is required', 400, 'User-Agent is required'); - } else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) { - throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive'); - } - // Create temp file const file = await this.getStreamAndTypeFromUrl(url); if (file === '404') { @@ -366,11 +278,11 @@ export class FileServerService { }; } else { const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) })) - .resize({ - height: 'emoji' in request.query ? 128 : 320, - withoutEnlargement: true, - }) - .webp(webpDefault); + .resize({ + height: 'emoji' in request.query ? 128 : 320, + withoutEnlargement: true, + }) + .webp(webpDefault); image = { data, @@ -420,36 +332,11 @@ export class FileServerService { } if (!image) { - if (request.headers.range && file.file && file.file.size > 0) { - const range = request.headers.range as string; - const parts = range.replace(/bytes=/, '').split('-'); - const start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; - if (end > file.file.size) { - end = file.file.size - 1; - } - const chunksize = end - start + 1; - - image = { - data: fs.createReadStream(file.path, { - start, - end, - }), - ext: file.ext, - type: file.mime, - }; - - reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); - reply.header('Accept-Ranges', 'bytes'); - reply.header('Content-Length', chunksize); - reply.code(206); - } else { - image = { - data: fs.createReadStream(file.path), - ext: file.ext, - type: file.mime, - }; - } + image = { + data: fs.createReadStream(file.path), + ext: file.ext, + type: file.mime, + }; } if ('cleanup' in file) { @@ -468,8 +355,8 @@ export class FileServerService { reply.header('Content-Disposition', contentDisposition( 'inline', - correctFilename(file.filename, image.ext), - ), + correctFilename(file.filename, image.ext) + ) ); return image.data; } catch (e) { @@ -480,8 +367,8 @@ export class FileServerService { @bindThis private async getStreamAndTypeFromUrl(url: string): Promise< - { state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: MiDriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } - | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; } + { state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } + | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; mime: string; ext: string | null; path: string; } | '404' | '204' > { @@ -497,7 +384,7 @@ export class FileServerService { @bindThis private async downloadAndDetectTypeFromUrl(url: string): Promise< - { state: 'remote'; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } + { state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } > { const [path, cleanup] = await createTemp(); try { @@ -519,8 +406,8 @@ export class FileServerService { @bindThis private async getFileFromKey(key: string): Promise< - { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; } - | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; } + { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; } + | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; mime: string; ext: string | null; path: string; } | '404' | '204' > { @@ -539,7 +426,6 @@ export class FileServerService { if (!file.storedInternal) { if (!(file.isLink && file.uri)) return '204'; const result = await this.downloadAndDetectTypeFromUrl(file.uri); - file.size = (await fs.promises.stat(result.path)).size; // DB file.sizeは正確とは限らないので return { ...result, url: file.uri, diff --git a/packages/backend/src/server/HealthServerService.ts b/packages/backend/src/server/HealthServerService.ts deleted file mode 100644 index 5980609f02..0000000000 --- a/packages/backend/src/server/HealthServerService.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import { DataSource } from 'typeorm'; -import { bindThis } from '@/decorators.js'; -import { DI } from '@/di-symbols.js'; -import { readyRef } from '@/boot/ready.js'; -import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; -import type { MeiliSearch } from 'meilisearch'; - -@Injectable() -export class HealthServerService { - constructor( - @Inject(DI.redis) - private redis: Redis.Redis, - - @Inject(DI.redisForPub) - private redisForPub: Redis.Redis, - - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, - - @Inject(DI.redisForReactions) - private redisForReactions: Redis.Redis, - - @Inject(DI.db) - private db: DataSource, - - @Inject(DI.meilisearch) - private meilisearch: MeiliSearch | null, - ) {} - - @bindThis - public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { - fastify.get('/', async (request, reply) => { - reply.code(await Promise.all([ - new Promise((resolve, reject) => readyRef.value ? resolve() : reject()), - this.redis.ping(), - this.redisForPub.ping(), - this.redisForSub.ping(), - this.redisForTimelines.ping(), - this.redisForReactions.ping(), - this.db.query('SELECT 1'), - ...(this.meilisearch ? [this.meilisearch.health()] : []), - ]).then(() => 200, () => 503)); - reply.header('Cache-Control', 'no-store'); - }); - - done(); - } -} diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 239ef82dec..666a91fcee 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -1,24 +1,19 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; +import type { NotesRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { MemorySingleCache } from '@/misc/cache.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import NotesChart from '@/core/chart/charts/notes.js'; import UsersChart from '@/core/chart/charts/users.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; -import { SystemAccountService } from '@/core/SystemAccountService.js'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; const nodeinfo2_1path = '/nodeinfo/2.1'; const nodeinfo2_0path = '/nodeinfo/2.0'; -const nodeinfo_homepage = 'https://misskey-hub.net'; @Injectable() export class NodeinfoServerService { @@ -26,7 +21,13 @@ export class NodeinfoServerService { @Inject(DI.config) private config: Config, - private systemAccountService: SystemAccountService, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private userEntityService: UserEntityService, private metaService: MetaService, private notesChart: NotesChart, private usersChart: UsersChart, @@ -36,18 +37,18 @@ export class NodeinfoServerService { @bindThis public getLinks() { - return [{ + return [/* (awaiting release) { rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', - href: this.config.url + nodeinfo2_1path, - }, { - rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', - href: this.config.url + nodeinfo2_0path, - }]; + href: config.url + nodeinfo2_1path + }, */{ + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', + href: this.config.url + nodeinfo2_0path, + }]; } @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { - const nodeinfo2 = async (version: number) => { + const nodeinfo2 = async () => { const now = Date.now(); const notesChart = await this.notesChart.getChart('hour', 1, null); @@ -70,16 +71,14 @@ export class NodeinfoServerService { const activeHalfyear = null; const activeMonth = null; - const proxyAccount = await this.systemAccountService.fetch('proxy'); + const proxyAccount = meta.proxyAccountId ? await this.userEntityService.pack(meta.proxyAccountId).catch(() => null) : null; const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const document: any = { + return { software: { name: 'misskey', version: this.config.version, - homepage: nodeinfo_homepage, repository: meta.repositoryUrl, }, protocols: ['activitypub'], @@ -96,20 +95,12 @@ export class NodeinfoServerService { metadata: { nodeName: meta.name, nodeDescription: meta.description, - nodeAdmins: [{ - name: meta.maintainerName, - email: meta.maintainerEmail, - }], - // deprecated maintainer: { name: meta.maintainerName, email: meta.maintainerEmail, }, langs: meta.langs, tosUrl: meta.termsOfServiceUrl, - privacyPolicyUrl: meta.privacyPolicyUrl, - inquiryUrl: meta.inquiryUrl, - impressumUrl: meta.impressumUrl, repositoryUrl: meta.repositoryUrl, feedbackUrl: meta.feedbackUrl, disableRegistration: meta.disableRegistration, @@ -118,53 +109,30 @@ export class NodeinfoServerService { emailRequiredForSignup: meta.emailRequiredForSignup, enableHcaptcha: meta.enableHcaptcha, enableRecaptcha: meta.enableRecaptcha, - enableMcaptcha: meta.enableMcaptcha, - enableTurnstile: meta.enableTurnstile, maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, enableEmail: meta.enableEmail, enableServiceWorker: meta.enableServiceWorker, - proxyAccountName: proxyAccount.username, + proxyAccountName: proxyAccount ? proxyAccount.username : null, themeColor: meta.themeColor ?? '#86b300', }, }; - if (version >= 21) { - document.software.repository = meta.repositoryUrl; - document.software.homepage = meta.repositoryUrl; - } - return document; }; - const cache = new MemorySingleCache>>(1000 * 60 * 10); // 10m + const cache = new MemorySingleCache>>(1000 * 60 * 10); fastify.get(nodeinfo2_1path, async (request, reply) => { - const base = await cache.fetch(() => nodeinfo2(21)); + const base = await cache.fetch(() => nodeinfo2()); - reply - .type( - 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"', - ) - .header('Cache-Control', 'public, max-age=600') - .header('Access-Control-Allow-Headers', 'Accept') - .header('Access-Control-Allow-Methods', 'GET, OPTIONS') - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Expose-Headers', 'Vary'); + reply.header('Cache-Control', 'public, max-age=600'); return { version: '2.1', ...base }; }); fastify.get(nodeinfo2_0path, async (request, reply) => { - const base = await cache.fetch(() => nodeinfo2(20)); + const base = await cache.fetch(() => nodeinfo2()); delete (base as any).software.repository; - reply - .type( - 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"', - ) - .header('Cache-Control', 'public, max-age=600') - .header('Access-Control-Allow-Headers', 'Accept') - .header('Access-Control-Allow-Methods', 'GET, OPTIONS') - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Expose-Headers', 'Vary'); + reply.header('Cache-Control', 'public, max-age=600'); return { version: '2.0', ...base }; }); diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index 0223650329..5d80eb4e0c 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -1,14 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Module } from '@nestjs/common'; import { EndpointsModule } from '@/server/api/EndpointsModule.js'; import { CoreModule } from '@/core/CoreModule.js'; import { ApiCallService } from './api/ApiCallService.js'; import { FileServerService } from './FileServerService.js'; -import { HealthServerService } from './HealthServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js'; import { ServerService } from './ServerService.js'; import { WellKnownServerService } from './WellKnownServerService.js'; @@ -23,13 +17,9 @@ import { SigninApiService } from './api/SigninApiService.js'; import { SigninService } from './api/SigninService.js'; import { SignupApiService } from './api/SignupApiService.js'; import { StreamingApiServerService } from './api/StreamingApiServerService.js'; -import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { ClientServerService } from './web/ClientServerService.js'; import { FeedService } from './web/FeedService.js'; import { UrlPreviewService } from './web/UrlPreviewService.js'; -import { ClientLoggerService } from './web/ClientLoggerService.js'; -import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; - import { MainChannelService } from './api/stream/channels/main.js'; import { AdminChannelService } from './api/stream/channels/admin.js'; import { AntennaChannelService } from './api/stream/channels/antenna.js'; @@ -43,12 +33,10 @@ import { LocalTimelineChannelService } from './api/stream/channels/local-timelin import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js'; import { ServerStatsChannelService } from './api/stream/channels/server-stats.js'; import { UserListChannelService } from './api/stream/channels/user-list.js'; +import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; +import { ClientLoggerService } from './web/ClientLoggerService.js'; import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js'; -import { ChatUserChannelService } from './api/stream/channels/chat-user.js'; -import { ChatRoomChannelService } from './api/stream/channels/chat-room.js'; -import { ReversiChannelService } from './api/stream/channels/reversi.js'; -import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js'; -import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js'; +import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; @Module({ imports: [ @@ -59,7 +47,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j ClientServerService, ClientLoggerService, FeedService, - HealthServerService, UrlPreviewService, ActivityPubServerService, FileServerService, @@ -74,7 +61,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j AuthenticateService, RateLimiterService, SigninApiService, - SigninWithPasskeyApiService, SigninService, SignupApiService, StreamingApiServerService, @@ -86,10 +72,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j GlobalTimelineChannelService, HashtagChannelService, RoleTimelineChannelService, - ChatUserChannelService, - ChatRoomChannelService, - ReversiChannelService, - ReversiGameChannelService, HomeTimelineChannelService, HybridTimelineChannelService, LocalTimelineChannelService, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 23c085ee27..a98df389e1 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -1,23 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import cluster from 'node:cluster'; import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import Fastify, { type FastifyInstance } from 'fastify'; +import Fastify, { FastifyInstance } from 'fastify'; import fastifyStatic from '@fastify/static'; -import fastifyRawBody from 'fastify-raw-body'; import { IsNull } from 'typeorm'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { Config } from '@/config.js'; -import type { EmojisRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { genIdenticon } from '@/misc/gen-identicon.js'; +import { createTemp } from '@/misc/create-temp.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; @@ -27,7 +22,6 @@ import { ApiServerService } from './api/ApiServerService.js'; import { StreamingApiServerService } from './api/StreamingApiServerService.js'; import { WellKnownServerService } from './WellKnownServerService.js'; import { FileServerService } from './FileServerService.js'; -import { HealthServerService } from './HealthServerService.js'; import { ClientServerService } from './web/ClientServerService.js'; import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; @@ -43,9 +37,6 @@ export class ServerService implements OnApplicationShutdown { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -63,20 +54,19 @@ export class ServerService implements OnApplicationShutdown { private wellKnownServerService: WellKnownServerService, private nodeinfoServerService: NodeinfoServerService, private fileServerService: FileServerService, - private healthServerService: HealthServerService, private clientServerService: ClientServerService, private globalEventService: GlobalEventService, private loggerService: LoggerService, private oauth2ProviderService: OAuth2ProviderService, ) { - this.logger = this.loggerService.getLogger('server', 'gray'); + this.logger = this.loggerService.getLogger('server', 'gray', false); } @bindThis public async launch(): Promise { const fastify = Fastify({ trustProxy: true, - logger: false, + logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''), }); this.#fastify = fastify; @@ -89,13 +79,6 @@ export class ServerService implements OnApplicationShutdown { }); } - // Register raw-body parser for ActivityPub HTTP signature validation. - await fastify.register(fastifyRawBody, { - global: false, - encoding: null, - runFirst: true, - }); - // Register non-serving static server so that the child services can use reply.sendFile. // `root` here is just a placeholder and each call must use its own `rootPath`. fastify.register(fastifyStatic, { @@ -103,52 +86,13 @@ export class ServerService implements OnApplicationShutdown { serve: false, }); - // if the requester looks like to be performing an ActivityPub object lookup, reject all external redirects - // - // this will break lookup that involve copying a URL from a third-party server, like trying to lookup http://charlie.example.com/@alice@alice.com - // - // this is not required by standard but protect us from peers that did not validate final URL. - if (!this.meta.allowExternalApRedirect) { - const maybeApLookupRegex = /application\/activity\+json|application\/ld\+json.+activitystreams/i; - fastify.addHook('onSend', (request, reply, _, done) => { - const location = reply.getHeader('location'); - if (reply.statusCode < 300 || reply.statusCode >= 400 || typeof location !== 'string') { - done(); - return; - } - - if (!maybeApLookupRegex.test(request.headers.accept ?? '')) { - done(); - return; - } - - const effectiveLocation = process.env.NODE_ENV === 'production' ? location : location.replace(/^http:\/\//, 'https://'); - if (effectiveLocation.startsWith(`https://${this.config.host}/`)) { - done(); - return; - } - - reply.status(406); - reply.removeHeader('location'); - reply.header('content-type', 'text/plain; charset=utf-8'); - reply.header('link', `<${encodeURI(location)}>; rel="canonical"`); - done(null, [ - 'Refusing to relay remote ActivityPub object lookup.', - '', - `Please remove 'application/activity+json' and 'application/ld+json' from the Accept header or fetch using the authoritative URL at ${location}.`, - ].join('\n')); - }); - } - fastify.register(this.apiServerService.createServer, { prefix: '/api' }); fastify.register(this.openApiServerService.createServer); fastify.register(this.fileServerService.createServer); fastify.register(this.activityPubServerService.createServer); fastify.register(this.nodeinfoServerService.createServer); fastify.register(this.wellKnownServerService.createServer); - fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' }); - fastify.register(this.oauth2ProviderService.createTokenServer, { prefix: '/oauth/token' }); - fastify.register(this.healthServerService.createServer, { prefix: '/healthz' }); + fastify.register(this.oauth2ProviderService.createServer); fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { const path = request.params.path; @@ -160,20 +104,12 @@ export class ServerService implements OnApplicationShutdown { return; } - const emojiPath = path.replace(/\.webp$/i, ''); - const pathChunks = emojiPath.split('@'); - - if (pathChunks.length > 2) { - reply.code(400); - return; - } - - const name = pathChunks.shift(); - const host = pathChunks.pop(); + const name = path.split('@')[0].replace('.webp', ''); + const host = path.split('@')[1]?.replace('.webp', ''); const emoji = await this.emojisRepository.findOneBy({ // `@.` is the spec of ReactionService.decodeReaction - host: (host === undefined || host === '.') ? IsNull() : host, + host: (host == null || host === '.') ? IsNull() : host, name: name, }); @@ -203,8 +139,8 @@ export class ServerService implements OnApplicationShutdown { } return await reply.redirect( - url.toString(), 301, + url.toString(), ); }); @@ -221,21 +157,18 @@ export class ServerService implements OnApplicationShutdown { reply.header('Cache-Control', 'public, max-age=86400'); if (user) { - reply.redirect((user.avatarId == null ? null : user.avatarUrl) ?? this.userEntityService.getIdenticonUrl(user)); + reply.redirect(user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user)); } else { reply.redirect('/static-assets/user-unknown.png'); } }); fastify.get<{ Params: { x: string } }>('/identicon/:x', async (request, reply) => { + const [temp, cleanup] = await createTemp(); + await genIdenticon(request.params.x, fs.createWriteStream(temp)); reply.header('Content-Type', 'image/png'); reply.header('Cache-Control', 'public, max-age=86400'); - - if (this.meta.enableIdenticonGeneration) { - return await genIdenticon(request.params.x); - } else { - return reply.redirect('/static-assets/avatar.png'); - } + return fs.createReadStream(temp).on('close', () => cleanup()); }); fastify.get<{ Params: { code: string } }>('/verify-email/:code', async (request, reply) => { @@ -250,14 +183,14 @@ export class ServerService implements OnApplicationShutdown { }); this.globalEventService.publishMainStream(profile.userId, 'meUpdated', await this.userEntityService.pack(profile.userId, { id: profile.userId }, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, })); - reply.code(200).send('Verification succeeded! メールアドレスの認証に成功しました。'); - return; + reply.code(200); + return 'Verify succeeded!'; } else { - reply.code(404).send('Verification failed. Please try again. メールアドレスの認証に失敗しました。もう一度お試しください'); + reply.code(404); return; } }); @@ -287,35 +220,17 @@ export class ServerService implements OnApplicationShutdown { } }); - if (this.config.socket) { - if (fs.existsSync(this.config.socket)) { - fs.unlinkSync(this.config.socket); - } - fastify.listen({ path: this.config.socket }, (err, address) => { - if (this.config.chmodSocket) { - fs.chmodSync(this.config.socket!, this.config.chmodSocket); - } - }); - } else { - fastify.listen({ port: this.config.port, host: '0.0.0.0' }); - } + fastify.listen({ port: this.config.port, host: '0.0.0.0' }); await fastify.ready(); } @bindThis public async dispose(): Promise { - await this.streamingApiServerService.detach(); + await this.streamingApiServerService.detach(); await this.#fastify.close(); } - /** - * Get the Fastify instance for testing. - */ - public get fastify(): FastifyInstance { - return this.#fastify; - } - @bindThis async onApplicationShutdown(signal: string): Promise { await this.dispose(); diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts index ebfd1a421d..9bf8deb221 100644 --- a/packages/backend/src/server/WellKnownServerService.ts +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -1,24 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import vary from 'vary'; -import fastifyAccepts from '@fastify/accepts'; import { DI } from '@/di-symbols.js'; -import type { MiMeta, UsersRepository } from '@/models/_.js'; +import type { UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js'; -import type { MiUser } from '@/models/User.js'; +import type { User } from '@/models/entities/User.js'; import * as Acct from '@/misc/acct.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { bindThis } from '@/decorators.js'; import { NodeinfoServerService } from './NodeinfoServerService.js'; -import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { FindOptionsWhere } from 'typeorm'; +import { bindThis } from '@/decorators.js'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; +import fastifyAccepts from '@fastify/accepts'; @Injectable() export class WellKnownServerService { @@ -26,15 +20,11 @@ export class WellKnownServerService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, private nodeinfoServerService: NodeinfoServerService, private userEntityService: UserEntityService, - private oauth2ProviderService: OAuth2ProviderService, ) { //this.createServer = this.createServer.bind(this); } @@ -69,11 +59,6 @@ export class WellKnownServerService { }); fastify.get('/.well-known/host-meta', async (request, reply) => { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - reply.header('Content-Type', xrd); return XRD({ element: 'Link', attributes: { rel: 'lrdd', @@ -83,12 +68,7 @@ export class WellKnownServerService { }); fastify.get('/.well-known/host-meta.json', async (request, reply) => { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - - reply.header('Content-Type', 'application/json'); + reply.header('Content-Type', jrd); return { links: [{ rel: 'lrdd', @@ -99,36 +79,22 @@ export class WellKnownServerService { }); fastify.get('/.well-known/nodeinfo', async (request, reply) => { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - return { links: this.nodeinfoServerService.getLinks() }; }); - fastify.get('/.well-known/oauth-authorization-server', async () => { - return this.oauth2ProviderService.generateRFC8414(); - }); - /* TODO fastify.get('/.well-known/change-password', async (request, reply) => { }); */ fastify.get<{ Querystring: { resource: string } }>(webFingerPath, async (request, reply) => { - if (this.meta.federation === 'none') { - reply.code(403); - return; - } - - const fromId = (id: MiUser['id']): FindOptionsWhere => ({ + const fromId = (id: User['id']): FindOptionsWhere => ({ id, host: IsNull(), isSuspended: false, }); - const generateQuery = (resource: string): FindOptionsWhere | number => + const generateQuery = (resource: string): FindOptionsWhere | number => resource.startsWith(`${this.config.url.toLowerCase()}/users/`) ? fromId(resource.split('/').pop()!) : fromAcct(Acct.parse( @@ -136,9 +102,9 @@ fastify.get('/.well-known/change-password', async (request, reply) => { resource.startsWith('acct:') ? resource.slice('acct:'.length) : resource)); - const fromAcct = (acct: Acct.Acct): FindOptionsWhere | number => + const fromAcct = (acct: Acct.Acct): FindOptionsWhere | number => !acct.host || acct.host === this.config.host.toLowerCase() ? { - usernameLower: acct.username.toLowerCase(), + usernameLower: acct.username, host: IsNull(), isSuspended: false, } : 422; diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 7a4af407a3..09e3724394 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -1,23 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { randomUUID } from 'node:crypto'; +import { pipeline } from 'node:stream'; import * as fs from 'node:fs'; -import * as stream from 'node:stream/promises'; +import { promisify } from 'node:util'; import { Inject, Injectable } from '@nestjs/common'; -import * as Sentry from '@sentry/node'; +import { v4 as uuid } from 'uuid'; import { DI } from '@/di-symbols.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; -import type { MiLocalUser, MiUser } from '@/models/User.js'; -import type { MiAccessToken } from '@/models/AccessToken.js'; +import type { LocalUser, User } from '@/models/entities/User.js'; +import type { AccessToken } from '@/models/entities/AccessToken.js'; import type Logger from '@/logger.js'; -import type { MiMeta, UserIpsRepository } from '@/models/_.js'; +import type { UserIpsRepository } from '@/models/index.js'; +import { MetaService } from '@/core/MetaService.js'; import { createTemp } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import type { Config } from '@/config.js'; import { ApiError } from './error.js'; import { RateLimiterService } from './RateLimiterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; @@ -26,6 +21,8 @@ import type { FastifyRequest, FastifyReply } from 'fastify'; import type { OnApplicationShutdown } from '@nestjs/common'; import type { IEndpointMeta, IEndpoint } from './endpoints.js'; +const pump = promisify(pipeline); + const accessDenied = { message: 'Access denied.', code: 'ACCESS_DENIED', @@ -35,26 +32,21 @@ const accessDenied = { @Injectable() export class ApiCallService implements OnApplicationShutdown { private logger: Logger; - private userIpHistories: Map>; - private userIpHistoriesClearIntervalId: NodeJS.Timeout; + private userIpHistories: Map>; + private userIpHistoriesClearIntervalId: NodeJS.Timer; constructor( - @Inject(DI.meta) - private meta: MiMeta, - - @Inject(DI.config) - private config: Config, - @Inject(DI.userIpsRepository) private userIpsRepository: UserIpsRepository, + private metaService: MetaService, private authenticateService: AuthenticateService, private rateLimiterService: RateLimiterService, private roleService: RoleService, private apiLoggerService: ApiLoggerService, ) { this.logger = this.apiLoggerService.logger; - this.userIpHistories = new Map>(); + this.userIpHistories = new Map>(); this.userIpHistoriesClearIntervalId = setInterval(() => { this.userIpHistories.clear(); @@ -65,16 +57,6 @@ export class ApiCallService implements OnApplicationShutdown { let statusCode = err.httpStatusCode; if (err.httpStatusCode === 401) { reply.header('WWW-Authenticate', 'Bearer realm="Misskey"'); - } else if (err.code === 'RATE_LIMIT_EXCEEDED') { - const info: unknown = err.info; - const unixEpochInSeconds = Date.now(); - if (typeof(info) === 'object' && info && 'resetMs' in info && typeof(info.resetMs) === 'number') { - const cooldownInSeconds = Math.ceil((info.resetMs - unixEpochInSeconds) / 1000); - // もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく - reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10)); - } else { - this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`); - } } else if (err.kind === 'client') { reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`); statusCode = statusCode ?? 400; @@ -104,51 +86,6 @@ export class ApiCallService implements OnApplicationShutdown { } } - #onExecError(ep: IEndpoint, data: any, err: Error, userId?: MiUser['id']): void { - if (err instanceof ApiError || err instanceof AuthenticationError) { - throw err; - } else { - const errId = randomUUID(); - this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, { - ep: ep.name, - ps: data, - e: { - message: err.message, - code: err.name, - stack: err.stack, - id: errId, - }, - }); - - if (this.config.sentryForBackend) { - Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, { - level: 'error', - user: { - id: userId, - }, - extra: { - ep: ep.name, - ps: data, - e: { - message: err.message, - code: err.name, - stack: err.stack, - id: errId, - }, - }, - }); - } - - throw new ApiError(null, { - e: { - message: err.message, - code: err.name, - id: errId, - }, - }); - } - } - @bindThis public handleRequest( endpoint: IEndpoint & { exec: any }, @@ -200,17 +137,8 @@ export class ApiCallService implements OnApplicationShutdown { return; } - const [path, cleanup] = await createTemp(); - await stream.pipeline(multipartData.file, fs.createWriteStream(path)); - - // ファイルサイズが制限を超えていた場合 - // なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある - if (multipartData.file.truncated) { - cleanup(); - reply.code(413); - reply.send(); - return; - } + const [path] = await createTemp(); + await pump(multipartData.file, fs.createWriteStream(path)); const fields = {} as Record; for (const [k, v] of Object.entries(multipartData.fields)) { @@ -266,8 +194,9 @@ export class ApiCallService implements OnApplicationShutdown { } @bindThis - private logIp(request: FastifyRequest, user: MiLocalUser) { - if (!this.meta.enableIpLogging) return; + private async logIp(request: FastifyRequest, user: LocalUser) { + const meta = await this.metaService.fetch(); + if (!meta.enableIpLogging) return; const ip = request.ip; const ips = this.userIpHistories.get(user.id); if (ips == null || !ips.has(ip)) { @@ -291,8 +220,8 @@ export class ApiCallService implements OnApplicationShutdown { @bindThis private async call( ep: IEndpoint & { exec: any }, - user: MiLocalUser | null | undefined, - token: MiAccessToken | null | undefined, + user: LocalUser | null | undefined, + token: AccessToken | null | undefined, data: any, file: { name: string; @@ -326,15 +255,14 @@ export class ApiCallService implements OnApplicationShutdown { if (factor > 0) { // Rate limit - const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor); - if (rateLimit != null) { + await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor).catch(err => { throw new ApiError({ message: 'Rate limit exceeded. Please try again later.', code: 'RATE_LIMIT_EXCEEDED', id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', httpStatusCode: 429, - }, rateLimit.info); - } + }); + }); } } @@ -367,7 +295,7 @@ export class ApiCallService implements OnApplicationShutdown { } } - if ((ep.meta.requireModerator || ep.meta.requireAdmin) && (this.meta.rootUserId !== user!.id)) { + if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) { const myRoles = await this.roleService.getUserRoles(user!.id); if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) { throw new ApiError({ @@ -387,10 +315,9 @@ export class ApiCallService implements OnApplicationShutdown { } } - if (ep.meta.requiredRolePolicy != null && (this.meta.rootUserId !== user!.id)) { - const myRoles = await this.roleService.getUserRoles(user!.id); + if (ep.meta.requireRolePolicy != null && !user!.isRoot) { const policies = await this.roleService.getUserPolicies(user!.id); - if (!policies[ep.meta.requiredRolePolicy] && !myRoles.some(r => r.isAdministrator)) { + if (!policies[ep.meta.requireRolePolicy]) { throw new ApiError({ message: 'You are not assigned to a required role.', code: 'ROLE_PERMISSION_DENIED', @@ -400,8 +327,7 @@ export class ApiCallService implements OnApplicationShutdown { } } - if (token && ((ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) - || (!ep.meta.kind && (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin)))) { + if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) { throw new ApiError({ message: 'Your app does not have the necessary permissions to use this endpoint.', code: 'PERMISSION_DENIED', @@ -432,15 +358,31 @@ export class ApiCallService implements OnApplicationShutdown { } // API invoking - if (this.config.sentryForBackend) { - return await Sentry.startSpan({ - name: 'API: ' + ep.name, - }, () => ep.exec(data, user, token, file, request.ip, request.headers) - .catch((err: Error) => this.#onExecError(ep, data, err, user?.id))); - } else { - return await ep.exec(data, user, token, file, request.ip, request.headers) - .catch((err: Error) => this.#onExecError(ep, data, err, user?.id)); - } + return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => { + if (err instanceof ApiError || err instanceof AuthenticationError) { + throw err; + } else { + const errId = uuid(); + this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, { + ep: ep.name, + ps: data, + e: { + message: err.message, + code: err.name, + stack: err.stack, + id: errId, + }, + }); + console.error(err, errId); + throw new ApiError(null, { + e: { + message: err.message, + code: err.name, + id: errId, + }, + }); + } + }); } @bindThis diff --git a/packages/backend/src/server/api/ApiLoggerService.ts b/packages/backend/src/server/api/ApiLoggerService.ts index 72b71c0b5c..7f534b1efd 100644 --- a/packages/backend/src/server/api/ApiLoggerService.ts +++ b/packages/backend/src/server/api/ApiLoggerService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index 32818003ad..d3b1c7786d 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -1,15 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import cors from '@fastify/cors'; import multipart from '@fastify/multipart'; +import fastifyCookie from '@fastify/cookie'; import { ModuleRef } from '@nestjs/core'; -import { AuthenticationResponseJSON } from '@simplewebauthn/types'; import type { Config } from '@/config.js'; -import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js'; +import type { UsersRepository, InstancesRepository, AccessTokensRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -17,7 +12,6 @@ import endpoints from './endpoints.js'; import { ApiCallService } from './ApiCallService.js'; import { SignupApiService } from './SignupApiService.js'; import { SigninApiService } from './SigninApiService.js'; -import { SigninWithPasskeyApiService } from './SigninWithPasskeyApiService.js'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; @Injectable() @@ -28,6 +22,9 @@ export class ApiServerService { @Inject(DI.config) private config: Config, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, @@ -38,7 +35,6 @@ export class ApiServerService { private apiCallService: ApiCallService, private signupApiService: SignupApiService, private signinApiService: SigninApiService, - private signinWithPasskeyApiService: SigninWithPasskeyApiService, ) { //this.createServer = this.createServer.bind(this); } @@ -51,11 +47,13 @@ export class ApiServerService { fastify.register(multipart, { limits: { - fileSize: this.config.maxFileSize, + fileSize: this.config.maxFileSize ?? 262144000, files: 1, }, }); + fastify.register(fastifyCookie, {}); + // Prevent cache fastify.addHook('onRequest', (request, reply, done) => { reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); @@ -115,31 +113,21 @@ export class ApiServerService { 'hcaptcha-response'?: string; 'g-recaptcha-response'?: string; 'turnstile-response'?: string; - 'm-captcha-response'?: string; - 'testcaptcha-response'?: string; } }>('/signup', (request, reply) => this.signupApiService.signup(request, reply)); fastify.post<{ Body: { username: string; - password?: string; + password: string; token?: string; - credential?: AuthenticationResponseJSON; - 'hcaptcha-response'?: string; - 'g-recaptcha-response'?: string; - 'turnstile-response'?: string; - 'm-captcha-response'?: string; - 'testcaptcha-response'?: string; + signature?: string; + authenticatorData?: string; + clientDataJSON?: string; + credentialId?: string; + challengeId?: string; }; - }>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply)); - - fastify.post<{ - Body: { - credential?: AuthenticationResponseJSON; - context?: string; - }; - }>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply)); + }>('/signin', (request, reply) => this.signinApiService.signin(request, reply)); fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply)); @@ -147,7 +135,7 @@ export class ApiServerService { const instances = await this.instancesRepository.find({ select: ['host'], where: { - suspensionState: 'none', + isSuspended: false, }, }); @@ -167,7 +155,7 @@ export class ApiServerService { return { ok: true, token: token.token, - user: await this.userEntityService.pack(token.userId, null, { schema: 'UserDetailedNotMe' }), + user: await this.userEntityService.pack(token.userId, null, { detail: true }), }; } else { return { diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index 601618553e..4ad0197d87 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -1,17 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/_.js'; -import type { MiLocalUser } from '@/models/User.js'; -import type { MiAccessToken } from '@/models/AccessToken.js'; +import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js'; +import type { LocalUser } from '@/models/entities/User.js'; +import type { AccessToken } from '@/models/entities/AccessToken.js'; import { MemoryKVCache } from '@/misc/cache.js'; -import type { MiApp } from '@/models/App.js'; +import type { App } from '@/models/entities/App.js'; import { CacheService } from '@/core/CacheService.js'; -import { isNativeUserToken } from '@/misc/token.js'; +import isNativeToken from '@/misc/is-native-token.js'; import { bindThis } from '@/decorators.js'; export class AuthenticationError extends Error { @@ -23,7 +18,7 @@ export class AuthenticationError extends Error { @Injectable() export class AuthenticateService implements OnApplicationShutdown { - private appCache: MemoryKVCache; + private appCache: MemoryKVCache; constructor( @Inject(DI.usersRepository) @@ -37,23 +32,23 @@ export class AuthenticateService implements OnApplicationShutdown { private cacheService: CacheService, ) { - this.appCache = new MemoryKVCache(1000 * 60 * 60 * 24 * 7); // 1w + this.appCache = new MemoryKVCache(Infinity); } @bindThis - public async authenticate(token: string | null | undefined): Promise<[MiLocalUser | null, MiAccessToken | null]> { + public async authenticate(token: string | null | undefined): Promise<[LocalUser | null, AccessToken | null]> { if (token == null) { return [null, null]; } - - if (isNativeUserToken(token)) { + + if (isNativeToken(token)) { const user = await this.cacheService.localUserByNativeTokenCache.fetch(token, - () => this.usersRepository.findOneBy({ token }) as Promise); - + () => this.usersRepository.findOneBy({ token }) as Promise); + if (user == null) { throw new AuthenticationError('user not found'); } - + return [user, null]; } else { const accessToken = await this.accessTokensRepository.findOne({ @@ -63,28 +58,28 @@ export class AuthenticateService implements OnApplicationShutdown { token: token, // miauth }], }); - + if (accessToken == null) { throw new AuthenticationError('invalid signature'); } - + this.accessTokensRepository.update(accessToken.id, { lastUsedAt: new Date(), }); - + const user = await this.cacheService.localUserByIdCache.fetch(accessToken.userId, () => this.usersRepository.findOneBy({ id: accessToken.userId, - }) as Promise); - + }) as Promise); + if (accessToken.appId) { const app = await this.appCache.fetch(accessToken.appId, () => this.appsRepository.findOneByOrFail({ id: accessToken.appId! })); - + return [user, { id: accessToken.id, permission: app.permission, - } as MiAccessToken]; + } as AccessToken]; } else { return [user, accessToken]; } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 9cfb2f0ac0..d1ff3fe925 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -1,18 +1,682 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; -import * as endpointsObject from './endpoint-list.js'; +import * as ep___admin_meta from './endpoints/admin/meta.js'; +import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js'; +import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js'; +import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js'; +import * as ep___admin_ad_create from './endpoints/admin/ad/create.js'; +import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js'; +import * as ep___admin_ad_list from './endpoints/admin/ad/list.js'; +import * as ep___admin_ad_update from './endpoints/admin/ad/update.js'; +import * as ep___admin_announcements_create from './endpoints/admin/announcements/create.js'; +import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js'; +import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js'; +import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js'; +import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; +import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; +import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; +import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; +import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js'; +import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js'; +import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js'; +import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js'; +import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js'; +import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js'; +import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js'; +import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js'; +import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js'; +import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js'; +import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js'; +import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js'; +import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js'; +import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js'; +import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js'; +import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; +import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js'; +import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js'; +import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; +import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; +import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js'; +import * as ep___invite from './endpoints/invite.js'; +import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; +import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; +import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; +import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js'; +import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.js'; +import * as ep___admin_queue_stats from './endpoints/admin/queue/stats.js'; +import * as ep___admin_relays_add from './endpoints/admin/relays/add.js'; +import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; +import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js'; +import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js'; +import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js'; +import * as ep___admin_sendEmail from './endpoints/admin/send-email.js'; +import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; +import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; +import * as ep___admin_showUser from './endpoints/admin/show-user.js'; +import * as ep___admin_showUsers from './endpoints/admin/show-users.js'; +import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js'; +import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; +import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; +import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; +import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js'; +import * as ep___admin_roles_create from './endpoints/admin/roles/create.js'; +import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js'; +import * as ep___admin_roles_list from './endpoints/admin/roles/list.js'; +import * as ep___admin_roles_show from './endpoints/admin/roles/show.js'; +import * as ep___admin_roles_update from './endpoints/admin/roles/update.js'; +import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; +import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; +import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js'; +import * as ep___admin_roles_users from './endpoints/admin/roles/users.js'; +import * as ep___announcements from './endpoints/announcements.js'; +import * as ep___antennas_create from './endpoints/antennas/create.js'; +import * as ep___antennas_delete from './endpoints/antennas/delete.js'; +import * as ep___antennas_list from './endpoints/antennas/list.js'; +import * as ep___antennas_notes from './endpoints/antennas/notes.js'; +import * as ep___antennas_show from './endpoints/antennas/show.js'; +import * as ep___antennas_update from './endpoints/antennas/update.js'; +import * as ep___ap_get from './endpoints/ap/get.js'; +import * as ep___ap_show from './endpoints/ap/show.js'; +import * as ep___app_create from './endpoints/app/create.js'; +import * as ep___app_show from './endpoints/app/show.js'; +import * as ep___auth_accept from './endpoints/auth/accept.js'; +import * as ep___auth_session_generate from './endpoints/auth/session/generate.js'; +import * as ep___auth_session_show from './endpoints/auth/session/show.js'; +import * as ep___auth_session_userkey from './endpoints/auth/session/userkey.js'; +import * as ep___blocking_create from './endpoints/blocking/create.js'; +import * as ep___blocking_delete from './endpoints/blocking/delete.js'; +import * as ep___blocking_list from './endpoints/blocking/list.js'; +import * as ep___channels_create from './endpoints/channels/create.js'; +import * as ep___channels_featured from './endpoints/channels/featured.js'; +import * as ep___channels_follow from './endpoints/channels/follow.js'; +import * as ep___channels_followed from './endpoints/channels/followed.js'; +import * as ep___channels_owned from './endpoints/channels/owned.js'; +import * as ep___channels_show from './endpoints/channels/show.js'; +import * as ep___channels_timeline from './endpoints/channels/timeline.js'; +import * as ep___channels_unfollow from './endpoints/channels/unfollow.js'; +import * as ep___channels_update from './endpoints/channels/update.js'; +import * as ep___channels_favorite from './endpoints/channels/favorite.js'; +import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js'; +import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js'; +import * as ep___channels_search from './endpoints/channels/search.js'; +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_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'; +import * as ep___charts_user_following from './endpoints/charts/user/following.js'; +import * as ep___charts_user_notes from './endpoints/charts/user/notes.js'; +import * as ep___charts_user_pv from './endpoints/charts/user/pv.js'; +import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js'; +import * as ep___charts_users from './endpoints/charts/users.js'; +import * as ep___clips_addNote from './endpoints/clips/add-note.js'; +import * as ep___clips_removeNote from './endpoints/clips/remove-note.js'; +import * as ep___clips_create from './endpoints/clips/create.js'; +import * as ep___clips_delete from './endpoints/clips/delete.js'; +import * as ep___clips_list from './endpoints/clips/list.js'; +import * as ep___clips_notes from './endpoints/clips/notes.js'; +import * as ep___clips_show from './endpoints/clips/show.js'; +import * as ep___clips_update from './endpoints/clips/update.js'; +import * as ep___clips_favorite from './endpoints/clips/favorite.js'; +import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js'; +import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js'; +import * as ep___drive from './endpoints/drive.js'; +import * as ep___drive_files from './endpoints/drive/files.js'; +import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js'; +import * as ep___drive_files_checkExistence from './endpoints/drive/files/check-existence.js'; +import * as ep___drive_files_create from './endpoints/drive/files/create.js'; +import * as ep___drive_files_delete from './endpoints/drive/files/delete.js'; +import * as ep___drive_files_findByHash from './endpoints/drive/files/find-by-hash.js'; +import * as ep___drive_files_find from './endpoints/drive/files/find.js'; +import * as ep___drive_files_show from './endpoints/drive/files/show.js'; +import * as ep___drive_files_update from './endpoints/drive/files/update.js'; +import * as ep___drive_files_uploadFromUrl from './endpoints/drive/files/upload-from-url.js'; +import * as ep___drive_folders from './endpoints/drive/folders.js'; +import * as ep___drive_folders_create from './endpoints/drive/folders/create.js'; +import * as ep___drive_folders_delete from './endpoints/drive/folders/delete.js'; +import * as ep___drive_folders_find from './endpoints/drive/folders/find.js'; +import * as ep___drive_folders_show from './endpoints/drive/folders/show.js'; +import * as ep___drive_folders_update from './endpoints/drive/folders/update.js'; +import * as ep___drive_stream from './endpoints/drive/stream.js'; +import * as ep___emailAddress_available from './endpoints/email-address/available.js'; +import * as ep___endpoint from './endpoints/endpoint.js'; +import * as ep___endpoints from './endpoints/endpoints.js'; +import * as ep___exportCustomEmojis from './endpoints/export-custom-emojis.js'; +import * as ep___federation_followers from './endpoints/federation/followers.js'; +import * as ep___federation_following from './endpoints/federation/following.js'; +import * as ep___federation_instances from './endpoints/federation/instances.js'; +import * as ep___federation_showInstance from './endpoints/federation/show-instance.js'; +import * as ep___federation_updateRemoteUser from './endpoints/federation/update-remote-user.js'; +import * as ep___federation_users from './endpoints/federation/users.js'; +import * as ep___federation_stats from './endpoints/federation/stats.js'; +import * as ep___following_create from './endpoints/following/create.js'; +import * as ep___following_delete from './endpoints/following/delete.js'; +import * as ep___following_invalidate from './endpoints/following/invalidate.js'; +import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; +import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; +import * as ep___following_requests_list from './endpoints/following/requests/list.js'; +import * as ep___following_requests_reject from './endpoints/following/requests/reject.js'; +import * as ep___gallery_featured from './endpoints/gallery/featured.js'; +import * as ep___gallery_popular from './endpoints/gallery/popular.js'; +import * as ep___gallery_posts from './endpoints/gallery/posts.js'; +import * as ep___gallery_posts_create from './endpoints/gallery/posts/create.js'; +import * as ep___gallery_posts_delete from './endpoints/gallery/posts/delete.js'; +import * as ep___gallery_posts_like from './endpoints/gallery/posts/like.js'; +import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js'; +import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js'; +import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js'; +import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js'; +import * as ep___hashtags_list from './endpoints/hashtags/list.js'; +import * as ep___hashtags_search from './endpoints/hashtags/search.js'; +import * as ep___hashtags_show from './endpoints/hashtags/show.js'; +import * as ep___hashtags_trend from './endpoints/hashtags/trend.js'; +import * as ep___hashtags_users from './endpoints/hashtags/users.js'; +import * as ep___i from './endpoints/i.js'; +import * as ep___i_2fa_done from './endpoints/i/2fa/done.js'; +import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js'; +import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js'; +import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js'; +import * as ep___i_2fa_register from './endpoints/i/2fa/register.js'; +import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js'; +import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js'; +import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js'; +import * as ep___i_apps from './endpoints/i/apps.js'; +import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js'; +import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js'; +import * as ep___i_changePassword from './endpoints/i/change-password.js'; +import * as ep___i_deleteAccount from './endpoints/i/delete-account.js'; +import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; +import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; +import * as ep___i_exportMute from './endpoints/i/export-mute.js'; +import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; +import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js'; +import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; +import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; +import * as ep___i_favorites from './endpoints/i/favorites.js'; +import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js'; +import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js'; +import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js'; +import * as ep___i_importBlocking from './endpoints/i/import-blocking.js'; +import * as ep___i_importFollowing from './endpoints/i/import-following.js'; +import * as ep___i_importMuting from './endpoints/i/import-muting.js'; +import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js'; +import * as ep___i_importAntennas from './endpoints/i/import-antennas.js'; +import * as ep___i_notifications from './endpoints/i/notifications.js'; +import * as ep___i_pageLikes from './endpoints/i/page-likes.js'; +import * as ep___i_pages from './endpoints/i/pages.js'; +import * as ep___i_pin from './endpoints/i/pin.js'; +import * as ep___i_readAllUnreadNotes from './endpoints/i/read-all-unread-notes.js'; +import * as ep___i_readAnnouncement from './endpoints/i/read-announcement.js'; +import * as ep___i_regenerateToken from './endpoints/i/regenerate-token.js'; +import * as ep___i_registry_getAll from './endpoints/i/registry/get-all.js'; +import * as ep___i_registry_getDetail from './endpoints/i/registry/get-detail.js'; +import * as ep___i_registry_get from './endpoints/i/registry/get.js'; +import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js'; +import * as ep___i_registry_keys from './endpoints/i/registry/keys.js'; +import * as ep___i_registry_remove from './endpoints/i/registry/remove.js'; +import * as ep___i_registry_scopes from './endpoints/i/registry/scopes.js'; +import * as ep___i_registry_set from './endpoints/i/registry/set.js'; +import * as ep___i_revokeToken from './endpoints/i/revoke-token.js'; +import * as ep___i_signinHistory from './endpoints/i/signin-history.js'; +import * as ep___i_unpin from './endpoints/i/unpin.js'; +import * as ep___i_updateEmail from './endpoints/i/update-email.js'; +import * as ep___i_update from './endpoints/i/update.js'; +import * as ep___i_move from './endpoints/i/move.js'; +import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js'; +import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; +import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; +import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; +import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; +import * as ep___meta from './endpoints/meta.js'; +import * as ep___emojis from './endpoints/emojis.js'; +import * as ep___emoji from './endpoints/emoji.js'; +import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; +import * as ep___mute_create from './endpoints/mute/create.js'; +import * as ep___mute_delete from './endpoints/mute/delete.js'; +import * as ep___mute_list from './endpoints/mute/list.js'; +import * as ep___renoteMute_create from './endpoints/renote-mute/create.js'; +import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js'; +import * as ep___renoteMute_list from './endpoints/renote-mute/list.js'; +import * as ep___my_apps from './endpoints/my/apps.js'; +import * as ep___notes from './endpoints/notes.js'; +import * as ep___notes_children from './endpoints/notes/children.js'; +import * as ep___notes_clips from './endpoints/notes/clips.js'; +import * as ep___notes_conversation from './endpoints/notes/conversation.js'; +import * as ep___notes_create from './endpoints/notes/create.js'; +import * as ep___notes_delete from './endpoints/notes/delete.js'; +import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; +import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; +import * as ep___notes_featured from './endpoints/notes/featured.js'; +import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; +import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; +import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; +import * as ep___notes_mentions from './endpoints/notes/mentions.js'; +import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js'; +import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js'; +import * as ep___notes_reactions from './endpoints/notes/reactions.js'; +import * as ep___notes_reactions_create from './endpoints/notes/reactions/create.js'; +import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js'; +import * as ep___notes_renotes from './endpoints/notes/renotes.js'; +import * as ep___notes_replies from './endpoints/notes/replies.js'; +import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js'; +import * as ep___notes_search from './endpoints/notes/search.js'; +import * as ep___notes_show from './endpoints/notes/show.js'; +import * as ep___notes_state from './endpoints/notes/state.js'; +import * as ep___notes_threadMuting_create from './endpoints/notes/thread-muting/create.js'; +import * as ep___notes_threadMuting_delete from './endpoints/notes/thread-muting/delete.js'; +import * as ep___notes_timeline from './endpoints/notes/timeline.js'; +import * as ep___notes_translate from './endpoints/notes/translate.js'; +import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; +import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; +import * as ep___notifications_create from './endpoints/notifications/create.js'; +import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; +import * as ep___pagePush from './endpoints/page-push.js'; +import * as ep___pages_create from './endpoints/pages/create.js'; +import * as ep___pages_delete from './endpoints/pages/delete.js'; +import * as ep___pages_featured from './endpoints/pages/featured.js'; +import * as ep___pages_like from './endpoints/pages/like.js'; +import * as ep___pages_show from './endpoints/pages/show.js'; +import * as ep___pages_unlike from './endpoints/pages/unlike.js'; +import * as ep___pages_update from './endpoints/pages/update.js'; +import * as ep___flash_create from './endpoints/flash/create.js'; +import * as ep___flash_delete from './endpoints/flash/delete.js'; +import * as ep___flash_featured from './endpoints/flash/featured.js'; +import * as ep___flash_like from './endpoints/flash/like.js'; +import * as ep___flash_show from './endpoints/flash/show.js'; +import * as ep___flash_unlike from './endpoints/flash/unlike.js'; +import * as ep___flash_update from './endpoints/flash/update.js'; +import * as ep___flash_my from './endpoints/flash/my.js'; +import * as ep___flash_myLikes from './endpoints/flash/my-likes.js'; +import * as ep___ping from './endpoints/ping.js'; +import * as ep___pinnedUsers from './endpoints/pinned-users.js'; +import * as ep___promo_read from './endpoints/promo/read.js'; +import * as ep___roles_list from './endpoints/roles/list.js'; +import * as ep___roles_show from './endpoints/roles/show.js'; +import * as ep___roles_users from './endpoints/roles/users.js'; +import * as ep___roles_notes from './endpoints/roles/notes.js'; +import * as ep___requestResetPassword from './endpoints/request-reset-password.js'; +import * as ep___resetDb from './endpoints/reset-db.js'; +import * as ep___resetPassword from './endpoints/reset-password.js'; +import * as ep___serverInfo from './endpoints/server-info.js'; +import * as ep___stats from './endpoints/stats.js'; +import * as ep___sw_show_registration from './endpoints/sw/show-registration.js'; +import * as ep___sw_update_registration from './endpoints/sw/update-registration.js'; +import * as ep___sw_register from './endpoints/sw/register.js'; +import * as ep___sw_unregister from './endpoints/sw/unregister.js'; +import * as ep___test from './endpoints/test.js'; +import * as ep___username_available from './endpoints/username/available.js'; +import * as ep___users from './endpoints/users.js'; +import * as ep___users_clips from './endpoints/users/clips.js'; +import * as ep___users_followers from './endpoints/users/followers.js'; +import * as ep___users_following from './endpoints/users/following.js'; +import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; +import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; +import * as ep___users_lists_create from './endpoints/users/lists/create.js'; +import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; +import * as ep___users_lists_list from './endpoints/users/lists/list.js'; +import * as ep___users_lists_pull from './endpoints/users/lists/pull.js'; +import * as ep___users_lists_push from './endpoints/users/lists/push.js'; +import * as ep___users_lists_show from './endpoints/users/lists/show.js'; +import * as ep___users_lists_update from './endpoints/users/lists/update.js'; +import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js'; +import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js'; +import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js'; +import * as ep___users_notes from './endpoints/users/notes.js'; +import * as ep___users_pages from './endpoints/users/pages.js'; +import * as ep___users_reactions from './endpoints/users/reactions.js'; +import * as ep___users_recommendation from './endpoints/users/recommendation.js'; +import * as ep___users_relation from './endpoints/users/relation.js'; +import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js'; +import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js'; +import * as ep___users_search from './endpoints/users/search.js'; +import * as ep___users_show from './endpoints/users/show.js'; +import * as ep___users_achievements from './endpoints/users/achievements.js'; +import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; +import * as ep___fetchRss from './endpoints/fetch-rss.js'; +import * as ep___retention from './endpoints/retention.js'; import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import type { Provider } from '@nestjs/common'; -const endpoints = Object.entries(endpointsObject); -const endpointProviders = endpoints.map(([path, endpoint]): Provider => ({ provide: `ep:${path}`, useClass: endpoint.default })); +const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default }; +const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default }; +const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default }; +const $admin_accounts_delete: Provider = { provide: 'ep:admin/accounts/delete', useClass: ep___admin_accounts_delete.default }; +const $admin_ad_create: Provider = { provide: 'ep:admin/ad/create', useClass: ep___admin_ad_create.default }; +const $admin_ad_delete: Provider = { provide: 'ep:admin/ad/delete', useClass: ep___admin_ad_delete.default }; +const $admin_ad_list: Provider = { provide: 'ep:admin/ad/list', useClass: ep___admin_ad_list.default }; +const $admin_ad_update: Provider = { provide: 'ep:admin/ad/update', useClass: ep___admin_ad_update.default }; +const $admin_announcements_create: Provider = { provide: 'ep:admin/announcements/create', useClass: ep___admin_announcements_create.default }; +const $admin_announcements_delete: Provider = { provide: 'ep:admin/announcements/delete', useClass: ep___admin_announcements_delete.default }; +const $admin_announcements_list: Provider = { provide: 'ep:admin/announcements/list', useClass: ep___admin_announcements_list.default }; +const $admin_announcements_update: Provider = { provide: 'ep:admin/announcements/update', useClass: ep___admin_announcements_update.default }; +const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default }; +const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default }; +const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default }; +const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default }; +const $admin_drive_showFile: Provider = { provide: 'ep:admin/drive/show-file', useClass: ep___admin_drive_showFile.default }; +const $admin_emoji_addAliasesBulk: Provider = { provide: 'ep:admin/emoji/add-aliases-bulk', useClass: ep___admin_emoji_addAliasesBulk.default }; +const $admin_emoji_add: Provider = { provide: 'ep:admin/emoji/add', useClass: ep___admin_emoji_add.default }; +const $admin_emoji_copy: Provider = { provide: 'ep:admin/emoji/copy', useClass: ep___admin_emoji_copy.default }; +const $admin_emoji_deleteBulk: Provider = { provide: 'ep:admin/emoji/delete-bulk', useClass: ep___admin_emoji_deleteBulk.default }; +const $admin_emoji_delete: Provider = { provide: 'ep:admin/emoji/delete', useClass: ep___admin_emoji_delete.default }; +const $admin_emoji_importZip: Provider = { provide: 'ep:admin/emoji/import-zip', useClass: ep___admin_emoji_importZip.default }; +const $admin_emoji_listRemote: Provider = { provide: 'ep:admin/emoji/list-remote', useClass: ep___admin_emoji_listRemote.default }; +const $admin_emoji_list: Provider = { provide: 'ep:admin/emoji/list', useClass: ep___admin_emoji_list.default }; +const $admin_emoji_removeAliasesBulk: Provider = { provide: 'ep:admin/emoji/remove-aliases-bulk', useClass: ep___admin_emoji_removeAliasesBulk.default }; +const $admin_emoji_setAliasesBulk: Provider = { provide: 'ep:admin/emoji/set-aliases-bulk', useClass: ep___admin_emoji_setAliasesBulk.default }; +const $admin_emoji_setCategoryBulk: Provider = { provide: 'ep:admin/emoji/set-category-bulk', useClass: ep___admin_emoji_setCategoryBulk.default }; +const $admin_emoji_setLicenseBulk: Provider = { provide: 'ep:admin/emoji/set-license-bulk', useClass: ep___admin_emoji_setLicenseBulk.default }; +const $admin_emoji_update: Provider = { provide: 'ep:admin/emoji/update', useClass: ep___admin_emoji_update.default }; +const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federation/delete-all-files', useClass: ep___admin_federation_deleteAllFiles.default }; +const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default }; +const $admin_federation_removeAllFollowing: Provider = { provide: 'ep:admin/federation/remove-all-following', useClass: ep___admin_federation_removeAllFollowing.default }; +const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federation/update-instance', useClass: ep___admin_federation_updateInstance.default }; +const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default }; +const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default }; +const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default }; +const $invite: Provider = { provide: 'ep:invite', useClass: ep___invite.default }; +const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default }; +const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default }; +const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default }; +const $admin_queue_inboxDelayed: Provider = { provide: 'ep:admin/queue/inbox-delayed', useClass: ep___admin_queue_inboxDelayed.default }; +const $admin_queue_promote: Provider = { provide: 'ep:admin/queue/promote', useClass: ep___admin_queue_promote.default }; +const $admin_queue_stats: Provider = { provide: 'ep:admin/queue/stats', useClass: ep___admin_queue_stats.default }; +const $admin_relays_add: Provider = { provide: 'ep:admin/relays/add', useClass: ep___admin_relays_add.default }; +const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass: ep___admin_relays_list.default }; +const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default }; +const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default }; +const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default }; +const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default }; +const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default }; +const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default }; +const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default }; +const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default }; +const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default }; +const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default }; +const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default }; +const $admin_deleteAccount: Provider = { provide: 'ep:admin/delete-account', useClass: ep___admin_deleteAccount.default }; +const $admin_updateUserNote: Provider = { provide: 'ep:admin/update-user-note', useClass: ep___admin_updateUserNote.default }; +const $admin_roles_create: Provider = { provide: 'ep:admin/roles/create', useClass: ep___admin_roles_create.default }; +const $admin_roles_delete: Provider = { provide: 'ep:admin/roles/delete', useClass: ep___admin_roles_delete.default }; +const $admin_roles_list: Provider = { provide: 'ep:admin/roles/list', useClass: ep___admin_roles_list.default }; +const $admin_roles_show: Provider = { provide: 'ep:admin/roles/show', useClass: ep___admin_roles_show.default }; +const $admin_roles_update: Provider = { provide: 'ep:admin/roles/update', useClass: ep___admin_roles_update.default }; +const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useClass: ep___admin_roles_assign.default }; +const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default }; +const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default }; +const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default }; +const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default }; +const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default }; +const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default }; +const $antennas_list: Provider = { provide: 'ep:antennas/list', useClass: ep___antennas_list.default }; +const $antennas_notes: Provider = { provide: 'ep:antennas/notes', useClass: ep___antennas_notes.default }; +const $antennas_show: Provider = { provide: 'ep:antennas/show', useClass: ep___antennas_show.default }; +const $antennas_update: Provider = { provide: 'ep:antennas/update', useClass: ep___antennas_update.default }; +const $ap_get: Provider = { provide: 'ep:ap/get', useClass: ep___ap_get.default }; +const $ap_show: Provider = { provide: 'ep:ap/show', useClass: ep___ap_show.default }; +const $app_create: Provider = { provide: 'ep:app/create', useClass: ep___app_create.default }; +const $app_show: Provider = { provide: 'ep:app/show', useClass: ep___app_show.default }; +const $auth_accept: Provider = { provide: 'ep:auth/accept', useClass: ep___auth_accept.default }; +const $auth_session_generate: Provider = { provide: 'ep:auth/session/generate', useClass: ep___auth_session_generate.default }; +const $auth_session_show: Provider = { provide: 'ep:auth/session/show', useClass: ep___auth_session_show.default }; +const $auth_session_userkey: Provider = { provide: 'ep:auth/session/userkey', useClass: ep___auth_session_userkey.default }; +const $blocking_create: Provider = { provide: 'ep:blocking/create', useClass: ep___blocking_create.default }; +const $blocking_delete: Provider = { provide: 'ep:blocking/delete', useClass: ep___blocking_delete.default }; +const $blocking_list: Provider = { provide: 'ep:blocking/list', useClass: ep___blocking_list.default }; +const $channels_create: Provider = { provide: 'ep:channels/create', useClass: ep___channels_create.default }; +const $channels_featured: Provider = { provide: 'ep:channels/featured', useClass: ep___channels_featured.default }; +const $channels_follow: Provider = { provide: 'ep:channels/follow', useClass: ep___channels_follow.default }; +const $channels_followed: Provider = { provide: 'ep:channels/followed', useClass: ep___channels_followed.default }; +const $channels_owned: Provider = { provide: 'ep:channels/owned', useClass: ep___channels_owned.default }; +const $channels_show: Provider = { provide: 'ep:channels/show', useClass: ep___channels_show.default }; +const $channels_timeline: Provider = { provide: 'ep:channels/timeline', useClass: ep___channels_timeline.default }; +const $channels_unfollow: Provider = { provide: 'ep:channels/unfollow', useClass: ep___channels_unfollow.default }; +const $channels_update: Provider = { provide: 'ep:channels/update', useClass: ep___channels_update.default }; +const $channels_favorite: Provider = { provide: 'ep:channels/favorite', useClass: ep___channels_favorite.default }; +const $channels_unfavorite: Provider = { provide: 'ep:channels/unfavorite', useClass: ep___channels_unfavorite.default }; +const $channels_myFavorites: Provider = { provide: 'ep:channels/my-favorites', useClass: ep___channels_myFavorites.default }; +const $channels_search: Provider = { provide: 'ep:channels/search', useClass: ep___channels_search.default }; +const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useClass: ep___charts_activeUsers.default }; +const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default }; +const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default }; +const $charts_federation: Provider = { provide: 'ep:charts/federation', useClass: ep___charts_federation.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 }; +const $charts_user_following: Provider = { provide: 'ep:charts/user/following', useClass: ep___charts_user_following.default }; +const $charts_user_notes: Provider = { provide: 'ep:charts/user/notes', useClass: ep___charts_user_notes.default }; +const $charts_user_pv: Provider = { provide: 'ep:charts/user/pv', useClass: ep___charts_user_pv.default }; +const $charts_user_reactions: Provider = { provide: 'ep:charts/user/reactions', useClass: ep___charts_user_reactions.default }; +const $charts_users: Provider = { provide: 'ep:charts/users', useClass: ep___charts_users.default }; +const $clips_addNote: Provider = { provide: 'ep:clips/add-note', useClass: ep___clips_addNote.default }; +const $clips_removeNote: Provider = { provide: 'ep:clips/remove-note', useClass: ep___clips_removeNote.default }; +const $clips_create: Provider = { provide: 'ep:clips/create', useClass: ep___clips_create.default }; +const $clips_delete: Provider = { provide: 'ep:clips/delete', useClass: ep___clips_delete.default }; +const $clips_list: Provider = { provide: 'ep:clips/list', useClass: ep___clips_list.default }; +const $clips_notes: Provider = { provide: 'ep:clips/notes', useClass: ep___clips_notes.default }; +const $clips_show: Provider = { provide: 'ep:clips/show', useClass: ep___clips_show.default }; +const $clips_update: Provider = { provide: 'ep:clips/update', useClass: ep___clips_update.default }; +const $clips_favorite: Provider = { provide: 'ep:clips/favorite', useClass: ep___clips_favorite.default }; +const $clips_unfavorite: Provider = { provide: 'ep:clips/unfavorite', useClass: ep___clips_unfavorite.default }; +const $clips_myFavorites: Provider = { provide: 'ep:clips/my-favorites', useClass: ep___clips_myFavorites.default }; +const $drive: Provider = { provide: 'ep:drive', useClass: ep___drive.default }; +const $drive_files: Provider = { provide: 'ep:drive/files', useClass: ep___drive_files.default }; +const $drive_files_attachedNotes: Provider = { provide: 'ep:drive/files/attached-notes', useClass: ep___drive_files_attachedNotes.default }; +const $drive_files_checkExistence: Provider = { provide: 'ep:drive/files/check-existence', useClass: ep___drive_files_checkExistence.default }; +const $drive_files_create: Provider = { provide: 'ep:drive/files/create', useClass: ep___drive_files_create.default }; +const $drive_files_delete: Provider = { provide: 'ep:drive/files/delete', useClass: ep___drive_files_delete.default }; +const $drive_files_findByHash: Provider = { provide: 'ep:drive/files/find-by-hash', useClass: ep___drive_files_findByHash.default }; +const $drive_files_find: Provider = { provide: 'ep:drive/files/find', useClass: ep___drive_files_find.default }; +const $drive_files_show: Provider = { provide: 'ep:drive/files/show', useClass: ep___drive_files_show.default }; +const $drive_files_update: Provider = { provide: 'ep:drive/files/update', useClass: ep___drive_files_update.default }; +const $drive_files_uploadFromUrl: Provider = { provide: 'ep:drive/files/upload-from-url', useClass: ep___drive_files_uploadFromUrl.default }; +const $drive_folders: Provider = { provide: 'ep:drive/folders', useClass: ep___drive_folders.default }; +const $drive_folders_create: Provider = { provide: 'ep:drive/folders/create', useClass: ep___drive_folders_create.default }; +const $drive_folders_delete: Provider = { provide: 'ep:drive/folders/delete', useClass: ep___drive_folders_delete.default }; +const $drive_folders_find: Provider = { provide: 'ep:drive/folders/find', useClass: ep___drive_folders_find.default }; +const $drive_folders_show: Provider = { provide: 'ep:drive/folders/show', useClass: ep___drive_folders_show.default }; +const $drive_folders_update: Provider = { provide: 'ep:drive/folders/update', useClass: ep___drive_folders_update.default }; +const $drive_stream: Provider = { provide: 'ep:drive/stream', useClass: ep___drive_stream.default }; +const $emailAddress_available: Provider = { provide: 'ep:email-address/available', useClass: ep___emailAddress_available.default }; +const $endpoint: Provider = { provide: 'ep:endpoint', useClass: ep___endpoint.default }; +const $endpoints: Provider = { provide: 'ep:endpoints', useClass: ep___endpoints.default }; +const $exportCustomEmojis: Provider = { provide: 'ep:export-custom-emojis', useClass: ep___exportCustomEmojis.default }; +const $federation_followers: Provider = { provide: 'ep:federation/followers', useClass: ep___federation_followers.default }; +const $federation_following: Provider = { provide: 'ep:federation/following', useClass: ep___federation_following.default }; +const $federation_instances: Provider = { provide: 'ep:federation/instances', useClass: ep___federation_instances.default }; +const $federation_showInstance: Provider = { provide: 'ep:federation/show-instance', useClass: ep___federation_showInstance.default }; +const $federation_updateRemoteUser: Provider = { provide: 'ep:federation/update-remote-user', useClass: ep___federation_updateRemoteUser.default }; +const $federation_users: Provider = { provide: 'ep:federation/users', useClass: ep___federation_users.default }; +const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass: ep___federation_stats.default }; +const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default }; +const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default }; +const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default }; +const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default }; +const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default }; +const $following_requests_list: Provider = { provide: 'ep:following/requests/list', useClass: ep___following_requests_list.default }; +const $following_requests_reject: Provider = { provide: 'ep:following/requests/reject', useClass: ep___following_requests_reject.default }; +const $gallery_featured: Provider = { provide: 'ep:gallery/featured', useClass: ep___gallery_featured.default }; +const $gallery_popular: Provider = { provide: 'ep:gallery/popular', useClass: ep___gallery_popular.default }; +const $gallery_posts: Provider = { provide: 'ep:gallery/posts', useClass: ep___gallery_posts.default }; +const $gallery_posts_create: Provider = { provide: 'ep:gallery/posts/create', useClass: ep___gallery_posts_create.default }; +const $gallery_posts_delete: Provider = { provide: 'ep:gallery/posts/delete', useClass: ep___gallery_posts_delete.default }; +const $gallery_posts_like: Provider = { provide: 'ep:gallery/posts/like', useClass: ep___gallery_posts_like.default }; +const $gallery_posts_show: Provider = { provide: 'ep:gallery/posts/show', useClass: ep___gallery_posts_show.default }; +const $gallery_posts_unlike: Provider = { provide: 'ep:gallery/posts/unlike', useClass: ep___gallery_posts_unlike.default }; +const $gallery_posts_update: Provider = { provide: 'ep:gallery/posts/update', useClass: ep___gallery_posts_update.default }; +const $getOnlineUsersCount: Provider = { provide: 'ep:get-online-users-count', useClass: ep___getOnlineUsersCount.default }; +const $hashtags_list: Provider = { provide: 'ep:hashtags/list', useClass: ep___hashtags_list.default }; +const $hashtags_search: Provider = { provide: 'ep:hashtags/search', useClass: ep___hashtags_search.default }; +const $hashtags_show: Provider = { provide: 'ep:hashtags/show', useClass: ep___hashtags_show.default }; +const $hashtags_trend: Provider = { provide: 'ep:hashtags/trend', useClass: ep___hashtags_trend.default }; +const $hashtags_users: Provider = { provide: 'ep:hashtags/users', useClass: ep___hashtags_users.default }; +const $i: Provider = { provide: 'ep:i', useClass: ep___i.default }; +const $i_2fa_done: Provider = { provide: 'ep:i/2fa/done', useClass: ep___i_2fa_done.default }; +const $i_2fa_keyDone: Provider = { provide: 'ep:i/2fa/key-done', useClass: ep___i_2fa_keyDone.default }; +const $i_2fa_passwordLess: Provider = { provide: 'ep:i/2fa/password-less', useClass: ep___i_2fa_passwordLess.default }; +const $i_2fa_registerKey: Provider = { provide: 'ep:i/2fa/register-key', useClass: ep___i_2fa_registerKey.default }; +const $i_2fa_register: Provider = { provide: 'ep:i/2fa/register', useClass: ep___i_2fa_register.default }; +const $i_2fa_updateKey: Provider = { provide: 'ep:i/2fa/update-key', useClass: ep___i_2fa_updateKey.default }; +const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: ep___i_2fa_removeKey.default }; +const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default }; +const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default }; +const $i_authorizedApps: Provider = { provide: 'ep:i/authorized-apps', useClass: ep___i_authorizedApps.default }; +const $i_claimAchievement: Provider = { provide: 'ep:i/claim-achievement', useClass: ep___i_claimAchievement.default }; +const $i_changePassword: Provider = { provide: 'ep:i/change-password', useClass: ep___i_changePassword.default }; +const $i_deleteAccount: Provider = { provide: 'ep:i/delete-account', useClass: ep___i_deleteAccount.default }; +const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: ep___i_exportBlocking.default }; +const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default }; +const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default }; +const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default }; +const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default }; +const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default }; +const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default }; +const $i_favorites: Provider = { provide: 'ep:i/favorites', useClass: ep___i_favorites.default }; +const $i_gallery_likes: Provider = { provide: 'ep:i/gallery/likes', useClass: ep___i_gallery_likes.default }; +const $i_gallery_posts: Provider = { provide: 'ep:i/gallery/posts', useClass: ep___i_gallery_posts.default }; +const $i_getWordMutedNotesCount: Provider = { provide: 'ep:i/get-word-muted-notes-count', useClass: ep___i_getWordMutedNotesCount.default }; +const $i_importBlocking: Provider = { provide: 'ep:i/import-blocking', useClass: ep___i_importBlocking.default }; +const $i_importFollowing: Provider = { provide: 'ep:i/import-following', useClass: ep___i_importFollowing.default }; +const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep___i_importMuting.default }; +const $i_importUserLists: Provider = { provide: 'ep:i/import-user-lists', useClass: ep___i_importUserLists.default }; +const $i_importAntennas: Provider = { provide: 'ep:i/import-antennas', useClass: ep___i_importAntennas.default }; +const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep___i_notifications.default }; +const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default }; +const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default }; +const $i_pin: Provider = { provide: 'ep:i/pin', useClass: ep___i_pin.default }; +const $i_readAllUnreadNotes: Provider = { provide: 'ep:i/read-all-unread-notes', useClass: ep___i_readAllUnreadNotes.default }; +const $i_readAnnouncement: Provider = { provide: 'ep:i/read-announcement', useClass: ep___i_readAnnouncement.default }; +const $i_regenerateToken: Provider = { provide: 'ep:i/regenerate-token', useClass: ep___i_regenerateToken.default }; +const $i_registry_getAll: Provider = { provide: 'ep:i/registry/get-all', useClass: ep___i_registry_getAll.default }; +const $i_registry_getDetail: Provider = { provide: 'ep:i/registry/get-detail', useClass: ep___i_registry_getDetail.default }; +const $i_registry_get: Provider = { provide: 'ep:i/registry/get', useClass: ep___i_registry_get.default }; +const $i_registry_keysWithType: Provider = { provide: 'ep:i/registry/keys-with-type', useClass: ep___i_registry_keysWithType.default }; +const $i_registry_keys: Provider = { provide: 'ep:i/registry/keys', useClass: ep___i_registry_keys.default }; +const $i_registry_remove: Provider = { provide: 'ep:i/registry/remove', useClass: ep___i_registry_remove.default }; +const $i_registry_scopes: Provider = { provide: 'ep:i/registry/scopes', useClass: ep___i_registry_scopes.default }; +const $i_registry_set: Provider = { provide: 'ep:i/registry/set', useClass: ep___i_registry_set.default }; +const $i_revokeToken: Provider = { provide: 'ep:i/revoke-token', useClass: ep___i_revokeToken.default }; +const $i_signinHistory: Provider = { provide: 'ep:i/signin-history', useClass: ep___i_signinHistory.default }; +const $i_unpin: Provider = { provide: 'ep:i/unpin', useClass: ep___i_unpin.default }; +const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default }; +const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default }; +const $i_move: Provider = { provide: 'ep:i/move', useClass: ep___i_move.default }; +const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default }; +const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default }; +const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default }; +const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default }; +const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default }; +const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default }; +const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default }; +const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default }; +const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default }; +const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default }; +const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default }; +const $mute_list: Provider = { provide: 'ep:mute/list', useClass: ep___mute_list.default }; +const $renoteMute_create: Provider = { provide: 'ep:renote-mute/create', useClass: ep___renoteMute_create.default }; +const $renoteMute_delete: Provider = { provide: 'ep:renote-mute/delete', useClass: ep___renoteMute_delete.default }; +const $renoteMute_list: Provider = { provide: 'ep:renote-mute/list', useClass: ep___renoteMute_list.default }; +const $my_apps: Provider = { provide: 'ep:my/apps', useClass: ep___my_apps.default }; +const $notes: Provider = { provide: 'ep:notes', useClass: ep___notes.default }; +const $notes_children: Provider = { provide: 'ep:notes/children', useClass: ep___notes_children.default }; +const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes_clips.default }; +const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default }; +const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default }; +const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default }; +const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default }; +const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default }; +const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; +const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default }; +const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default }; +const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', useClass: ep___notes_localTimeline.default }; +const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default }; +const $notes_polls_recommendation: Provider = { provide: 'ep:notes/polls/recommendation', useClass: ep___notes_polls_recommendation.default }; +const $notes_polls_vote: Provider = { provide: 'ep:notes/polls/vote', useClass: ep___notes_polls_vote.default }; +const $notes_reactions: Provider = { provide: 'ep:notes/reactions', useClass: ep___notes_reactions.default }; +const $notes_reactions_create: Provider = { provide: 'ep:notes/reactions/create', useClass: ep___notes_reactions_create.default }; +const $notes_reactions_delete: Provider = { provide: 'ep:notes/reactions/delete', useClass: ep___notes_reactions_delete.default }; +const $notes_renotes: Provider = { provide: 'ep:notes/renotes', useClass: ep___notes_renotes.default }; +const $notes_replies: Provider = { provide: 'ep:notes/replies', useClass: ep___notes_replies.default }; +const $notes_searchByTag: Provider = { provide: 'ep:notes/search-by-tag', useClass: ep___notes_searchByTag.default }; +const $notes_search: Provider = { provide: 'ep:notes/search', useClass: ep___notes_search.default }; +const $notes_show: Provider = { provide: 'ep:notes/show', useClass: ep___notes_show.default }; +const $notes_state: Provider = { provide: 'ep:notes/state', useClass: ep___notes_state.default }; +const $notes_threadMuting_create: Provider = { provide: 'ep:notes/thread-muting/create', useClass: ep___notes_threadMuting_create.default }; +const $notes_threadMuting_delete: Provider = { provide: 'ep:notes/thread-muting/delete', useClass: ep___notes_threadMuting_delete.default }; +const $notes_timeline: Provider = { provide: 'ep:notes/timeline', useClass: ep___notes_timeline.default }; +const $notes_translate: Provider = { provide: 'ep:notes/translate', useClass: ep___notes_translate.default }; +const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep___notes_unrenote.default }; +const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default }; +const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default }; +const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default }; +const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default }; +const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default }; +const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default }; +const $pages_featured: Provider = { provide: 'ep:pages/featured', useClass: ep___pages_featured.default }; +const $pages_like: Provider = { provide: 'ep:pages/like', useClass: ep___pages_like.default }; +const $pages_show: Provider = { provide: 'ep:pages/show', useClass: ep___pages_show.default }; +const $pages_unlike: Provider = { provide: 'ep:pages/unlike', useClass: ep___pages_unlike.default }; +const $pages_update: Provider = { provide: 'ep:pages/update', useClass: ep___pages_update.default }; +const $flash_create: Provider = { provide: 'ep:flash/create', useClass: ep___flash_create.default }; +const $flash_delete: Provider = { provide: 'ep:flash/delete', useClass: ep___flash_delete.default }; +const $flash_featured: Provider = { provide: 'ep:flash/featured', useClass: ep___flash_featured.default }; +const $flash_like: Provider = { provide: 'ep:flash/like', useClass: ep___flash_like.default }; +const $flash_show: Provider = { provide: 'ep:flash/show', useClass: ep___flash_show.default }; +const $flash_unlike: Provider = { provide: 'ep:flash/unlike', useClass: ep___flash_unlike.default }; +const $flash_update: Provider = { provide: 'ep:flash/update', useClass: ep___flash_update.default }; +const $flash_my: Provider = { provide: 'ep:flash/my', useClass: ep___flash_my.default }; +const $flash_myLikes: Provider = { provide: 'ep:flash/my-likes', useClass: ep___flash_myLikes.default }; +const $ping: Provider = { provide: 'ep:ping', useClass: ep___ping.default }; +const $pinnedUsers: Provider = { provide: 'ep:pinned-users', useClass: ep___pinnedUsers.default }; +const $promo_read: Provider = { provide: 'ep:promo/read', useClass: ep___promo_read.default }; +const $roles_list: Provider = { provide: 'ep:roles/list', useClass: ep___roles_list.default }; +const $roles_show: Provider = { provide: 'ep:roles/show', useClass: ep___roles_show.default }; +const $roles_users: Provider = { provide: 'ep:roles/users', useClass: ep___roles_users.default }; +const $roles_notes: Provider = { provide: 'ep:roles/notes', useClass: ep___roles_notes.default }; +const $requestResetPassword: Provider = { provide: 'ep:request-reset-password', useClass: ep___requestResetPassword.default }; +const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.default }; +const $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.default }; +const $serverInfo: Provider = { provide: 'ep:server-info', useClass: ep___serverInfo.default }; +const $stats: Provider = { provide: 'ep:stats', useClass: ep___stats.default }; +const $sw_show_registration: Provider = { provide: 'ep:sw/show-registration', useClass: ep___sw_show_registration.default }; +const $sw_update_registration: Provider = { provide: 'ep:sw/update-registration', useClass: ep___sw_update_registration.default }; +const $sw_register: Provider = { provide: 'ep:sw/register', useClass: ep___sw_register.default }; +const $sw_unregister: Provider = { provide: 'ep:sw/unregister', useClass: ep___sw_unregister.default }; +const $test: Provider = { provide: 'ep:test', useClass: ep___test.default }; +const $username_available: Provider = { provide: 'ep:username/available', useClass: ep___username_available.default }; +const $users: Provider = { provide: 'ep:users', useClass: ep___users.default }; +const $users_clips: Provider = { provide: 'ep:users/clips', useClass: ep___users_clips.default }; +const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep___users_followers.default }; +const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default }; +const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default }; +const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default }; +const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default }; +const $users_lists_delete: Provider = { provide: 'ep:users/lists/delete', useClass: ep___users_lists_delete.default }; +const $users_lists_list: Provider = { provide: 'ep:users/lists/list', useClass: ep___users_lists_list.default }; +const $users_lists_pull: Provider = { provide: 'ep:users/lists/pull', useClass: ep___users_lists_pull.default }; +const $users_lists_push: Provider = { provide: 'ep:users/lists/push', useClass: ep___users_lists_push.default }; +const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: ep___users_lists_show.default }; +const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default }; +const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default }; +const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default }; +const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default }; +const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default }; +const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default }; +const $users_reactions: Provider = { provide: 'ep:users/reactions', useClass: ep___users_reactions.default }; +const $users_recommendation: Provider = { provide: 'ep:users/recommendation', useClass: ep___users_recommendation.default }; +const $users_relation: Provider = { provide: 'ep:users/relation', useClass: ep___users_relation.default }; +const $users_reportAbuse: Provider = { provide: 'ep:users/report-abuse', useClass: ep___users_reportAbuse.default }; +const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-username-and-host', useClass: ep___users_searchByUsernameAndHost.default }; +const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default }; +const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default }; +const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default }; +const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default }; +const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; +const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default }; @Module({ imports: [ @@ -21,10 +685,678 @@ const endpointProviders = endpoints.map(([path, endpoint]): Provider => ({ provi providers: [ GetterService, ApiLoggerService, - ...endpointProviders, + $admin_meta, + $admin_abuseUserReports, + $admin_accounts_create, + $admin_accounts_delete, + $admin_ad_create, + $admin_ad_delete, + $admin_ad_list, + $admin_ad_update, + $admin_announcements_create, + $admin_announcements_delete, + $admin_announcements_list, + $admin_announcements_update, + $admin_deleteAllFilesOfAUser, + $admin_drive_cleanRemoteFiles, + $admin_drive_cleanup, + $admin_drive_files, + $admin_drive_showFile, + $admin_emoji_addAliasesBulk, + $admin_emoji_add, + $admin_emoji_copy, + $admin_emoji_deleteBulk, + $admin_emoji_delete, + $admin_emoji_importZip, + $admin_emoji_listRemote, + $admin_emoji_list, + $admin_emoji_removeAliasesBulk, + $admin_emoji_setAliasesBulk, + $admin_emoji_setCategoryBulk, + $admin_emoji_setLicenseBulk, + $admin_emoji_update, + $admin_federation_deleteAllFiles, + $admin_federation_refreshRemoteInstanceMetadata, + $admin_federation_removeAllFollowing, + $admin_federation_updateInstance, + $admin_getIndexStats, + $admin_getTableStats, + $admin_getUserIps, + $invite, + $admin_promo_create, + $admin_queue_clear, + $admin_queue_deliverDelayed, + $admin_queue_inboxDelayed, + $admin_queue_promote, + $admin_queue_stats, + $admin_relays_add, + $admin_relays_list, + $admin_relays_remove, + $admin_resetPassword, + $admin_resolveAbuseUserReport, + $admin_sendEmail, + $admin_serverInfo, + $admin_showModerationLogs, + $admin_showUser, + $admin_showUsers, + $admin_suspendUser, + $admin_unsuspendUser, + $admin_updateMeta, + $admin_deleteAccount, + $admin_updateUserNote, + $admin_roles_create, + $admin_roles_delete, + $admin_roles_list, + $admin_roles_show, + $admin_roles_update, + $admin_roles_assign, + $admin_roles_unassign, + $admin_roles_updateDefaultPolicies, + $admin_roles_users, + $announcements, + $antennas_create, + $antennas_delete, + $antennas_list, + $antennas_notes, + $antennas_show, + $antennas_update, + $ap_get, + $ap_show, + $app_create, + $app_show, + $auth_accept, + $auth_session_generate, + $auth_session_show, + $auth_session_userkey, + $blocking_create, + $blocking_delete, + $blocking_list, + $channels_create, + $channels_featured, + $channels_follow, + $channels_followed, + $channels_owned, + $channels_show, + $channels_timeline, + $channels_unfollow, + $channels_update, + $channels_favorite, + $channels_unfavorite, + $channels_myFavorites, + $channels_search, + $charts_activeUsers, + $charts_apRequest, + $charts_drive, + $charts_federation, + $charts_instance, + $charts_notes, + $charts_user_drive, + $charts_user_following, + $charts_user_notes, + $charts_user_pv, + $charts_user_reactions, + $charts_users, + $clips_addNote, + $clips_removeNote, + $clips_create, + $clips_delete, + $clips_list, + $clips_notes, + $clips_show, + $clips_update, + $clips_favorite, + $clips_unfavorite, + $clips_myFavorites, + $drive, + $drive_files, + $drive_files_attachedNotes, + $drive_files_checkExistence, + $drive_files_create, + $drive_files_delete, + $drive_files_findByHash, + $drive_files_find, + $drive_files_show, + $drive_files_update, + $drive_files_uploadFromUrl, + $drive_folders, + $drive_folders_create, + $drive_folders_delete, + $drive_folders_find, + $drive_folders_show, + $drive_folders_update, + $drive_stream, + $emailAddress_available, + $endpoint, + $endpoints, + $exportCustomEmojis, + $federation_followers, + $federation_following, + $federation_instances, + $federation_showInstance, + $federation_updateRemoteUser, + $federation_users, + $federation_stats, + $following_create, + $following_delete, + $following_invalidate, + $following_requests_accept, + $following_requests_cancel, + $following_requests_list, + $following_requests_reject, + $gallery_featured, + $gallery_popular, + $gallery_posts, + $gallery_posts_create, + $gallery_posts_delete, + $gallery_posts_like, + $gallery_posts_show, + $gallery_posts_unlike, + $gallery_posts_update, + $getOnlineUsersCount, + $hashtags_list, + $hashtags_search, + $hashtags_show, + $hashtags_trend, + $hashtags_users, + $i, + $i_2fa_done, + $i_2fa_keyDone, + $i_2fa_passwordLess, + $i_2fa_registerKey, + $i_2fa_register, + $i_2fa_updateKey, + $i_2fa_removeKey, + $i_2fa_unregister, + $i_apps, + $i_authorizedApps, + $i_claimAchievement, + $i_changePassword, + $i_deleteAccount, + $i_exportBlocking, + $i_exportFollowing, + $i_exportMute, + $i_exportNotes, + $i_exportFavorites, + $i_exportUserLists, + $i_exportAntennas, + $i_favorites, + $i_gallery_likes, + $i_gallery_posts, + $i_getWordMutedNotesCount, + $i_importBlocking, + $i_importFollowing, + $i_importMuting, + $i_importUserLists, + $i_importAntennas, + $i_notifications, + $i_pageLikes, + $i_pages, + $i_pin, + $i_readAllUnreadNotes, + $i_readAnnouncement, + $i_regenerateToken, + $i_registry_getAll, + $i_registry_getDetail, + $i_registry_get, + $i_registry_keysWithType, + $i_registry_keys, + $i_registry_remove, + $i_registry_scopes, + $i_registry_set, + $i_revokeToken, + $i_signinHistory, + $i_unpin, + $i_updateEmail, + $i_update, + $i_move, + $i_webhooks_create, + $i_webhooks_list, + $i_webhooks_show, + $i_webhooks_update, + $i_webhooks_delete, + $meta, + $emojis, + $emoji, + $miauth_genToken, + $mute_create, + $mute_delete, + $mute_list, + $renoteMute_create, + $renoteMute_delete, + $renoteMute_list, + $my_apps, + $notes, + $notes_children, + $notes_clips, + $notes_conversation, + $notes_create, + $notes_delete, + $notes_favorites_create, + $notes_favorites_delete, + $notes_featured, + $notes_globalTimeline, + $notes_hybridTimeline, + $notes_localTimeline, + $notes_mentions, + $notes_polls_recommendation, + $notes_polls_vote, + $notes_reactions, + $notes_reactions_create, + $notes_reactions_delete, + $notes_renotes, + $notes_replies, + $notes_searchByTag, + $notes_search, + $notes_show, + $notes_state, + $notes_threadMuting_create, + $notes_threadMuting_delete, + $notes_timeline, + $notes_translate, + $notes_unrenote, + $notes_userListTimeline, + $notifications_create, + $notifications_markAllAsRead, + $pagePush, + $pages_create, + $pages_delete, + $pages_featured, + $pages_like, + $pages_show, + $pages_unlike, + $pages_update, + $flash_create, + $flash_delete, + $flash_featured, + $flash_like, + $flash_show, + $flash_unlike, + $flash_update, + $flash_my, + $flash_myLikes, + $ping, + $pinnedUsers, + $promo_read, + $roles_list, + $roles_show, + $roles_users, + $roles_notes, + $requestResetPassword, + $resetDb, + $resetPassword, + $serverInfo, + $stats, + $sw_show_registration, + $sw_update_registration, + $sw_register, + $sw_unregister, + $test, + $username_available, + $users, + $users_clips, + $users_followers, + $users_following, + $users_gallery_posts, + $users_getFrequentlyRepliedUsers, + $users_lists_create, + $users_lists_delete, + $users_lists_list, + $users_lists_pull, + $users_lists_push, + $users_lists_show, + $users_lists_update, + $users_lists_favorite, + $users_lists_unfavorite, + $users_lists_create_from_public, + $users_notes, + $users_pages, + $users_reactions, + $users_recommendation, + $users_relation, + $users_reportAbuse, + $users_searchByUsernameAndHost, + $users_search, + $users_show, + $users_achievements, + $users_updateMemo, + $fetchRss, + $retention, ], exports: [ - ...endpointProviders, + $admin_meta, + $admin_abuseUserReports, + $admin_accounts_create, + $admin_accounts_delete, + $admin_ad_create, + $admin_ad_delete, + $admin_ad_list, + $admin_ad_update, + $admin_announcements_create, + $admin_announcements_delete, + $admin_announcements_list, + $admin_announcements_update, + $admin_deleteAllFilesOfAUser, + $admin_drive_cleanRemoteFiles, + $admin_drive_cleanup, + $admin_drive_files, + $admin_drive_showFile, + $admin_emoji_addAliasesBulk, + $admin_emoji_add, + $admin_emoji_copy, + $admin_emoji_deleteBulk, + $admin_emoji_delete, + $admin_emoji_importZip, + $admin_emoji_listRemote, + $admin_emoji_list, + $admin_emoji_removeAliasesBulk, + $admin_emoji_setAliasesBulk, + $admin_emoji_setCategoryBulk, + $admin_emoji_setLicenseBulk, + $admin_emoji_update, + $admin_federation_deleteAllFiles, + $admin_federation_refreshRemoteInstanceMetadata, + $admin_federation_removeAllFollowing, + $admin_federation_updateInstance, + $admin_getIndexStats, + $admin_getTableStats, + $admin_getUserIps, + $invite, + $admin_promo_create, + $admin_queue_clear, + $admin_queue_deliverDelayed, + $admin_queue_inboxDelayed, + $admin_queue_promote, + $admin_queue_stats, + $admin_relays_add, + $admin_relays_list, + $admin_relays_remove, + $admin_resetPassword, + $admin_resolveAbuseUserReport, + $admin_sendEmail, + $admin_serverInfo, + $admin_showModerationLogs, + $admin_showUser, + $admin_showUsers, + $admin_suspendUser, + $admin_unsuspendUser, + $admin_updateMeta, + $admin_deleteAccount, + $admin_updateUserNote, + $admin_roles_create, + $admin_roles_delete, + $admin_roles_list, + $admin_roles_show, + $admin_roles_update, + $admin_roles_assign, + $admin_roles_unassign, + $admin_roles_updateDefaultPolicies, + $admin_roles_users, + $announcements, + $antennas_create, + $antennas_delete, + $antennas_list, + $antennas_notes, + $antennas_show, + $antennas_update, + $ap_get, + $ap_show, + $app_create, + $app_show, + $auth_accept, + $auth_session_generate, + $auth_session_show, + $auth_session_userkey, + $blocking_create, + $blocking_delete, + $blocking_list, + $channels_create, + $channels_featured, + $channels_follow, + $channels_followed, + $channels_owned, + $channels_show, + $channels_timeline, + $channels_unfollow, + $channels_update, + $channels_favorite, + $channels_unfavorite, + $channels_myFavorites, + $channels_search, + $charts_activeUsers, + $charts_apRequest, + $charts_drive, + $charts_federation, + $charts_instance, + $charts_notes, + $charts_user_drive, + $charts_user_following, + $charts_user_notes, + $charts_user_pv, + $charts_user_reactions, + $charts_users, + $clips_addNote, + $clips_removeNote, + $clips_create, + $clips_delete, + $clips_list, + $clips_notes, + $clips_show, + $clips_update, + $clips_favorite, + $clips_unfavorite, + $clips_myFavorites, + $drive, + $drive_files, + $drive_files_attachedNotes, + $drive_files_checkExistence, + $drive_files_create, + $drive_files_delete, + $drive_files_findByHash, + $drive_files_find, + $drive_files_show, + $drive_files_update, + $drive_files_uploadFromUrl, + $drive_folders, + $drive_folders_create, + $drive_folders_delete, + $drive_folders_find, + $drive_folders_show, + $drive_folders_update, + $drive_stream, + $emailAddress_available, + $endpoint, + $endpoints, + $exportCustomEmojis, + $federation_followers, + $federation_following, + $federation_instances, + $federation_showInstance, + $federation_updateRemoteUser, + $federation_users, + $federation_stats, + $following_create, + $following_delete, + $following_invalidate, + $following_requests_accept, + $following_requests_cancel, + $following_requests_list, + $following_requests_reject, + $gallery_featured, + $gallery_popular, + $gallery_posts, + $gallery_posts_create, + $gallery_posts_delete, + $gallery_posts_like, + $gallery_posts_show, + $gallery_posts_unlike, + $gallery_posts_update, + $getOnlineUsersCount, + $hashtags_list, + $hashtags_search, + $hashtags_show, + $hashtags_trend, + $hashtags_users, + $i, + $i_2fa_done, + $i_2fa_keyDone, + $i_2fa_passwordLess, + $i_2fa_registerKey, + $i_2fa_register, + $i_2fa_updateKey, + $i_2fa_removeKey, + $i_2fa_unregister, + $i_apps, + $i_authorizedApps, + $i_claimAchievement, + $i_changePassword, + $i_deleteAccount, + $i_exportBlocking, + $i_exportFollowing, + $i_exportMute, + $i_exportNotes, + $i_exportFavorites, + $i_exportUserLists, + $i_exportAntennas, + $i_favorites, + $i_gallery_likes, + $i_gallery_posts, + $i_getWordMutedNotesCount, + $i_importBlocking, + $i_importFollowing, + $i_importMuting, + $i_importUserLists, + $i_importAntennas, + $i_notifications, + $i_pageLikes, + $i_pages, + $i_pin, + $i_readAllUnreadNotes, + $i_readAnnouncement, + $i_regenerateToken, + $i_registry_getAll, + $i_registry_getDetail, + $i_registry_get, + $i_registry_keysWithType, + $i_registry_keys, + $i_registry_remove, + $i_registry_scopes, + $i_registry_set, + $i_revokeToken, + $i_signinHistory, + $i_unpin, + $i_updateEmail, + $i_update, + $i_move, + $i_webhooks_create, + $i_webhooks_list, + $i_webhooks_show, + $i_webhooks_update, + $i_webhooks_delete, + $meta, + $emojis, + $emoji, + $miauth_genToken, + $mute_create, + $mute_delete, + $mute_list, + $renoteMute_create, + $renoteMute_delete, + $renoteMute_list, + $my_apps, + $notes, + $notes_children, + $notes_clips, + $notes_conversation, + $notes_create, + $notes_delete, + $notes_favorites_create, + $notes_favorites_delete, + $notes_featured, + $notes_globalTimeline, + $notes_hybridTimeline, + $notes_localTimeline, + $notes_mentions, + $notes_polls_recommendation, + $notes_polls_vote, + $notes_reactions, + $notes_reactions_create, + $notes_reactions_delete, + $notes_renotes, + $notes_replies, + $notes_searchByTag, + $notes_search, + $notes_show, + $notes_state, + $notes_threadMuting_create, + $notes_threadMuting_delete, + $notes_timeline, + $notes_translate, + $notes_unrenote, + $notes_userListTimeline, + $notifications_create, + $notifications_markAllAsRead, + $pagePush, + $pages_create, + $pages_delete, + $pages_featured, + $pages_like, + $pages_show, + $pages_unlike, + $pages_update, + $flash_create, + $flash_delete, + $flash_featured, + $flash_like, + $flash_show, + $flash_unlike, + $flash_update, + $flash_my, + $flash_myLikes, + $ping, + $pinnedUsers, + $promo_read, + $roles_list, + $roles_show, + $roles_users, + $roles_notes, + $requestResetPassword, + $resetDb, + $resetPassword, + $serverInfo, + $stats, + $sw_register, + $sw_unregister, + $test, + $username_available, + $users, + $users_clips, + $users_followers, + $users_following, + $users_gallery_posts, + $users_getFrequentlyRepliedUsers, + $users_lists_create, + $users_lists_delete, + $users_lists_list, + $users_lists_pull, + $users_lists_push, + $users_lists_show, + $users_lists_update, + $users_lists_favorite, + $users_lists_unfavorite, + $users_lists_create_from_public, + $users_notes, + $users_pages, + $users_reactions, + $users_recommendation, + $users_relation, + $users_reportAbuse, + $users_searchByUsernameAndHost, + $users_search, + $users_show, + $users_achievements, + $users_updateMemo, + $fetchRss, + $retention, ], }) export class EndpointsModule {} diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index 444e6db744..c94884a78c 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -1,14 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UsersRepository } from '@/models/_.js'; +import type { NotesRepository, UsersRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import type { MiNote } from '@/models/Note.js'; +import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; +import type { Note } from '@/models/entities/Note.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -29,7 +24,7 @@ export class GetterService { * Get note for API processing */ @bindThis - public async getNote(noteId: MiNote['id']) { + public async getNote(noteId: Note['id']) { const note = await this.notesRepository.findOneBy({ id: noteId }); if (note == null) { @@ -39,36 +34,25 @@ export class GetterService { return note; } - @bindThis - public async getNoteWithUser(noteId: MiNote['id']) { - const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] }); - - if (note == null) { - throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); - } - - return note; - } - /** * Get user for API processing */ @bindThis - public async getUser(userId: MiUser['id']) { + public async getUser(userId: User['id']) { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) { throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.'); } - return user as MiLocalUser | MiRemoteUser; + return user as LocalUser | RemoteUser; } /** * Get remote user for API processing */ @bindThis - public async getRemoteUser(userId: MiUser['id']) { + public async getRemoteUser(userId: User['id']) { const user = await this.getUser(userId); if (!this.userEntityService.isRemoteUser(user)) { @@ -82,7 +66,7 @@ export class GetterService { * Get local user for API processing */ @bindThis - public async getLocalUser(userId: MiUser['id']) { + public async getLocalUser(userId: User['id']) { const user = await this.getUser(userId); if (!this.userEntityService.isLocalUser(user)) { diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts index a730d8c60e..fe2db1d66a 100644 --- a/packages/backend/src/server/api/RateLimiterService.ts +++ b/packages/backend/src/server/api/RateLimiterService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import Limiter from 'ratelimiter'; import * as Redis from 'ioredis'; @@ -12,14 +7,6 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import type { IEndpointMeta } from './endpoints.js'; -type RateLimitInfo = { - code: 'BRIEF_REQUEST_INTERVAL', - info: Limiter.LimiterInfo, -} | { - code: 'RATE_LIMIT_EXCEEDED', - info: Limiter.LimiterInfo, -}; - @Injectable() export class RateLimiterService { private logger: Logger; @@ -39,55 +26,75 @@ export class RateLimiterService { } @bindThis - private checkLimiter(options: Limiter.LimiterOption): Promise { - return new Promise((resolve, reject) => { - new Limiter(options).get((err, info) => { - if (err) { - return reject(err); - } - resolve(info); - }); + public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string, factor = 1) { + return new Promise((ok, reject) => { + if (this.disabled) ok(); + + // Short-term limit + const min = (): void => { + const minIntervalLimiter = new Limiter({ + id: `${actor}:${limitation.key}:min`, + duration: limitation.minInterval! * factor, + max: 1, + db: this.redisClient, + }); + + minIntervalLimiter.get((err, info) => { + if (err) { + return reject('ERR'); + } + + this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); + + if (info.remaining === 0) { + reject('BRIEF_REQUEST_INTERVAL'); + } else { + if (hasLongTermLimit) { + max(); + } else { + ok(); + } + } + }); + }; + + // Long term limit + const max = (): void => { + const limiter = new Limiter({ + id: `${actor}:${limitation.key}`, + duration: limitation.duration! * factor, + max: limitation.max! / factor, + db: this.redisClient, + }); + + limiter.get((err, info) => { + if (err) { + return reject('ERR'); + } + + this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); + + if (info.remaining === 0) { + reject('RATE_LIMIT_EXCEEDED'); + } else { + ok(); + } + }); + }; + + const hasShortTermLimit = typeof limitation.minInterval === 'number'; + + const hasLongTermLimit = + typeof limitation.duration === 'number' && + typeof limitation.max === 'number'; + + if (hasShortTermLimit) { + min(); + } else if (hasLongTermLimit) { + max(); + } else { + ok(); + } }); } - - @bindThis - public async limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string, factor = 1): Promise { - if (this.disabled) { - return null; - } - - // Short-term limit - if (limitation.minInterval != null) { - const info = await this.checkLimiter({ - id: `${actor}:${limitation.key}:min`, - duration: limitation.minInterval * factor, - max: 1, - db: this.redisClient, - }); - - this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); - - if (info.remaining === 0) { - return { code: 'BRIEF_REQUEST_INTERVAL', info }; - } - } - - // Long term limit - if (limitation.duration != null && limitation.max != null) { - const info = await this.checkLimiter({ - id: `${actor}:${limitation.key}`, - duration: limitation.duration, - max: limitation.max / factor, - db: this.redisClient, - }); - - this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); - - if (info.remaining === 0) { - return { code: 'RATE_LIMIT_EXCEEDED', info }; - } - } - - return null; - } } diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 3e889372d8..bd3d8a28da 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -1,33 +1,19 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - +import { randomBytes } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; +import * as OTPAuth from 'otpauth'; import { IsNull } from 'typeorm'; -import * as Misskey from 'misskey-js'; import { DI } from '@/di-symbols.js'; -import type { - MiMeta, - SigninsRepository, - UserProfilesRepository, - UserSecurityKeysRepository, - UsersRepository, -} from '@/models/_.js'; +import type { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; -import type { MiLocalUser } from '@/models/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; +import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; import { bindThis } from '@/decorators.js'; -import { WebAuthnService } from '@/core/WebAuthnService.js'; -import { UserAuthService } from '@/core/UserAuthService.js'; -import { CaptchaService } from '@/core/CaptchaService.js'; -import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; -import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; -import type { FastifyReply, FastifyRequest } from 'fastify'; +import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() export class SigninApiService { @@ -35,17 +21,17 @@ export class SigninApiService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - @Inject(DI.userSecurityKeysRepository) - private userSecurityKeysRepository: UserSecurityKeysRepository, + @Inject(DI.attestationChallengesRepository) + private attestationChallengesRepository: AttestationChallengesRepository, @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, @@ -53,9 +39,7 @@ export class SigninApiService { private idService: IdService, private rateLimiterService: RateLimiterService, private signinService: SigninService, - private userAuthService: UserAuthService, - private webAuthnService: WebAuthnService, - private captchaService: CaptchaService, + private twoFactorAuthenticationService: TwoFactorAuthenticationService, ) { } @@ -64,14 +48,13 @@ export class SigninApiService { request: FastifyRequest<{ Body: { username: string; - password?: string; + password: string; token?: string; - credential?: AuthenticationResponseJSON; - 'hcaptcha-response'?: string; - 'g-recaptcha-response'?: string; - 'turnstile-response'?: string; - 'm-captcha-response'?: string; - 'testcaptcha-response'?: string; + signature?: string; + authenticatorData?: string; + clientDataJSON?: string; + credentialId?: string; + challengeId?: string; }; }>, reply: FastifyReply, @@ -89,9 +72,10 @@ export class SigninApiService { return { error }; } + try { // not more than 1 attempt per second and not more than 10 attempts per hour - const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip)); - if (rateLimit != null) { + await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip)); + } catch (err) { reply.code(429); return { error: { @@ -107,6 +91,11 @@ export class SigninApiService { return; } + if (typeof password !== 'string') { + reply.code(400); + return; + } + if (token != null && typeof token !== 'string') { reply.code(400); return; @@ -116,7 +105,7 @@ export class SigninApiService { const user = await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull(), - }) as MiLocalUser; + }) as LocalUser; if (user == null) { return error(404, { @@ -131,35 +120,15 @@ export class SigninApiService { } const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1); - - if (password == null) { - reply.code(200); - if (profile.twoFactorEnabled) { - return { - finished: false, - next: 'password', - } satisfies Misskey.entities.SigninFlowResponse; - } else { - return { - finished: false, - next: 'captcha', - } satisfies Misskey.entities.SigninFlowResponse; - } - } - - if (typeof password !== 'string') { - reply.code(400); - return; - } // Compare password const same = await bcrypt.compare(password, profile.password!); - const fail = async (status?: number, failure?: { id: string; }) => { - // Append signin history + const fail = async (status?: number, failure?: { id: string }) => { + // Append signin history await this.signinsRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), userId: user.id, ip: request.ip, headers: request.headers as any, @@ -170,38 +139,6 @@ export class SigninApiService { }; if (!profile.twoFactorEnabled) { - if (process.env.NODE_ENV !== 'test') { - if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) { - await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => { - throw new FastifyReplyError(400, err); - }); - } - - if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) { - await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { - throw new FastifyReplyError(400, err); - }); - } - - if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) { - await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { - throw new FastifyReplyError(400, err); - }); - } - - if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) { - await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => { - throw new FastifyReplyError(400, err); - }); - } - - if (this.meta.enableTestcaptcha) { - await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => { - throw new FastifyReplyError(400, err); - }); - } - } - if (same) { return this.signinService.signin(request, reply, user); } else { @@ -218,59 +155,127 @@ export class SigninApiService { }); } - try { - await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (e) { + const delta = OTPAuth.TOTP.validate({ + secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!), + digits: 6, + token, + window: 1, + }); + + if (delta === null) { return await fail(403, { id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f', }); + } else { + return this.signinService.signin(request, reply, user); } - - return this.signinService.signin(request, reply, user); - } else if (body.credential) { + } else if (body.credentialId && body.clientDataJSON && body.authenticatorData && body.signature) { if (!same && !profile.usePasswordLessLogin) { return await fail(403, { id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', }); } - const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential); + const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex'); + const clientData = JSON.parse(clientDataJSON.toString('utf-8')); + const challenge = await this.attestationChallengesRepository.findOneBy({ + userId: user.id, + id: body.challengeId, + registrationChallenge: false, + challenge: this.twoFactorAuthenticationService.hash(clientData.challenge).toString('hex'), + }); - if (authorized) { + if (!challenge) { + return await fail(403, { + id: '2715a88a-2125-4013-932f-aa6fe72792da', + }); + } + + await this.attestationChallengesRepository.delete({ + userId: user.id, + id: body.challengeId, + }); + + if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) { + return await fail(403, { + id: '2715a88a-2125-4013-932f-aa6fe72792da', + }); + } + + const securityKey = await this.userSecurityKeysRepository.findOneBy({ + id: Buffer.from( + body.credentialId + .replace(/-/g, '+') + .replace(/_/g, '/'), + 'base64', + ).toString('hex'), + }); + + if (!securityKey) { + return await fail(403, { + id: '66269679-aeaf-4474-862b-eb761197e046', + }); + } + + const isValid = this.twoFactorAuthenticationService.verifySignin({ + publicKey: Buffer.from(securityKey.publicKey, 'hex'), + authenticatorData: Buffer.from(body.authenticatorData, 'hex'), + clientDataJSON, + clientData, + signature: Buffer.from(body.signature, 'hex'), + challenge: challenge.challenge, + }); + + if (isValid) { return this.signinService.signin(request, reply, user); } else { return await fail(403, { id: '93b86c4b-72f9-40eb-9815-798928603d1e', }); } - } else if (securityKeysAvailable) { + } else { if (!same && !profile.usePasswordLessLogin) { return await fail(403, { id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', }); } - const authRequest = await this.webAuthnService.initiateAuthentication(user.id); + const keys = await this.userSecurityKeysRepository.findBy({ + userId: user.id, + }); + + if (keys.length === 0) { + return await fail(403, { + id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4', + }); + } + + // 32 byte challenge + const challenge = randomBytes(32).toString('base64') + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + + const challengeId = this.idService.genId(); + + await this.attestationChallengesRepository.insert({ + userId: user.id, + id: challengeId, + challenge: this.twoFactorAuthenticationService.hash(Buffer.from(challenge, 'utf-8')).toString('hex'), + createdAt: new Date(), + registrationChallenge: false, + }); reply.code(200); return { - finished: false, - next: 'passkey', - authRequest, - } satisfies Misskey.entities.SigninFlowResponse; - } else { - if (!same || !profile.twoFactorEnabled) { - return await fail(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', - }); - } else { - reply.code(200); - return { - finished: false, - next: 'totp', - } satisfies Misskey.entities.SigninFlowResponse; - } + challenge, + challengeId, + securityKeys: keys.map(key => ({ + id: key.id, + })), + }; } - // never get here + // never get here } } + diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts index 640356b50c..aaf1d10b42 100644 --- a/packages/backend/src/server/api/SigninService.ts +++ b/packages/backend/src/server/api/SigninService.ts @@ -1,67 +1,51 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import * as Misskey from 'misskey-js'; import { DI } from '@/di-symbols.js'; -import type { SigninsRepository, UserProfilesRepository } from '@/models/_.js'; +import type { SigninsRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; import { IdService } from '@/core/IdService.js'; -import type { MiLocalUser } from '@/models/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { SigninEntityService } from '@/core/entities/SigninEntityService.js'; import { bindThis } from '@/decorators.js'; -import { EmailService } from '@/core/EmailService.js'; -import { NotificationService } from '@/core/NotificationService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() export class SigninService { constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - private signinEntityService: SigninEntityService, - private emailService: EmailService, - private notificationService: NotificationService, private idService: IdService, private globalEventService: GlobalEventService, ) { } @bindThis - public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) { + public signin(request: FastifyRequest, reply: FastifyReply, user: LocalUser) { setImmediate(async () => { - this.notificationService.createNotification(user.id, 'login', {}); - - const record = await this.signinsRepository.insertOne({ - id: this.idService.gen(), + // Append signin history + const record = await this.signinsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), userId: user.id, ip: request.ip, headers: request.headers as any, success: true, - }); - + }).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0])); + + // Publish signin event this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record)); - - const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.email && profile.emailVerified) { - this.emailService.sendEmail(profile.email, 'New login / ログインがありました', - 'There is a new login. If you do not recognize this login, update the security status of your account, including changing your password. / 新しいログインがありました。このログインに心当たりがない場合は、パスワードを変更するなど、アカウントのセキュリティ状態を更新してください。', - 'There is a new login. If you do not recognize this login, update the security status of your account, including changing your password. / 新しいログインがありました。このログインに心当たりがない場合は、パスワードを変更するなど、アカウントのセキュリティ状態を更新してください。'); - } }); reply.code(200); return { - finished: true, id: user.id, - i: user.token!, - } satisfies Misskey.entities.SigninFlowResponse; + i: user.token, + }; } } diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts deleted file mode 100644 index 9ba23c54e2..0000000000 --- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts +++ /dev/null @@ -1,173 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { randomUUID } from 'crypto'; -import { Inject, Injectable } from '@nestjs/common'; -import { IsNull } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import type { - SigninsRepository, - UserProfilesRepository, - UsersRepository, -} from '@/models/_.js'; -import type { Config } from '@/config.js'; -import { getIpHash } from '@/misc/get-ip-hash.js'; -import type { MiLocalUser, MiUser } from '@/models/User.js'; -import { IdService } from '@/core/IdService.js'; -import { bindThis } from '@/decorators.js'; -import { WebAuthnService } from '@/core/WebAuthnService.js'; -import Logger from '@/logger.js'; -import { LoggerService } from '@/core/LoggerService.js'; -import type { IdentifiableError } from '@/misc/identifiable-error.js'; -import { RateLimiterService } from './RateLimiterService.js'; -import { SigninService } from './SigninService.js'; -import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; -import type { FastifyReply, FastifyRequest } from 'fastify'; - -@Injectable() -export class SigninWithPasskeyApiService { - private logger: Logger; - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - - @Inject(DI.signinsRepository) - private signinsRepository: SigninsRepository, - - private idService: IdService, - private rateLimiterService: RateLimiterService, - private signinService: SigninService, - private webAuthnService: WebAuthnService, - private loggerService: LoggerService, - ) { - this.logger = this.loggerService.getLogger('PasskeyAuth'); - } - - @bindThis - public async signin( - request: FastifyRequest<{ - Body: { - credential?: AuthenticationResponseJSON; - context?: string; - }; - }>, - reply: FastifyReply, - ) { - reply.header('Access-Control-Allow-Origin', this.config.url); - reply.header('Access-Control-Allow-Credentials', 'true'); - - const body = request.body; - const credential = body['credential']; - - function error(status: number, error: { id: string }) { - reply.code(status); - return { error }; - } - - const fail = async (userId: MiUser['id'], status?: number, failure?: { id: string }) => { - // Append signin history - await this.signinsRepository.insert({ - id: this.idService.gen(), - userId: userId, - ip: request.ip, - headers: request.headers as any, - success: false, - }); - return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' }); - }; - - try { - // Not more than 1 API call per 250ms and not more than 100 attempts per 30min - // NOTE: 1 Sign-in require 2 API calls - await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip)); - } catch (err) { - reply.code(429); - return { - error: { - message: 'Too many failed attempts to sign in. Try again later.', - code: 'TOO_MANY_AUTHENTICATION_FAILURES', - id: '22d05606-fbcf-421a-a2db-b32610dcfd1b', - }, - }; - } - - // Initiate Passkey Auth challenge with context - if (!credential) { - const context = randomUUID(); - this.logger.info(`Initiate Passkey challenge: context: ${context}`); - const authChallengeOptions = { - option: await this.webAuthnService.initiateSignInWithPasskeyAuthentication(context), - context: context, - }; - reply.code(200); - return authChallengeOptions; - } - - const context = body.context; - if (!context || typeof context !== 'string') { - // If try Authentication without context - return error(400, { - id: '1658cc2e-4495-461f-aee4-d403cdf073c1', - }); - } - - this.logger.debug(`Try Sign-in with Passkey: context: ${context}`); - - let authorizedUserId: MiUser['id'] | null; - try { - authorizedUserId = await this.webAuthnService.verifySignInWithPasskeyAuthentication(context, credential); - } catch (err) { - this.logger.warn(`Passkey challenge Verify error! : ${err}`); - const errorId = (err as IdentifiableError).id; - return error(403, { - id: errorId, - }); - } - - if (!authorizedUserId) { - return error(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', - }); - } - - // Fetch user - const user = await this.usersRepository.findOneBy({ - id: authorizedUserId, - host: IsNull(), - }) as MiLocalUser | null; - - if (user == null) { - return error(403, { - id: '652f899f-66d4-490e-993e-6606c8ec04c3', - }); - } - - if (user.isSuspended) { - return error(403, { - id: 'e03a5f46-d309-4865-9b69-56282d94e1eb', - }); - } - - const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - - // Authentication was successful, but passwordless login is not enabled - if (!profile.usePasswordLessLogin) { - return await fail(user.id, 403, { - id: '2d84773e-f7b7-4d0b-8f72-bb69b584c912', - }); - } - - const signinResponse = this.signinService.signin(request, reply, user); - return { - signinResponse: signinResponse, - }; - } -} diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index 3ec5e5d3e6..fc5f3811eb 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -1,25 +1,21 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket, MiMeta } from '@/models/_.js'; +import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; +import { MetaService } from '@/core/MetaService.js'; import { CaptchaService } from '@/core/CaptchaService.js'; import { IdService } from '@/core/IdService.js'; import { SignupService } from '@/core/SignupService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { EmailService } from '@/core/EmailService.js'; -import { MiLocalUser } from '@/models/User.js'; +import { LocalUser } from '@/models/entities/User.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { bindThis } from '@/decorators.js'; -import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { SigninService } from './SigninService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; +import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; @Injectable() export class SignupApiService { @@ -27,9 +23,6 @@ export class SignupApiService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -47,6 +40,7 @@ export class SignupApiService { private userEntityService: UserEntityService, private idService: IdService, + private metaService: MetaService, private captchaService: CaptchaService, private signupService: SignupService, private signinService: SigninService, @@ -66,43 +60,31 @@ export class SignupApiService { 'hcaptcha-response'?: string; 'g-recaptcha-response'?: string; 'turnstile-response'?: string; - 'm-captcha-response'?: string; - 'testcaptcha-response'?: string; } }>, reply: FastifyReply, ) { const body = request.body; + const instance = await this.metaService.fetch(true); + // Verify *Captcha // ただしテスト時はこの機構は障害となるため無効にする if (process.env.NODE_ENV !== 'test') { - if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) { - await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => { + if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { + await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => { throw new FastifyReplyError(400, err); }); } - if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) { - await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { + if (instance.enableRecaptcha && instance.recaptchaSecretKey) { + await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { throw new FastifyReplyError(400, err); }); } - if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) { - await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { - throw new FastifyReplyError(400, err); - }); - } - - if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) { - await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => { - throw new FastifyReplyError(400, err); - }); - } - - if (this.meta.enableTestcaptcha) { - await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => { + if (instance.enableTurnstile && instance.turnstileSecretKey) { + await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(err => { throw new FastifyReplyError(400, err); }); } @@ -114,7 +96,7 @@ export class SignupApiService { const invitationCode = body['invitationCode']; const emailAddress = body['emailAddress']; - if (this.meta.emailRequiredForSignup) { + if (instance.emailRequiredForSignup) { if (emailAddress == null || typeof emailAddress !== 'string') { reply.code(400); return; @@ -127,58 +109,35 @@ export class SignupApiService { } } - let ticket: MiRegistrationTicket | null = null; - - if (this.meta.disableRegistration) { + if (instance.disableRegistration) { if (invitationCode == null || typeof invitationCode !== 'string') { reply.code(400); return; } - ticket = await this.registrationTicketsRepository.findOneBy({ + const ticket = await this.registrationTicketsRepository.findOneBy({ code: invitationCode, }); - if (ticket == null || ticket.usedById != null) { + if (ticket == null) { reply.code(400); return; } - if (ticket.expiresAt && ticket.expiresAt < new Date()) { - reply.code(400); - return; - } - - // メアド認証が有効の場合 - if (this.meta.emailRequiredForSignup) { - // メアド認証済みならエラー - if (ticket.usedBy) { - reply.code(400); - return; - } - - // 認証しておらず、メール送信から30分以内ならエラー - if (ticket.usedAt && ticket.usedAt.getTime() + (1000 * 60 * 30) > Date.now()) { - reply.code(400); - return; - } - } else if (ticket.usedAt) { - reply.code(400); - return; - } + this.registrationTicketsRepository.delete(ticket.id); } - if (this.meta.emailRequiredForSignup) { - if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { + if (instance.emailRequiredForSignup) { + if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { throw new FastifyReplyError(400, 'DUPLICATED_USERNAME'); } // Check deleted username duplication - if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) { + if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) { throw new FastifyReplyError(400, 'USED_USERNAME'); } - const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); + const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); if (isPreserved) { throw new FastifyReplyError(400, 'DENIED_USERNAME'); } @@ -189,8 +148,9 @@ export class SignupApiService { const salt = await bcrypt.genSalt(8); const hash = await bcrypt.hash(password, salt); - const pendingUser = await this.userPendingsRepository.insertOne({ - id: this.idService.gen(), + await this.userPendingsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), code, email: emailAddress!, username: username, @@ -203,13 +163,6 @@ export class SignupApiService { `To complete signup, please click this link:
${link}`, `To complete signup, please click this link: ${link}`); - if (ticket) { - await this.registrationTicketsRepository.update(ticket.id, { - usedAt: new Date(), - pendingUserId: pendingUser.id, - }); - } - reply.code(204); return; } else { @@ -219,18 +172,10 @@ export class SignupApiService { }); const res = await this.userEntityService.pack(account, account, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, }); - if (ticket) { - await this.registrationTicketsRepository.update(ticket.id, { - usedAt: new Date(), - usedBy: account, - usedById: account.id, - }); - } - return { ...res, token: secret, @@ -250,10 +195,6 @@ export class SignupApiService { try { const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code }); - if (this.idService.parse(pendingUser.id).date.getTime() + (1000 * 60 * 30) < Date.now()) { - throw new FastifyReplyError(400, 'EXPIRED'); - } - const { account, secret } = await this.signupService.signup({ username: pendingUser.username, passwordHash: pendingUser.password, @@ -271,16 +212,7 @@ export class SignupApiService { emailVerifyCode: null, }); - const ticket = await this.registrationTicketsRepository.findOneBy({ pendingUserId: pendingUser.id }); - if (ticket) { - await this.registrationTicketsRepository.update(ticket.id, { - usedBy: account, - usedById: account.id, - pendingUserId: null, - }); - } - - return this.signinService.signin(request, reply, account as MiLocalUser); + return this.signinService.signin(request, reply, account as LocalUser); } catch (err) { throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString()); } diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index 2a4e1fc574..4a0342d2b4 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -1,22 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { EventEmitter } from 'events'; import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import * as WebSocket from 'ws'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, MiAccessToken } from '@/models/_.js'; +import type { UsersRepository, AccessToken } from '@/models/index.js'; +import type { Config } from '@/config.js'; +import { NoteReadService } from '@/core/NoteReadService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; -import { MiLocalUser } from '@/models/User.js'; -import { UserService } from '@/core/UserService.js'; -import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; +import { LocalUser } from '@/models/entities/User.js'; import { AuthenticateService, AuthenticationError } from './AuthenticateService.js'; -import MainStreamConnection from './stream/Connection.js'; +import MainStreamConnection from './stream/index.js'; import { ChannelsService } from './stream/ChannelsService.js'; import type * as http from 'node:http'; @@ -27,6 +23,9 @@ export class StreamingApiServerService { #cleanConnectionsIntervalId: NodeJS.Timeout | null = null; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.redisForSub) private redisForSub: Redis.Redis, @@ -34,11 +33,10 @@ export class StreamingApiServerService { private usersRepository: UsersRepository, private cacheService: CacheService, + private noteReadService: NoteReadService, private authenticateService: AuthenticateService, private channelsService: ChannelsService, private notificationService: NotificationService, - private usersService: UserService, - private channelFollowingService: ChannelFollowingService, ) { } @@ -57,8 +55,8 @@ export class StreamingApiServerService { const q = new URL(request.url, `http://${request.headers.host}`).searchParams; - let user: MiLocalUser | null = null; - let app: MiAccessToken | null = null; + let user: LocalUser | null = null; + let app: AccessToken | null = null; // https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 // Note that the standard WHATWG WebSocket API does not support setting any headers, @@ -69,10 +67,6 @@ export class StreamingApiServerService { try { [user, app] = await this.authenticateService.authenticate(token); - - if (app !== null && !app.permission.some(p => p === 'read:account')) { - throw new AuthenticationError('Your app does not have necessary permissions to use websocket API.'); - } } catch (e) { if (e instanceof AuthenticationError) { socket.write([ @@ -94,9 +88,9 @@ export class StreamingApiServerService { const stream = new MainStreamConnection( this.channelsService, + this.noteReadService, this.notificationService, this.cacheService, - this.channelFollowingService, user, app, ); @@ -109,43 +103,41 @@ export class StreamingApiServerService { }); }); - const globalEv = new EventEmitter(); - - this.redisForSub.on('message', (_: string, data: string) => { - const parsed = JSON.parse(data); - globalEv.emit('message', parsed); - }); - this.#wss.on('connection', async (connection: WebSocket.WebSocket, request: http.IncomingMessage, ctx: { stream: MainStreamConnection, - user: MiLocalUser | null; - app: MiAccessToken | null + user: LocalUser | null; + app: AccessToken | null }) => { const { stream, user, app } = ctx; const ev = new EventEmitter(); - function onRedisMessage(data: any): void { - ev.emit(data.channel, data.message); + async function onRedisMessage(_: string, data: string): Promise { + const parsed = JSON.parse(data); + ev.emit(parsed.channel, parsed.message); } - globalEv.on('message', onRedisMessage); + this.redisForSub.on('message', onRedisMessage); await stream.listen(ev, connection); this.#connections.set(connection, Date.now()); const userUpdateIntervalId = user ? setInterval(() => { - this.usersService.updateLastActiveDate(user); + this.usersRepository.update(user.id, { + lastActiveDate: new Date(), + }); }, 1000 * 60 * 5) : null; if (user) { - this.usersService.updateLastActiveDate(user); + this.usersRepository.update(user.id, { + lastActiveDate: new Date(), + }); } connection.once('close', () => { ev.removeAllListeners(); stream.dispose(); - globalEv.off('message', onRedisMessage); + this.redisForSub.off('message', onRedisMessage); this.#connections.delete(connection); if (userUpdateIntervalId) clearInterval(userUpdateIntervalId); }); diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts index e061aa3a8e..05141854c7 100644 --- a/packages/backend/src/server/api/endpoint-base.ts +++ b/packages/backend/src/server/api/endpoint-base.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as fs from 'node:fs'; import _Ajv from 'ajv'; import type { Schema, SchemaType } from '@/misc/json-schema.js'; -import type { MiLocalUser } from '@/models/User.js'; -import type { MiAccessToken } from '@/models/AccessToken.js'; +import type { LocalUser } from '@/models/entities/User.js'; +import type { AccessToken } from '@/models/entities/AccessToken.js'; import { ApiError } from './error.js'; import type { IEndpointMeta } from './endpoints.js'; @@ -28,34 +23,34 @@ type File = { // TODO: paramsの型をT['params']のスキーマ定義から推論する type Executor = - (params: SchemaType, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record | null) => + (params: SchemaType, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record | null) => Promise>>; export abstract class Endpoint { - public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => Promise; + public exec: (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => Promise; constructor(meta: T, paramDef: Ps, cb: Executor) { const validate = ajv.compile(paramDef); - this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => { + this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => { let cleanup: undefined | (() => void) = undefined; - + if (meta.requireFile) { cleanup = () => { if (file) fs.unlink(file.path, () => {}); }; - + if (file == null) return Promise.reject(new ApiError({ message: 'File required.', code: 'FILE_REQUIRED', id: '4267801e-70d1-416a-b011-4ee502885d8b', })); } - + const valid = validate(params); if (!valid) { if (file) cleanup!(); - + const errors = validate.errors!; const err = new ApiError({ message: 'Invalid param.', @@ -67,7 +62,7 @@ export abstract class Endpoint { }); return Promise.reject(err); } - + return cb(params as SchemaType, user, token, file, cleanup, ip, headers); }; } diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts deleted file mode 100644 index 1fdd000fdf..0000000000 --- a/packages/backend/src/server/api/endpoint-list.ts +++ /dev/null @@ -1,431 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* - * This file contains list of all endpoints exported as pathname of API endpoint - * - * When you add new endpoint, you should add it to this file. - * This file is used to generate API documentation and EndpointsModule. - */ - -export * as 'admin/abuse-report/notification-recipient/create' from './endpoints/admin/abuse-report/notification-recipient/create.js'; -export * as 'admin/abuse-report/notification-recipient/delete' from './endpoints/admin/abuse-report/notification-recipient/delete.js'; -export * as 'admin/abuse-report/notification-recipient/list' from './endpoints/admin/abuse-report/notification-recipient/list.js'; -export * as 'admin/abuse-report/notification-recipient/show' from './endpoints/admin/abuse-report/notification-recipient/show.js'; -export * as 'admin/abuse-report/notification-recipient/update' from './endpoints/admin/abuse-report/notification-recipient/update.js'; -export * as 'admin/abuse-user-reports' from './endpoints/admin/abuse-user-reports.js'; -export * as 'admin/accounts/create' from './endpoints/admin/accounts/create.js'; -export * as 'admin/accounts/delete' from './endpoints/admin/accounts/delete.js'; -export * as 'admin/accounts/find-by-email' from './endpoints/admin/accounts/find-by-email.js'; -export * as 'admin/ad/create' from './endpoints/admin/ad/create.js'; -export * as 'admin/ad/delete' from './endpoints/admin/ad/delete.js'; -export * as 'admin/ad/list' from './endpoints/admin/ad/list.js'; -export * as 'admin/ad/update' from './endpoints/admin/ad/update.js'; -export * as 'admin/announcements/create' from './endpoints/admin/announcements/create.js'; -export * as 'admin/announcements/delete' from './endpoints/admin/announcements/delete.js'; -export * as 'admin/announcements/list' from './endpoints/admin/announcements/list.js'; -export * as 'admin/announcements/update' from './endpoints/admin/announcements/update.js'; -export * as 'admin/avatar-decorations/create' from './endpoints/admin/avatar-decorations/create.js'; -export * as 'admin/avatar-decorations/delete' from './endpoints/admin/avatar-decorations/delete.js'; -export * as 'admin/avatar-decorations/list' from './endpoints/admin/avatar-decorations/list.js'; -export * as 'admin/avatar-decorations/update' from './endpoints/admin/avatar-decorations/update.js'; -export * as 'admin/captcha/current' from './endpoints/admin/captcha/current.js'; -export * as 'admin/captcha/save' from './endpoints/admin/captcha/save.js'; -export * as 'admin/delete-account' from './endpoints/admin/delete-account.js'; -export * as 'admin/delete-all-files-of-a-user' from './endpoints/admin/delete-all-files-of-a-user.js'; -export * as 'admin/drive/clean-remote-files' from './endpoints/admin/drive/clean-remote-files.js'; -export * as 'admin/drive/cleanup' from './endpoints/admin/drive/cleanup.js'; -export * as 'admin/drive/files' from './endpoints/admin/drive/files.js'; -export * as 'admin/drive/show-file' from './endpoints/admin/drive/show-file.js'; -export * as 'admin/emoji/add' from './endpoints/admin/emoji/add.js'; -export * as 'admin/emoji/add-aliases-bulk' from './endpoints/admin/emoji/add-aliases-bulk.js'; -export * as 'admin/emoji/copy' from './endpoints/admin/emoji/copy.js'; -export * as 'admin/emoji/delete' from './endpoints/admin/emoji/delete.js'; -export * as 'admin/emoji/delete-bulk' from './endpoints/admin/emoji/delete-bulk.js'; -export * as 'admin/emoji/import-zip' from './endpoints/admin/emoji/import-zip.js'; -export * as 'admin/emoji/list' from './endpoints/admin/emoji/list.js'; -export * as 'admin/emoji/list-remote' from './endpoints/admin/emoji/list-remote.js'; -export * as 'admin/emoji/remove-aliases-bulk' from './endpoints/admin/emoji/remove-aliases-bulk.js'; -export * as 'admin/emoji/set-aliases-bulk' from './endpoints/admin/emoji/set-aliases-bulk.js'; -export * as 'admin/emoji/set-category-bulk' from './endpoints/admin/emoji/set-category-bulk.js'; -export * as 'admin/emoji/set-license-bulk' from './endpoints/admin/emoji/set-license-bulk.js'; -export * as 'admin/emoji/update' from './endpoints/admin/emoji/update.js'; -export * as 'admin/federation/delete-all-files' from './endpoints/admin/federation/delete-all-files.js'; -export * as 'admin/federation/refresh-remote-instance-metadata' from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; -export * as 'admin/federation/remove-all-following' from './endpoints/admin/federation/remove-all-following.js'; -export * as 'admin/federation/update-instance' from './endpoints/admin/federation/update-instance.js'; -export * as 'admin/forward-abuse-user-report' from './endpoints/admin/forward-abuse-user-report.js'; -export * as 'admin/get-index-stats' from './endpoints/admin/get-index-stats.js'; -export * as 'admin/get-table-stats' from './endpoints/admin/get-table-stats.js'; -export * as 'admin/get-user-ips' from './endpoints/admin/get-user-ips.js'; -export * as 'admin/invite/create' from './endpoints/admin/invite/create.js'; -export * as 'admin/invite/list' from './endpoints/admin/invite/list.js'; -export * as 'admin/meta' from './endpoints/admin/meta.js'; -export * as 'admin/promo/create' from './endpoints/admin/promo/create.js'; -export * as 'admin/queue/clear' from './endpoints/admin/queue/clear.js'; -export * as 'admin/queue/deliver-delayed' from './endpoints/admin/queue/deliver-delayed.js'; -export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-delayed.js'; -export * as 'admin/queue/retry-job' from './endpoints/admin/queue/retry-job.js'; -export * as 'admin/queue/remove-job' from './endpoints/admin/queue/remove-job.js'; -export * as 'admin/queue/show-job' from './endpoints/admin/queue/show-job.js'; -export * as 'admin/queue/promote-jobs' from './endpoints/admin/queue/promote-jobs.js'; -export * as 'admin/queue/jobs' from './endpoints/admin/queue/jobs.js'; -export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js'; -export * as 'admin/queue/queues' from './endpoints/admin/queue/queues.js'; -export * as 'admin/queue/queue-stats' from './endpoints/admin/queue/queue-stats.js'; -export * as 'admin/relays/add' from './endpoints/admin/relays/add.js'; -export * as 'admin/relays/list' from './endpoints/admin/relays/list.js'; -export * as 'admin/relays/remove' from './endpoints/admin/relays/remove.js'; -export * as 'admin/reset-password' from './endpoints/admin/reset-password.js'; -export * as 'admin/resolve-abuse-user-report' from './endpoints/admin/resolve-abuse-user-report.js'; -export * as 'admin/roles/assign' from './endpoints/admin/roles/assign.js'; -export * as 'admin/roles/create' from './endpoints/admin/roles/create.js'; -export * as 'admin/roles/delete' from './endpoints/admin/roles/delete.js'; -export * as 'admin/roles/list' from './endpoints/admin/roles/list.js'; -export * as 'admin/roles/show' from './endpoints/admin/roles/show.js'; -export * as 'admin/roles/unassign' from './endpoints/admin/roles/unassign.js'; -export * as 'admin/roles/update' from './endpoints/admin/roles/update.js'; -export * as 'admin/roles/update-default-policies' from './endpoints/admin/roles/update-default-policies.js'; -export * as 'admin/roles/users' from './endpoints/admin/roles/users.js'; -export * as 'admin/send-email' from './endpoints/admin/send-email.js'; -export * as 'admin/server-info' from './endpoints/admin/server-info.js'; -export * as 'admin/show-moderation-logs' from './endpoints/admin/show-moderation-logs.js'; -export * as 'admin/show-user' from './endpoints/admin/show-user.js'; -export * as 'admin/show-users' from './endpoints/admin/show-users.js'; -export * as 'admin/suspend-user' from './endpoints/admin/suspend-user.js'; -export * as 'admin/system-webhook/create' from './endpoints/admin/system-webhook/create.js'; -export * as 'admin/system-webhook/delete' from './endpoints/admin/system-webhook/delete.js'; -export * as 'admin/system-webhook/list' from './endpoints/admin/system-webhook/list.js'; -export * as 'admin/system-webhook/show' from './endpoints/admin/system-webhook/show.js'; -export * as 'admin/system-webhook/test' from './endpoints/admin/system-webhook/test.js'; -export * as 'admin/system-webhook/update' from './endpoints/admin/system-webhook/update.js'; -export * as 'admin/unset-user-avatar' from './endpoints/admin/unset-user-avatar.js'; -export * as 'admin/unset-user-banner' from './endpoints/admin/unset-user-banner.js'; -export * as 'admin/unsuspend-user' from './endpoints/admin/unsuspend-user.js'; -export * as 'admin/update-abuse-user-report' from './endpoints/admin/update-abuse-user-report.js'; -export * as 'admin/update-meta' from './endpoints/admin/update-meta.js'; -export * as 'admin/update-proxy-account' from './endpoints/admin/update-proxy-account.js'; -export * as 'admin/update-user-note' from './endpoints/admin/update-user-note.js'; -export * as 'announcements' from './endpoints/announcements.js'; -export * as 'announcements/show' from './endpoints/announcements/show.js'; -export * as 'antennas/create' from './endpoints/antennas/create.js'; -export * as 'antennas/delete' from './endpoints/antennas/delete.js'; -export * as 'antennas/list' from './endpoints/antennas/list.js'; -export * as 'antennas/notes' from './endpoints/antennas/notes.js'; -export * as 'antennas/show' from './endpoints/antennas/show.js'; -export * as 'antennas/update' from './endpoints/antennas/update.js'; -export * as 'ap/get' from './endpoints/ap/get.js'; -export * as 'ap/show' from './endpoints/ap/show.js'; -export * as 'app/create' from './endpoints/app/create.js'; -export * as 'app/show' from './endpoints/app/show.js'; -export * as 'auth/accept' from './endpoints/auth/accept.js'; -export * as 'auth/session/generate' from './endpoints/auth/session/generate.js'; -export * as 'auth/session/show' from './endpoints/auth/session/show.js'; -export * as 'auth/session/userkey' from './endpoints/auth/session/userkey.js'; -export * as 'blocking/create' from './endpoints/blocking/create.js'; -export * as 'blocking/delete' from './endpoints/blocking/delete.js'; -export * as 'blocking/list' from './endpoints/blocking/list.js'; -export * as 'bubble-game/ranking' from './endpoints/bubble-game/ranking.js'; -export * as 'bubble-game/register' from './endpoints/bubble-game/register.js'; -export * as 'channels/create' from './endpoints/channels/create.js'; -export * as 'channels/favorite' from './endpoints/channels/favorite.js'; -export * as 'channels/featured' from './endpoints/channels/featured.js'; -export * as 'channels/follow' from './endpoints/channels/follow.js'; -export * as 'channels/followed' from './endpoints/channels/followed.js'; -export * as 'channels/my-favorites' from './endpoints/channels/my-favorites.js'; -export * as 'channels/owned' from './endpoints/channels/owned.js'; -export * as 'channels/search' from './endpoints/channels/search.js'; -export * as 'channels/show' from './endpoints/channels/show.js'; -export * as 'channels/timeline' from './endpoints/channels/timeline.js'; -export * as 'channels/unfavorite' from './endpoints/channels/unfavorite.js'; -export * as 'channels/unfollow' from './endpoints/channels/unfollow.js'; -export * as 'channels/update' from './endpoints/channels/update.js'; -export * as 'charts/active-users' from './endpoints/charts/active-users.js'; -export * as 'charts/ap-request' from './endpoints/charts/ap-request.js'; -export * as 'charts/drive' from './endpoints/charts/drive.js'; -export * as 'charts/federation' from './endpoints/charts/federation.js'; -export * as 'charts/instance' from './endpoints/charts/instance.js'; -export * as 'charts/notes' from './endpoints/charts/notes.js'; -export * as 'charts/user/drive' from './endpoints/charts/user/drive.js'; -export * as 'charts/user/following' from './endpoints/charts/user/following.js'; -export * as 'charts/user/notes' from './endpoints/charts/user/notes.js'; -export * as 'charts/user/pv' from './endpoints/charts/user/pv.js'; -export * as 'charts/user/reactions' from './endpoints/charts/user/reactions.js'; -export * as 'charts/users' from './endpoints/charts/users.js'; -export * as 'clips/add-note' from './endpoints/clips/add-note.js'; -export * as 'clips/create' from './endpoints/clips/create.js'; -export * as 'clips/delete' from './endpoints/clips/delete.js'; -export * as 'clips/favorite' from './endpoints/clips/favorite.js'; -export * as 'clips/list' from './endpoints/clips/list.js'; -export * as 'clips/my-favorites' from './endpoints/clips/my-favorites.js'; -export * as 'clips/notes' from './endpoints/clips/notes.js'; -export * as 'clips/remove-note' from './endpoints/clips/remove-note.js'; -export * as 'clips/show' from './endpoints/clips/show.js'; -export * as 'clips/unfavorite' from './endpoints/clips/unfavorite.js'; -export * as 'clips/update' from './endpoints/clips/update.js'; -export * as 'drive' from './endpoints/drive.js'; -export * as 'drive/files' from './endpoints/drive/files.js'; -export * as 'drive/files/attached-notes' from './endpoints/drive/files/attached-notes.js'; -export * as 'drive/files/check-existence' from './endpoints/drive/files/check-existence.js'; -export * as 'drive/files/create' from './endpoints/drive/files/create.js'; -export * as 'drive/files/delete' from './endpoints/drive/files/delete.js'; -export * as 'drive/files/find' from './endpoints/drive/files/find.js'; -export * as 'drive/files/find-by-hash' from './endpoints/drive/files/find-by-hash.js'; -export * as 'drive/files/show' from './endpoints/drive/files/show.js'; -export * as 'drive/files/update' from './endpoints/drive/files/update.js'; -export * as 'drive/files/move-bulk' from './endpoints/drive/files/move-bulk.js'; -export * as 'drive/files/upload-from-url' from './endpoints/drive/files/upload-from-url.js'; -export * as 'drive/folders' from './endpoints/drive/folders.js'; -export * as 'drive/folders/create' from './endpoints/drive/folders/create.js'; -export * as 'drive/folders/delete' from './endpoints/drive/folders/delete.js'; -export * as 'drive/folders/find' from './endpoints/drive/folders/find.js'; -export * as 'drive/folders/show' from './endpoints/drive/folders/show.js'; -export * as 'drive/folders/update' from './endpoints/drive/folders/update.js'; -export * as 'drive/stream' from './endpoints/drive/stream.js'; -export * as 'email-address/available' from './endpoints/email-address/available.js'; -export * as 'emoji' from './endpoints/emoji.js'; -export * as 'emojis' from './endpoints/emojis.js'; -export * as 'endpoint' from './endpoints/endpoint.js'; -export * as 'endpoints' from './endpoints/endpoints.js'; -export * as 'export-custom-emojis' from './endpoints/export-custom-emojis.js'; -export * as 'federation/followers' from './endpoints/federation/followers.js'; -export * as 'federation/following' from './endpoints/federation/following.js'; -export * as 'federation/instances' from './endpoints/federation/instances.js'; -export * as 'federation/show-instance' from './endpoints/federation/show-instance.js'; -export * as 'federation/stats' from './endpoints/federation/stats.js'; -export * as 'federation/update-remote-user' from './endpoints/federation/update-remote-user.js'; -export * as 'federation/users' from './endpoints/federation/users.js'; -export * as 'fetch-external-resources' from './endpoints/fetch-external-resources.js'; -export * as 'fetch-rss' from './endpoints/fetch-rss.js'; -export * as 'flash/create' from './endpoints/flash/create.js'; -export * as 'flash/delete' from './endpoints/flash/delete.js'; -export * as 'flash/featured' from './endpoints/flash/featured.js'; -export * as 'flash/like' from './endpoints/flash/like.js'; -export * as 'flash/my' from './endpoints/flash/my.js'; -export * as 'flash/my-likes' from './endpoints/flash/my-likes.js'; -export * as 'flash/show' from './endpoints/flash/show.js'; -export * as 'flash/unlike' from './endpoints/flash/unlike.js'; -export * as 'flash/update' from './endpoints/flash/update.js'; -export * as 'following/create' from './endpoints/following/create.js'; -export * as 'following/delete' from './endpoints/following/delete.js'; -export * as 'following/invalidate' from './endpoints/following/invalidate.js'; -export * as 'following/requests/accept' from './endpoints/following/requests/accept.js'; -export * as 'following/requests/cancel' from './endpoints/following/requests/cancel.js'; -export * as 'following/requests/list' from './endpoints/following/requests/list.js'; -export * as 'following/requests/reject' from './endpoints/following/requests/reject.js'; -export * as 'following/requests/sent' from './endpoints/following/requests/sent.js'; -export * as 'following/update' from './endpoints/following/update.js'; -export * as 'following/update-all' from './endpoints/following/update-all.js'; -export * as 'gallery/featured' from './endpoints/gallery/featured.js'; -export * as 'gallery/popular' from './endpoints/gallery/popular.js'; -export * as 'gallery/posts' from './endpoints/gallery/posts.js'; -export * as 'gallery/posts/create' from './endpoints/gallery/posts/create.js'; -export * as 'gallery/posts/delete' from './endpoints/gallery/posts/delete.js'; -export * as 'gallery/posts/like' from './endpoints/gallery/posts/like.js'; -export * as 'gallery/posts/show' from './endpoints/gallery/posts/show.js'; -export * as 'gallery/posts/unlike' from './endpoints/gallery/posts/unlike.js'; -export * as 'gallery/posts/update' from './endpoints/gallery/posts/update.js'; -export * as 'get-avatar-decorations' from './endpoints/get-avatar-decorations.js'; -export * as 'get-online-users-count' from './endpoints/get-online-users-count.js'; -export * as 'hashtags/list' from './endpoints/hashtags/list.js'; -export * as 'hashtags/search' from './endpoints/hashtags/search.js'; -export * as 'hashtags/show' from './endpoints/hashtags/show.js'; -export * as 'hashtags/trend' from './endpoints/hashtags/trend.js'; -export * as 'hashtags/users' from './endpoints/hashtags/users.js'; -export * as 'i' from './endpoints/i.js'; -export * as 'i/2fa/done' from './endpoints/i/2fa/done.js'; -export * as 'i/2fa/key-done' from './endpoints/i/2fa/key-done.js'; -export * as 'i/2fa/password-less' from './endpoints/i/2fa/password-less.js'; -export * as 'i/2fa/register' from './endpoints/i/2fa/register.js'; -export * as 'i/2fa/register-key' from './endpoints/i/2fa/register-key.js'; -export * as 'i/2fa/remove-key' from './endpoints/i/2fa/remove-key.js'; -export * as 'i/2fa/unregister' from './endpoints/i/2fa/unregister.js'; -export * as 'i/2fa/update-key' from './endpoints/i/2fa/update-key.js'; -export * as 'i/apps' from './endpoints/i/apps.js'; -export * as 'i/authorized-apps' from './endpoints/i/authorized-apps.js'; -export * as 'i/change-password' from './endpoints/i/change-password.js'; -export * as 'i/claim-achievement' from './endpoints/i/claim-achievement.js'; -export * as 'i/delete-account' from './endpoints/i/delete-account.js'; -export * as 'i/export-antennas' from './endpoints/i/export-antennas.js'; -export * as 'i/export-blocking' from './endpoints/i/export-blocking.js'; -export * as 'i/export-clips' from './endpoints/i/export-clips.js'; -export * as 'i/export-favorites' from './endpoints/i/export-favorites.js'; -export * as 'i/export-following' from './endpoints/i/export-following.js'; -export * as 'i/export-mute' from './endpoints/i/export-mute.js'; -export * as 'i/export-notes' from './endpoints/i/export-notes.js'; -export * as 'i/export-user-lists' from './endpoints/i/export-user-lists.js'; -export * as 'i/favorites' from './endpoints/i/favorites.js'; -export * as 'i/gallery/likes' from './endpoints/i/gallery/likes.js'; -export * as 'i/gallery/posts' from './endpoints/i/gallery/posts.js'; -export * as 'i/import-antennas' from './endpoints/i/import-antennas.js'; -export * as 'i/import-blocking' from './endpoints/i/import-blocking.js'; -export * as 'i/import-following' from './endpoints/i/import-following.js'; -export * as 'i/import-muting' from './endpoints/i/import-muting.js'; -export * as 'i/import-user-lists' from './endpoints/i/import-user-lists.js'; -export * as 'i/move' from './endpoints/i/move.js'; -export * as 'i/notifications' from './endpoints/i/notifications.js'; -export * as 'i/notifications-grouped' from './endpoints/i/notifications-grouped.js'; -export * as 'i/page-likes' from './endpoints/i/page-likes.js'; -export * as 'i/pages' from './endpoints/i/pages.js'; -export * as 'i/pin' from './endpoints/i/pin.js'; -export * as 'i/read-announcement' from './endpoints/i/read-announcement.js'; -export * as 'i/regenerate-token' from './endpoints/i/regenerate-token.js'; -export * as 'i/registry/get' from './endpoints/i/registry/get.js'; -export * as 'i/registry/get-all' from './endpoints/i/registry/get-all.js'; -export * as 'i/registry/get-detail' from './endpoints/i/registry/get-detail.js'; -export * as 'i/registry/keys' from './endpoints/i/registry/keys.js'; -export * as 'i/registry/keys-with-type' from './endpoints/i/registry/keys-with-type.js'; -export * as 'i/registry/remove' from './endpoints/i/registry/remove.js'; -export * as 'i/registry/scopes-with-domain' from './endpoints/i/registry/scopes-with-domain.js'; -export * as 'i/registry/set' from './endpoints/i/registry/set.js'; -export * as 'i/revoke-token' from './endpoints/i/revoke-token.js'; -export * as 'i/signin-history' from './endpoints/i/signin-history.js'; -export * as 'i/unpin' from './endpoints/i/unpin.js'; -export * as 'i/update' from './endpoints/i/update.js'; -export * as 'i/update-email' from './endpoints/i/update-email.js'; -export * as 'i/webhooks/create' from './endpoints/i/webhooks/create.js'; -export * as 'i/webhooks/delete' from './endpoints/i/webhooks/delete.js'; -export * as 'i/webhooks/list' from './endpoints/i/webhooks/list.js'; -export * as 'i/webhooks/show' from './endpoints/i/webhooks/show.js'; -export * as 'i/webhooks/test' from './endpoints/i/webhooks/test.js'; -export * as 'i/webhooks/update' from './endpoints/i/webhooks/update.js'; -export * as 'invite/create' from './endpoints/invite/create.js'; -export * as 'invite/delete' from './endpoints/invite/delete.js'; -export * as 'invite/limit' from './endpoints/invite/limit.js'; -export * as 'invite/list' from './endpoints/invite/list.js'; -export * as 'meta' from './endpoints/meta.js'; -export * as 'miauth/gen-token' from './endpoints/miauth/gen-token.js'; -export * as 'mute/create' from './endpoints/mute/create.js'; -export * as 'mute/delete' from './endpoints/mute/delete.js'; -export * as 'mute/list' from './endpoints/mute/list.js'; -export * as 'my/apps' from './endpoints/my/apps.js'; -export * as 'notes' from './endpoints/notes.js'; -export * as 'notes/children' from './endpoints/notes/children.js'; -export * as 'notes/clips' from './endpoints/notes/clips.js'; -export * as 'notes/conversation' from './endpoints/notes/conversation.js'; -export * as 'notes/create' from './endpoints/notes/create.js'; -export * as 'notes/delete' from './endpoints/notes/delete.js'; -export * as 'notes/favorites/create' from './endpoints/notes/favorites/create.js'; -export * as 'notes/favorites/delete' from './endpoints/notes/favorites/delete.js'; -export * as 'notes/featured' from './endpoints/notes/featured.js'; -export * as 'notes/global-timeline' from './endpoints/notes/global-timeline.js'; -export * as 'notes/hybrid-timeline' from './endpoints/notes/hybrid-timeline.js'; -export * as 'notes/local-timeline' from './endpoints/notes/local-timeline.js'; -export * as 'notes/mentions' from './endpoints/notes/mentions.js'; -export * as 'notes/polls/recommendation' from './endpoints/notes/polls/recommendation.js'; -export * as 'notes/polls/vote' from './endpoints/notes/polls/vote.js'; -export * as 'notes/reactions' from './endpoints/notes/reactions.js'; -export * as 'notes/reactions/create' from './endpoints/notes/reactions/create.js'; -export * as 'notes/reactions/delete' from './endpoints/notes/reactions/delete.js'; -export * as 'notes/renotes' from './endpoints/notes/renotes.js'; -export * as 'notes/replies' from './endpoints/notes/replies.js'; -export * as 'notes/search' from './endpoints/notes/search.js'; -export * as 'notes/search-by-tag' from './endpoints/notes/search-by-tag.js'; -export * as 'notes/show' from './endpoints/notes/show.js'; -export * as 'notes/show-partial-bulk' from './endpoints/notes/show-partial-bulk.js'; -export * as 'notes/state' from './endpoints/notes/state.js'; -export * as 'notes/thread-muting/create' from './endpoints/notes/thread-muting/create.js'; -export * as 'notes/thread-muting/delete' from './endpoints/notes/thread-muting/delete.js'; -export * as 'notes/timeline' from './endpoints/notes/timeline.js'; -export * as 'notes/translate' from './endpoints/notes/translate.js'; -export * as 'notes/unrenote' from './endpoints/notes/unrenote.js'; -export * as 'notes/user-list-timeline' from './endpoints/notes/user-list-timeline.js'; -export * as 'notifications/create' from './endpoints/notifications/create.js'; -export * as 'notifications/flush' from './endpoints/notifications/flush.js'; -export * as 'notifications/mark-all-as-read' from './endpoints/notifications/mark-all-as-read.js'; -export * as 'notifications/test-notification' from './endpoints/notifications/test-notification.js'; -export * as 'page-push' from './endpoints/page-push.js'; -export * as 'pages/create' from './endpoints/pages/create.js'; -export * as 'pages/delete' from './endpoints/pages/delete.js'; -export * as 'pages/featured' from './endpoints/pages/featured.js'; -export * as 'pages/like' from './endpoints/pages/like.js'; -export * as 'pages/show' from './endpoints/pages/show.js'; -export * as 'pages/unlike' from './endpoints/pages/unlike.js'; -export * as 'pages/update' from './endpoints/pages/update.js'; -export * as 'ping' from './endpoints/ping.js'; -export * as 'pinned-users' from './endpoints/pinned-users.js'; -export * as 'promo/read' from './endpoints/promo/read.js'; -export * as 'renote-mute/create' from './endpoints/renote-mute/create.js'; -export * as 'renote-mute/delete' from './endpoints/renote-mute/delete.js'; -export * as 'renote-mute/list' from './endpoints/renote-mute/list.js'; -export * as 'request-reset-password' from './endpoints/request-reset-password.js'; -export * as 'reset-db' from './endpoints/reset-db.js'; -export * as 'reset-password' from './endpoints/reset-password.js'; -export * as 'retention' from './endpoints/retention.js'; -export * as 'reversi/cancel-match' from './endpoints/reversi/cancel-match.js'; -export * as 'reversi/games' from './endpoints/reversi/games.js'; -export * as 'reversi/invitations' from './endpoints/reversi/invitations.js'; -export * as 'reversi/match' from './endpoints/reversi/match.js'; -export * as 'reversi/show-game' from './endpoints/reversi/show-game.js'; -export * as 'reversi/surrender' from './endpoints/reversi/surrender.js'; -export * as 'reversi/verify' from './endpoints/reversi/verify.js'; -export * as 'roles/list' from './endpoints/roles/list.js'; -export * as 'roles/notes' from './endpoints/roles/notes.js'; -export * as 'roles/show' from './endpoints/roles/show.js'; -export * as 'roles/users' from './endpoints/roles/users.js'; -export * as 'server-info' from './endpoints/server-info.js'; -export * as 'stats' from './endpoints/stats.js'; -export * as 'sw/register' from './endpoints/sw/register.js'; -export * as 'sw/show-registration' from './endpoints/sw/show-registration.js'; -export * as 'sw/unregister' from './endpoints/sw/unregister.js'; -export * as 'sw/update-registration' from './endpoints/sw/update-registration.js'; -export * as 'test' from './endpoints/test.js'; -export * as 'username/available' from './endpoints/username/available.js'; -export * as 'users' from './endpoints/users.js'; -export * as 'users/achievements' from './endpoints/users/achievements.js'; -export * as 'users/clips' from './endpoints/users/clips.js'; -export * as 'users/featured-notes' from './endpoints/users/featured-notes.js'; -export * as 'users/flashs' from './endpoints/users/flashs.js'; -export * as 'users/followers' from './endpoints/users/followers.js'; -export * as 'users/following' from './endpoints/users/following.js'; -export * as 'users/gallery/posts' from './endpoints/users/gallery/posts.js'; -export * as 'users/get-frequently-replied-users' from './endpoints/users/get-frequently-replied-users.js'; -export * as 'users/lists/create' from './endpoints/users/lists/create.js'; -export * as 'users/lists/create-from-public' from './endpoints/users/lists/create-from-public.js'; -export * as 'users/lists/delete' from './endpoints/users/lists/delete.js'; -export * as 'users/lists/favorite' from './endpoints/users/lists/favorite.js'; -export * as 'users/lists/get-memberships' from './endpoints/users/lists/get-memberships.js'; -export * as 'users/lists/list' from './endpoints/users/lists/list.js'; -export * as 'users/lists/pull' from './endpoints/users/lists/pull.js'; -export * as 'users/lists/push' from './endpoints/users/lists/push.js'; -export * as 'users/lists/show' from './endpoints/users/lists/show.js'; -export * as 'users/lists/unfavorite' from './endpoints/users/lists/unfavorite.js'; -export * as 'users/lists/update' from './endpoints/users/lists/update.js'; -export * as 'users/lists/update-membership' from './endpoints/users/lists/update-membership.js'; -export * as 'users/notes' from './endpoints/users/notes.js'; -export * as 'users/pages' from './endpoints/users/pages.js'; -export * as 'users/reactions' from './endpoints/users/reactions.js'; -export * as 'users/recommendation' from './endpoints/users/recommendation.js'; -export * as 'users/relation' from './endpoints/users/relation.js'; -export * as 'users/report-abuse' from './endpoints/users/report-abuse.js'; -export * as 'users/search' from './endpoints/users/search.js'; -export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js'; -export * as 'users/show' from './endpoints/users/show.js'; -export * as 'users/update-memo' from './endpoints/users/update-memo.js'; -export * as 'chat/messages/create-to-user' from './endpoints/chat/messages/create-to-user.js'; -export * as 'chat/messages/create-to-room' from './endpoints/chat/messages/create-to-room.js'; -export * as 'chat/messages/delete' from './endpoints/chat/messages/delete.js'; -export * as 'chat/messages/show' from './endpoints/chat/messages/show.js'; -export * as 'chat/messages/react' from './endpoints/chat/messages/react.js'; -export * as 'chat/messages/unreact' from './endpoints/chat/messages/unreact.js'; -export * as 'chat/messages/user-timeline' from './endpoints/chat/messages/user-timeline.js'; -export * as 'chat/messages/room-timeline' from './endpoints/chat/messages/room-timeline.js'; -export * as 'chat/messages/search' from './endpoints/chat/messages/search.js'; -export * as 'chat/rooms/create' from './endpoints/chat/rooms/create.js'; -export * as 'chat/rooms/delete' from './endpoints/chat/rooms/delete.js'; -export * as 'chat/rooms/join' from './endpoints/chat/rooms/join.js'; -export * as 'chat/rooms/leave' from './endpoints/chat/rooms/leave.js'; -export * as 'chat/rooms/mute' from './endpoints/chat/rooms/mute.js'; -export * as 'chat/rooms/show' from './endpoints/chat/rooms/show.js'; -export * as 'chat/rooms/owned' from './endpoints/chat/rooms/owned.js'; -export * as 'chat/rooms/joining' from './endpoints/chat/rooms/joining.js'; -export * as 'chat/rooms/update' from './endpoints/chat/rooms/update.js'; -export * as 'chat/rooms/members' from './endpoints/chat/rooms/members.js'; -export * as 'chat/rooms/invitations/create' from './endpoints/chat/rooms/invitations/create.js'; -export * as 'chat/rooms/invitations/ignore' from './endpoints/chat/rooms/invitations/ignore.js'; -export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitations/inbox.js'; -export * as 'chat/rooms/invitations/outbox' from './endpoints/chat/rooms/invitations/outbox.js'; -export * as 'chat/history' from './endpoints/chat/history.js'; -export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js'; diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 03c729ed18..94206ef870 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -1,14 +1,683 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ +import type { Schema } from '@/misc/json-schema.js'; +import { RolePolicies } from '@/core/RoleService.js'; -import { permissions } from 'misskey-js'; -import type { KeyOf, Schema } from '@/misc/json-schema.js'; +import * as ep___admin_meta from './endpoints/admin/meta.js'; +import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js'; +import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js'; +import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js'; +import * as ep___admin_ad_create from './endpoints/admin/ad/create.js'; +import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js'; +import * as ep___admin_ad_list from './endpoints/admin/ad/list.js'; +import * as ep___admin_ad_update from './endpoints/admin/ad/update.js'; +import * as ep___admin_announcements_create from './endpoints/admin/announcements/create.js'; +import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js'; +import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js'; +import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js'; +import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; +import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; +import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; +import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; +import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js'; +import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js'; +import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js'; +import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js'; +import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js'; +import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js'; +import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js'; +import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js'; +import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js'; +import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js'; +import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js'; +import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js'; +import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js'; +import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js'; +import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js'; +import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; +import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js'; +import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js'; +import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; +import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; +import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js'; +import * as ep___invite from './endpoints/invite.js'; +import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; +import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; +import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; +import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js'; +import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.js'; +import * as ep___admin_queue_stats from './endpoints/admin/queue/stats.js'; +import * as ep___admin_relays_add from './endpoints/admin/relays/add.js'; +import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; +import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js'; +import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js'; +import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js'; +import * as ep___admin_sendEmail from './endpoints/admin/send-email.js'; +import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; +import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; +import * as ep___admin_showUser from './endpoints/admin/show-user.js'; +import * as ep___admin_showUsers from './endpoints/admin/show-users.js'; +import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js'; +import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; +import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; +import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; +import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js'; +import * as ep___admin_roles_create from './endpoints/admin/roles/create.js'; +import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js'; +import * as ep___admin_roles_list from './endpoints/admin/roles/list.js'; +import * as ep___admin_roles_show from './endpoints/admin/roles/show.js'; +import * as ep___admin_roles_update from './endpoints/admin/roles/update.js'; +import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; +import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; +import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js'; +import * as ep___admin_roles_users from './endpoints/admin/roles/users.js'; +import * as ep___announcements from './endpoints/announcements.js'; +import * as ep___antennas_create from './endpoints/antennas/create.js'; +import * as ep___antennas_delete from './endpoints/antennas/delete.js'; +import * as ep___antennas_list from './endpoints/antennas/list.js'; +import * as ep___antennas_notes from './endpoints/antennas/notes.js'; +import * as ep___antennas_show from './endpoints/antennas/show.js'; +import * as ep___antennas_update from './endpoints/antennas/update.js'; +import * as ep___ap_get from './endpoints/ap/get.js'; +import * as ep___ap_show from './endpoints/ap/show.js'; +import * as ep___app_create from './endpoints/app/create.js'; +import * as ep___app_show from './endpoints/app/show.js'; +import * as ep___auth_accept from './endpoints/auth/accept.js'; +import * as ep___auth_session_generate from './endpoints/auth/session/generate.js'; +import * as ep___auth_session_show from './endpoints/auth/session/show.js'; +import * as ep___auth_session_userkey from './endpoints/auth/session/userkey.js'; +import * as ep___blocking_create from './endpoints/blocking/create.js'; +import * as ep___blocking_delete from './endpoints/blocking/delete.js'; +import * as ep___blocking_list from './endpoints/blocking/list.js'; +import * as ep___channels_create from './endpoints/channels/create.js'; +import * as ep___channels_featured from './endpoints/channels/featured.js'; +import * as ep___channels_follow from './endpoints/channels/follow.js'; +import * as ep___channels_followed from './endpoints/channels/followed.js'; +import * as ep___channels_owned from './endpoints/channels/owned.js'; +import * as ep___channels_show from './endpoints/channels/show.js'; +import * as ep___channels_timeline from './endpoints/channels/timeline.js'; +import * as ep___channels_unfollow from './endpoints/channels/unfollow.js'; +import * as ep___channels_update from './endpoints/channels/update.js'; +import * as ep___channels_favorite from './endpoints/channels/favorite.js'; +import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js'; +import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js'; +import * as ep___channels_search from './endpoints/channels/search.js'; +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_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'; +import * as ep___charts_user_following from './endpoints/charts/user/following.js'; +import * as ep___charts_user_notes from './endpoints/charts/user/notes.js'; +import * as ep___charts_user_pv from './endpoints/charts/user/pv.js'; +import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js'; +import * as ep___charts_users from './endpoints/charts/users.js'; +import * as ep___clips_addNote from './endpoints/clips/add-note.js'; +import * as ep___clips_removeNote from './endpoints/clips/remove-note.js'; +import * as ep___clips_create from './endpoints/clips/create.js'; +import * as ep___clips_delete from './endpoints/clips/delete.js'; +import * as ep___clips_list from './endpoints/clips/list.js'; +import * as ep___clips_notes from './endpoints/clips/notes.js'; +import * as ep___clips_show from './endpoints/clips/show.js'; +import * as ep___clips_update from './endpoints/clips/update.js'; +import * as ep___clips_favorite from './endpoints/clips/favorite.js'; +import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js'; +import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js'; +import * as ep___drive from './endpoints/drive.js'; +import * as ep___drive_files from './endpoints/drive/files.js'; +import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js'; +import * as ep___drive_files_checkExistence from './endpoints/drive/files/check-existence.js'; +import * as ep___drive_files_create from './endpoints/drive/files/create.js'; +import * as ep___drive_files_delete from './endpoints/drive/files/delete.js'; +import * as ep___drive_files_findByHash from './endpoints/drive/files/find-by-hash.js'; +import * as ep___drive_files_find from './endpoints/drive/files/find.js'; +import * as ep___drive_files_show from './endpoints/drive/files/show.js'; +import * as ep___drive_files_update from './endpoints/drive/files/update.js'; +import * as ep___drive_files_uploadFromUrl from './endpoints/drive/files/upload-from-url.js'; +import * as ep___drive_folders from './endpoints/drive/folders.js'; +import * as ep___drive_folders_create from './endpoints/drive/folders/create.js'; +import * as ep___drive_folders_delete from './endpoints/drive/folders/delete.js'; +import * as ep___drive_folders_find from './endpoints/drive/folders/find.js'; +import * as ep___drive_folders_show from './endpoints/drive/folders/show.js'; +import * as ep___drive_folders_update from './endpoints/drive/folders/update.js'; +import * as ep___drive_stream from './endpoints/drive/stream.js'; +import * as ep___emailAddress_available from './endpoints/email-address/available.js'; +import * as ep___endpoint from './endpoints/endpoint.js'; +import * as ep___endpoints from './endpoints/endpoints.js'; +import * as ep___exportCustomEmojis from './endpoints/export-custom-emojis.js'; +import * as ep___federation_followers from './endpoints/federation/followers.js'; +import * as ep___federation_following from './endpoints/federation/following.js'; +import * as ep___federation_instances from './endpoints/federation/instances.js'; +import * as ep___federation_showInstance from './endpoints/federation/show-instance.js'; +import * as ep___federation_updateRemoteUser from './endpoints/federation/update-remote-user.js'; +import * as ep___federation_users from './endpoints/federation/users.js'; +import * as ep___federation_stats from './endpoints/federation/stats.js'; +import * as ep___following_create from './endpoints/following/create.js'; +import * as ep___following_delete from './endpoints/following/delete.js'; +import * as ep___following_invalidate from './endpoints/following/invalidate.js'; +import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; +import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; +import * as ep___following_requests_list from './endpoints/following/requests/list.js'; +import * as ep___following_requests_reject from './endpoints/following/requests/reject.js'; +import * as ep___gallery_featured from './endpoints/gallery/featured.js'; +import * as ep___gallery_popular from './endpoints/gallery/popular.js'; +import * as ep___gallery_posts from './endpoints/gallery/posts.js'; +import * as ep___gallery_posts_create from './endpoints/gallery/posts/create.js'; +import * as ep___gallery_posts_delete from './endpoints/gallery/posts/delete.js'; +import * as ep___gallery_posts_like from './endpoints/gallery/posts/like.js'; +import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js'; +import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js'; +import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js'; +import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js'; +import * as ep___hashtags_list from './endpoints/hashtags/list.js'; +import * as ep___hashtags_search from './endpoints/hashtags/search.js'; +import * as ep___hashtags_show from './endpoints/hashtags/show.js'; +import * as ep___hashtags_trend from './endpoints/hashtags/trend.js'; +import * as ep___hashtags_users from './endpoints/hashtags/users.js'; +import * as ep___i from './endpoints/i.js'; +import * as ep___i_2fa_done from './endpoints/i/2fa/done.js'; +import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js'; +import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js'; +import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js'; +import * as ep___i_2fa_register from './endpoints/i/2fa/register.js'; +import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js'; +import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js'; +import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js'; +import * as ep___i_apps from './endpoints/i/apps.js'; +import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js'; +import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js'; +import * as ep___i_changePassword from './endpoints/i/change-password.js'; +import * as ep___i_deleteAccount from './endpoints/i/delete-account.js'; +import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; +import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; +import * as ep___i_exportMute from './endpoints/i/export-mute.js'; +import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; +import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js'; +import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; +import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; +import * as ep___i_favorites from './endpoints/i/favorites.js'; +import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js'; +import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js'; +import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js'; +import * as ep___i_importBlocking from './endpoints/i/import-blocking.js'; +import * as ep___i_importFollowing from './endpoints/i/import-following.js'; +import * as ep___i_importMuting from './endpoints/i/import-muting.js'; +import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js'; +import * as ep___i_importAntennas from './endpoints/i/import-antennas.js'; +import * as ep___i_notifications from './endpoints/i/notifications.js'; +import * as ep___i_pageLikes from './endpoints/i/page-likes.js'; +import * as ep___i_pages from './endpoints/i/pages.js'; +import * as ep___i_pin from './endpoints/i/pin.js'; +import * as ep___i_readAllUnreadNotes from './endpoints/i/read-all-unread-notes.js'; +import * as ep___i_readAnnouncement from './endpoints/i/read-announcement.js'; +import * as ep___i_regenerateToken from './endpoints/i/regenerate-token.js'; +import * as ep___i_registry_getAll from './endpoints/i/registry/get-all.js'; +import * as ep___i_registry_getDetail from './endpoints/i/registry/get-detail.js'; +import * as ep___i_registry_get from './endpoints/i/registry/get.js'; +import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js'; +import * as ep___i_registry_keys from './endpoints/i/registry/keys.js'; +import * as ep___i_registry_remove from './endpoints/i/registry/remove.js'; +import * as ep___i_registry_scopes from './endpoints/i/registry/scopes.js'; +import * as ep___i_registry_set from './endpoints/i/registry/set.js'; +import * as ep___i_revokeToken from './endpoints/i/revoke-token.js'; +import * as ep___i_signinHistory from './endpoints/i/signin-history.js'; +import * as ep___i_unpin from './endpoints/i/unpin.js'; +import * as ep___i_updateEmail from './endpoints/i/update-email.js'; +import * as ep___i_update from './endpoints/i/update.js'; +import * as ep___i_move from './endpoints/i/move.js'; +import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js'; +import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; +import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; +import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; +import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; +import * as ep___meta from './endpoints/meta.js'; +import * as ep___emojis from './endpoints/emojis.js'; +import * as ep___emoji from './endpoints/emoji.js'; +import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; +import * as ep___mute_create from './endpoints/mute/create.js'; +import * as ep___mute_delete from './endpoints/mute/delete.js'; +import * as ep___mute_list from './endpoints/mute/list.js'; +import * as ep___renoteMute_create from './endpoints/renote-mute/create.js'; +import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js'; +import * as ep___renoteMute_list from './endpoints/renote-mute/list.js'; +import * as ep___my_apps from './endpoints/my/apps.js'; +import * as ep___notes from './endpoints/notes.js'; +import * as ep___notes_children from './endpoints/notes/children.js'; +import * as ep___notes_clips from './endpoints/notes/clips.js'; +import * as ep___notes_conversation from './endpoints/notes/conversation.js'; +import * as ep___notes_create from './endpoints/notes/create.js'; +import * as ep___notes_delete from './endpoints/notes/delete.js'; +import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; +import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; +import * as ep___notes_featured from './endpoints/notes/featured.js'; +import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; +import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; +import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; +import * as ep___notes_mentions from './endpoints/notes/mentions.js'; +import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js'; +import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js'; +import * as ep___notes_reactions from './endpoints/notes/reactions.js'; +import * as ep___notes_reactions_create from './endpoints/notes/reactions/create.js'; +import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js'; +import * as ep___notes_renotes from './endpoints/notes/renotes.js'; +import * as ep___notes_replies from './endpoints/notes/replies.js'; +import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js'; +import * as ep___notes_search from './endpoints/notes/search.js'; +import * as ep___notes_show from './endpoints/notes/show.js'; +import * as ep___notes_state from './endpoints/notes/state.js'; +import * as ep___notes_threadMuting_create from './endpoints/notes/thread-muting/create.js'; +import * as ep___notes_threadMuting_delete from './endpoints/notes/thread-muting/delete.js'; +import * as ep___notes_timeline from './endpoints/notes/timeline.js'; +import * as ep___notes_translate from './endpoints/notes/translate.js'; +import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; +import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; +import * as ep___notifications_create from './endpoints/notifications/create.js'; +import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; +import * as ep___pagePush from './endpoints/page-push.js'; +import * as ep___pages_create from './endpoints/pages/create.js'; +import * as ep___pages_delete from './endpoints/pages/delete.js'; +import * as ep___pages_featured from './endpoints/pages/featured.js'; +import * as ep___pages_like from './endpoints/pages/like.js'; +import * as ep___pages_show from './endpoints/pages/show.js'; +import * as ep___pages_unlike from './endpoints/pages/unlike.js'; +import * as ep___pages_update from './endpoints/pages/update.js'; +import * as ep___flash_create from './endpoints/flash/create.js'; +import * as ep___flash_delete from './endpoints/flash/delete.js'; +import * as ep___flash_featured from './endpoints/flash/featured.js'; +import * as ep___flash_like from './endpoints/flash/like.js'; +import * as ep___flash_show from './endpoints/flash/show.js'; +import * as ep___flash_unlike from './endpoints/flash/unlike.js'; +import * as ep___flash_update from './endpoints/flash/update.js'; +import * as ep___flash_my from './endpoints/flash/my.js'; +import * as ep___flash_myLikes from './endpoints/flash/my-likes.js'; +import * as ep___ping from './endpoints/ping.js'; +import * as ep___pinnedUsers from './endpoints/pinned-users.js'; +import * as ep___promo_read from './endpoints/promo/read.js'; +import * as ep___roles_list from './endpoints/roles/list.js'; +import * as ep___roles_show from './endpoints/roles/show.js'; +import * as ep___roles_users from './endpoints/roles/users.js'; +import * as ep___roles_notes from './endpoints/roles/notes.js'; +import * as ep___requestResetPassword from './endpoints/request-reset-password.js'; +import * as ep___resetDb from './endpoints/reset-db.js'; +import * as ep___resetPassword from './endpoints/reset-password.js'; +import * as ep___serverInfo from './endpoints/server-info.js'; +import * as ep___stats from './endpoints/stats.js'; +import * as ep___sw_show_registration from './endpoints/sw/show-registration.js'; +import * as ep___sw_update_registration from './endpoints/sw/update-registration.js'; +import * as ep___sw_register from './endpoints/sw/register.js'; +import * as ep___sw_unregister from './endpoints/sw/unregister.js'; +import * as ep___test from './endpoints/test.js'; +import * as ep___username_available from './endpoints/username/available.js'; +import * as ep___users from './endpoints/users.js'; +import * as ep___users_clips from './endpoints/users/clips.js'; +import * as ep___users_followers from './endpoints/users/followers.js'; +import * as ep___users_following from './endpoints/users/following.js'; +import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; +import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; +import * as ep___users_lists_create from './endpoints/users/lists/create.js'; +import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; +import * as ep___users_lists_list from './endpoints/users/lists/list.js'; +import * as ep___users_lists_pull from './endpoints/users/lists/pull.js'; +import * as ep___users_lists_push from './endpoints/users/lists/push.js'; +import * as ep___users_lists_show from './endpoints/users/lists/show.js'; +import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js'; +import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js'; +import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js'; +import * as ep___users_lists_update from './endpoints/users/lists/update.js'; +import * as ep___users_notes from './endpoints/users/notes.js'; +import * as ep___users_pages from './endpoints/users/pages.js'; +import * as ep___users_reactions from './endpoints/users/reactions.js'; +import * as ep___users_recommendation from './endpoints/users/recommendation.js'; +import * as ep___users_relation from './endpoints/users/relation.js'; +import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js'; +import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js'; +import * as ep___users_search from './endpoints/users/search.js'; +import * as ep___users_show from './endpoints/users/show.js'; +import * as ep___users_achievements from './endpoints/users/achievements.js'; +import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; +import * as ep___fetchRss from './endpoints/fetch-rss.js'; +import * as ep___retention from './endpoints/retention.js'; -import * as endpointsObject from './endpoint-list.js'; +const eps = [ + ['admin/meta', ep___admin_meta], + ['admin/abuse-user-reports', ep___admin_abuseUserReports], + ['admin/accounts/create', ep___admin_accounts_create], + ['admin/accounts/delete', ep___admin_accounts_delete], + ['admin/ad/create', ep___admin_ad_create], + ['admin/ad/delete', ep___admin_ad_delete], + ['admin/ad/list', ep___admin_ad_list], + ['admin/ad/update', ep___admin_ad_update], + ['admin/announcements/create', ep___admin_announcements_create], + ['admin/announcements/delete', ep___admin_announcements_delete], + ['admin/announcements/list', ep___admin_announcements_list], + ['admin/announcements/update', ep___admin_announcements_update], + ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser], + ['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles], + ['admin/drive/cleanup', ep___admin_drive_cleanup], + ['admin/drive/files', ep___admin_drive_files], + ['admin/drive/show-file', ep___admin_drive_showFile], + ['admin/emoji/add-aliases-bulk', ep___admin_emoji_addAliasesBulk], + ['admin/emoji/add', ep___admin_emoji_add], + ['admin/emoji/copy', ep___admin_emoji_copy], + ['admin/emoji/delete-bulk', ep___admin_emoji_deleteBulk], + ['admin/emoji/delete', ep___admin_emoji_delete], + ['admin/emoji/import-zip', ep___admin_emoji_importZip], + ['admin/emoji/list-remote', ep___admin_emoji_listRemote], + ['admin/emoji/list', ep___admin_emoji_list], + ['admin/emoji/remove-aliases-bulk', ep___admin_emoji_removeAliasesBulk], + ['admin/emoji/set-aliases-bulk', ep___admin_emoji_setAliasesBulk], + ['admin/emoji/set-category-bulk', ep___admin_emoji_setCategoryBulk], + ['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk], + ['admin/emoji/update', ep___admin_emoji_update], + ['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles], + ['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata], + ['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing], + ['admin/federation/update-instance', ep___admin_federation_updateInstance], + ['admin/get-index-stats', ep___admin_getIndexStats], + ['admin/get-table-stats', ep___admin_getTableStats], + ['admin/get-user-ips', ep___admin_getUserIps], + ['invite', ep___invite], + ['admin/promo/create', ep___admin_promo_create], + ['admin/queue/clear', ep___admin_queue_clear], + ['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed], + ['admin/queue/inbox-delayed', ep___admin_queue_inboxDelayed], + ['admin/queue/promote', ep___admin_queue_promote], + ['admin/queue/stats', ep___admin_queue_stats], + ['admin/relays/add', ep___admin_relays_add], + ['admin/relays/list', ep___admin_relays_list], + ['admin/relays/remove', ep___admin_relays_remove], + ['admin/reset-password', ep___admin_resetPassword], + ['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport], + ['admin/send-email', ep___admin_sendEmail], + ['admin/server-info', ep___admin_serverInfo], + ['admin/show-moderation-logs', ep___admin_showModerationLogs], + ['admin/show-user', ep___admin_showUser], + ['admin/show-users', ep___admin_showUsers], + ['admin/suspend-user', ep___admin_suspendUser], + ['admin/unsuspend-user', ep___admin_unsuspendUser], + ['admin/update-meta', ep___admin_updateMeta], + ['admin/delete-account', ep___admin_deleteAccount], + ['admin/update-user-note', ep___admin_updateUserNote], + ['admin/roles/create', ep___admin_roles_create], + ['admin/roles/delete', ep___admin_roles_delete], + ['admin/roles/list', ep___admin_roles_list], + ['admin/roles/show', ep___admin_roles_show], + ['admin/roles/update', ep___admin_roles_update], + ['admin/roles/assign', ep___admin_roles_assign], + ['admin/roles/unassign', ep___admin_roles_unassign], + ['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies], + ['admin/roles/users', ep___admin_roles_users], + ['announcements', ep___announcements], + ['antennas/create', ep___antennas_create], + ['antennas/delete', ep___antennas_delete], + ['antennas/list', ep___antennas_list], + ['antennas/notes', ep___antennas_notes], + ['antennas/show', ep___antennas_show], + ['antennas/update', ep___antennas_update], + ['ap/get', ep___ap_get], + ['ap/show', ep___ap_show], + ['app/create', ep___app_create], + ['app/show', ep___app_show], + ['auth/accept', ep___auth_accept], + ['auth/session/generate', ep___auth_session_generate], + ['auth/session/show', ep___auth_session_show], + ['auth/session/userkey', ep___auth_session_userkey], + ['blocking/create', ep___blocking_create], + ['blocking/delete', ep___blocking_delete], + ['blocking/list', ep___blocking_list], + ['channels/create', ep___channels_create], + ['channels/featured', ep___channels_featured], + ['channels/follow', ep___channels_follow], + ['channels/followed', ep___channels_followed], + ['channels/owned', ep___channels_owned], + ['channels/show', ep___channels_show], + ['channels/timeline', ep___channels_timeline], + ['channels/unfollow', ep___channels_unfollow], + ['channels/update', ep___channels_update], + ['channels/favorite', ep___channels_favorite], + ['channels/unfavorite', ep___channels_unfavorite], + ['channels/my-favorites', ep___channels_myFavorites], + ['channels/search', ep___channels_search], + ['charts/active-users', ep___charts_activeUsers], + ['charts/ap-request', ep___charts_apRequest], + ['charts/drive', ep___charts_drive], + ['charts/federation', ep___charts_federation], + ['charts/instance', ep___charts_instance], + ['charts/notes', ep___charts_notes], + ['charts/user/drive', ep___charts_user_drive], + ['charts/user/following', ep___charts_user_following], + ['charts/user/notes', ep___charts_user_notes], + ['charts/user/pv', ep___charts_user_pv], + ['charts/user/reactions', ep___charts_user_reactions], + ['charts/users', ep___charts_users], + ['clips/add-note', ep___clips_addNote], + ['clips/remove-note', ep___clips_removeNote], + ['clips/create', ep___clips_create], + ['clips/delete', ep___clips_delete], + ['clips/list', ep___clips_list], + ['clips/notes', ep___clips_notes], + ['clips/show', ep___clips_show], + ['clips/update', ep___clips_update], + ['clips/favorite', ep___clips_favorite], + ['clips/unfavorite', ep___clips_unfavorite], + ['clips/my-favorites', ep___clips_myFavorites], + ['drive', ep___drive], + ['drive/files', ep___drive_files], + ['drive/files/attached-notes', ep___drive_files_attachedNotes], + ['drive/files/check-existence', ep___drive_files_checkExistence], + ['drive/files/create', ep___drive_files_create], + ['drive/files/delete', ep___drive_files_delete], + ['drive/files/find-by-hash', ep___drive_files_findByHash], + ['drive/files/find', ep___drive_files_find], + ['drive/files/show', ep___drive_files_show], + ['drive/files/update', ep___drive_files_update], + ['drive/files/upload-from-url', ep___drive_files_uploadFromUrl], + ['drive/folders', ep___drive_folders], + ['drive/folders/create', ep___drive_folders_create], + ['drive/folders/delete', ep___drive_folders_delete], + ['drive/folders/find', ep___drive_folders_find], + ['drive/folders/show', ep___drive_folders_show], + ['drive/folders/update', ep___drive_folders_update], + ['drive/stream', ep___drive_stream], + ['email-address/available', ep___emailAddress_available], + ['endpoint', ep___endpoint], + ['endpoints', ep___endpoints], + ['export-custom-emojis', ep___exportCustomEmojis], + ['federation/followers', ep___federation_followers], + ['federation/following', ep___federation_following], + ['federation/instances', ep___federation_instances], + ['federation/show-instance', ep___federation_showInstance], + ['federation/update-remote-user', ep___federation_updateRemoteUser], + ['federation/users', ep___federation_users], + ['federation/stats', ep___federation_stats], + ['following/create', ep___following_create], + ['following/delete', ep___following_delete], + ['following/invalidate', ep___following_invalidate], + ['following/requests/accept', ep___following_requests_accept], + ['following/requests/cancel', ep___following_requests_cancel], + ['following/requests/list', ep___following_requests_list], + ['following/requests/reject', ep___following_requests_reject], + ['gallery/featured', ep___gallery_featured], + ['gallery/popular', ep___gallery_popular], + ['gallery/posts', ep___gallery_posts], + ['gallery/posts/create', ep___gallery_posts_create], + ['gallery/posts/delete', ep___gallery_posts_delete], + ['gallery/posts/like', ep___gallery_posts_like], + ['gallery/posts/show', ep___gallery_posts_show], + ['gallery/posts/unlike', ep___gallery_posts_unlike], + ['gallery/posts/update', ep___gallery_posts_update], + ['get-online-users-count', ep___getOnlineUsersCount], + ['hashtags/list', ep___hashtags_list], + ['hashtags/search', ep___hashtags_search], + ['hashtags/show', ep___hashtags_show], + ['hashtags/trend', ep___hashtags_trend], + ['hashtags/users', ep___hashtags_users], + ['i', ep___i], + ['i/2fa/done', ep___i_2fa_done], + ['i/2fa/key-done', ep___i_2fa_keyDone], + ['i/2fa/password-less', ep___i_2fa_passwordLess], + ['i/2fa/register-key', ep___i_2fa_registerKey], + ['i/2fa/register', ep___i_2fa_register], + ['i/2fa/update-key', ep___i_2fa_updateKey], + ['i/2fa/remove-key', ep___i_2fa_removeKey], + ['i/2fa/unregister', ep___i_2fa_unregister], + ['i/apps', ep___i_apps], + ['i/authorized-apps', ep___i_authorizedApps], + ['i/claim-achievement', ep___i_claimAchievement], + ['i/change-password', ep___i_changePassword], + ['i/delete-account', ep___i_deleteAccount], + ['i/export-blocking', ep___i_exportBlocking], + ['i/export-following', ep___i_exportFollowing], + ['i/export-mute', ep___i_exportMute], + ['i/export-notes', ep___i_exportNotes], + ['i/export-favorites', ep___i_exportFavorites], + ['i/export-user-lists', ep___i_exportUserLists], + ['i/export-antennas', ep___i_exportAntennas], + ['i/favorites', ep___i_favorites], + ['i/gallery/likes', ep___i_gallery_likes], + ['i/gallery/posts', ep___i_gallery_posts], + ['i/get-word-muted-notes-count', ep___i_getWordMutedNotesCount], + ['i/import-blocking', ep___i_importBlocking], + ['i/import-following', ep___i_importFollowing], + ['i/import-muting', ep___i_importMuting], + ['i/import-user-lists', ep___i_importUserLists], + ['i/import-antennas', ep___i_importAntennas], + ['i/notifications', ep___i_notifications], + ['i/page-likes', ep___i_pageLikes], + ['i/pages', ep___i_pages], + ['i/pin', ep___i_pin], + ['i/read-all-unread-notes', ep___i_readAllUnreadNotes], + ['i/read-announcement', ep___i_readAnnouncement], + ['i/regenerate-token', ep___i_regenerateToken], + ['i/registry/get-all', ep___i_registry_getAll], + ['i/registry/get-detail', ep___i_registry_getDetail], + ['i/registry/get', ep___i_registry_get], + ['i/registry/keys-with-type', ep___i_registry_keysWithType], + ['i/registry/keys', ep___i_registry_keys], + ['i/registry/remove', ep___i_registry_remove], + ['i/registry/scopes', ep___i_registry_scopes], + ['i/registry/set', ep___i_registry_set], + ['i/revoke-token', ep___i_revokeToken], + ['i/signin-history', ep___i_signinHistory], + ['i/unpin', ep___i_unpin], + ['i/update-email', ep___i_updateEmail], + ['i/update', ep___i_update], + ['i/move', ep___i_move], + ['i/webhooks/create', ep___i_webhooks_create], + ['i/webhooks/list', ep___i_webhooks_list], + ['i/webhooks/show', ep___i_webhooks_show], + ['i/webhooks/update', ep___i_webhooks_update], + ['i/webhooks/delete', ep___i_webhooks_delete], + ['meta', ep___meta], + ['emojis', ep___emojis], + ['emoji', ep___emoji], + ['miauth/gen-token', ep___miauth_genToken], + ['mute/create', ep___mute_create], + ['mute/delete', ep___mute_delete], + ['mute/list', ep___mute_list], + ['renote-mute/create', ep___renoteMute_create], + ['renote-mute/delete', ep___renoteMute_delete], + ['renote-mute/list', ep___renoteMute_list], + ['my/apps', ep___my_apps], + ['notes', ep___notes], + ['notes/children', ep___notes_children], + ['notes/clips', ep___notes_clips], + ['notes/conversation', ep___notes_conversation], + ['notes/create', ep___notes_create], + ['notes/delete', ep___notes_delete], + ['notes/favorites/create', ep___notes_favorites_create], + ['notes/favorites/delete', ep___notes_favorites_delete], + ['notes/featured', ep___notes_featured], + ['notes/global-timeline', ep___notes_globalTimeline], + ['notes/hybrid-timeline', ep___notes_hybridTimeline], + ['notes/local-timeline', ep___notes_localTimeline], + ['notes/mentions', ep___notes_mentions], + ['notes/polls/recommendation', ep___notes_polls_recommendation], + ['notes/polls/vote', ep___notes_polls_vote], + ['notes/reactions', ep___notes_reactions], + ['notes/reactions/create', ep___notes_reactions_create], + ['notes/reactions/delete', ep___notes_reactions_delete], + ['notes/renotes', ep___notes_renotes], + ['notes/replies', ep___notes_replies], + ['notes/search-by-tag', ep___notes_searchByTag], + ['notes/search', ep___notes_search], + ['notes/show', ep___notes_show], + ['notes/state', ep___notes_state], + ['notes/thread-muting/create', ep___notes_threadMuting_create], + ['notes/thread-muting/delete', ep___notes_threadMuting_delete], + ['notes/timeline', ep___notes_timeline], + ['notes/translate', ep___notes_translate], + ['notes/unrenote', ep___notes_unrenote], + ['notes/user-list-timeline', ep___notes_userListTimeline], + ['notifications/create', ep___notifications_create], + ['notifications/mark-all-as-read', ep___notifications_markAllAsRead], + ['page-push', ep___pagePush], + ['pages/create', ep___pages_create], + ['pages/delete', ep___pages_delete], + ['pages/featured', ep___pages_featured], + ['pages/like', ep___pages_like], + ['pages/show', ep___pages_show], + ['pages/unlike', ep___pages_unlike], + ['pages/update', ep___pages_update], + ['flash/create', ep___flash_create], + ['flash/delete', ep___flash_delete], + ['flash/featured', ep___flash_featured], + ['flash/like', ep___flash_like], + ['flash/show', ep___flash_show], + ['flash/unlike', ep___flash_unlike], + ['flash/update', ep___flash_update], + ['flash/my', ep___flash_my], + ['flash/my-likes', ep___flash_myLikes], + ['ping', ep___ping], + ['pinned-users', ep___pinnedUsers], + ['promo/read', ep___promo_read], + ['roles/list', ep___roles_list], + ['roles/show', ep___roles_show], + ['roles/users', ep___roles_users], + ['roles/notes', ep___roles_notes], + ['request-reset-password', ep___requestResetPassword], + ['reset-db', ep___resetDb], + ['reset-password', ep___resetPassword], + ['server-info', ep___serverInfo], + ['stats', ep___stats], + ['sw/show-registration', ep___sw_show_registration], + ['sw/update-registration', ep___sw_update_registration], + ['sw/register', ep___sw_register], + ['sw/unregister', ep___sw_unregister], + ['test', ep___test], + ['username/available', ep___username_available], + ['users', ep___users], + ['users/clips', ep___users_clips], + ['users/followers', ep___users_followers], + ['users/following', ep___users_following], + ['users/gallery/posts', ep___users_gallery_posts], + ['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers], + ['users/lists/create', ep___users_lists_create], + ['users/lists/delete', ep___users_lists_delete], + ['users/lists/list', ep___users_lists_list], + ['users/lists/pull', ep___users_lists_pull], + ['users/lists/push', ep___users_lists_push], + ['users/lists/show', ep___users_lists_show], + ['users/lists/favorite', ep___users_lists_favorite], + ['users/lists/unfavorite', ep___users_lists_unfavorite], + ['users/lists/update', ep___users_lists_update], + ['users/lists/create-from-public', ep___users_lists_create_from_public], + ['users/notes', ep___users_notes], + ['users/pages', ep___users_pages], + ['users/reactions', ep___users_reactions], + ['users/recommendation', ep___users_recommendation], + ['users/relation', ep___users_relation], + ['users/report-abuse', ep___users_reportAbuse], + ['users/search-by-username-and-host', ep___users_searchByUsernameAndHost], + ['users/search', ep___users_search], + ['users/show', ep___users_show], + ['users/achievements', ep___users_achievements], + ['users/update-memo', ep___users_updateMemo], + ['fetch-rss', ep___fetchRss], + ['retention', ep___retention], +]; -interface IEndpointMetaBase { +export interface IEndpointMeta { readonly stability?: 'deprecated' | 'experimental' | 'stable'; readonly tags?: ReadonlyArray; @@ -39,7 +708,7 @@ interface IEndpointMetaBase { */ readonly requireAdmin?: boolean; - readonly requiredRolePolicy?: KeyOf<'RolePolicies'>; + readonly requireRolePolicy?: keyof RolePolicies; /** * 引っ越し済みのユーザーによるリクエストを禁止するか @@ -107,40 +776,18 @@ interface IEndpointMetaBase { readonly cacheSec?: number; } -export type IEndpointMeta = (Omit & { - requireCredential?: false, - requireAdmin?: false, - requireModerator?: false, -}) | (Omit & { - secure: true, -}) | (Omit & { - requireCredential: true, - kind: (typeof permissions)[number], -}) | (Omit & { - requireModerator: true, - kind: (typeof permissions)[number], -}) | (Omit & { - requireAdmin: true, - kind: (typeof permissions)[number], -}); - export interface IEndpoint { name: string; meta: IEndpointMeta; params: Schema; } -const endpoints: IEndpoint[] = Object.entries(endpointsObject).map(([name, ep]) => { +const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => { return { name: name, - get meta() { - return ep.meta ?? {}; - }, - get params() { - return ep.paramDef; - }, + get meta() { return ep.meta ?? {}; }, + get params() { return ep.paramDef; }, }; }); -// eslint-disable-next-line import/no-default-export export default endpoints; diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts deleted file mode 100644 index bdfbcba518..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ApiError } from '@/server/api/error.js'; -import { - AbuseReportNotificationRecipientEntityService, -} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; -import { DI } from '@/di-symbols.js'; -import type { UserProfilesRepository } from '@/models/_.js'; - -export const meta = { - tags: ['admin', 'abuse-report', 'notification-recipient'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:abuse-report:notification-recipient', - - res: { - type: 'object', - ref: 'AbuseReportNotificationRecipient', - }, - - errors: { - correlationCheckEmail: { - message: 'If "method" is email, "userId" must be set.', - code: 'CORRELATION_CHECK_EMAIL', - id: '348bb8ae-575a-6fe9-4327-5811999def8f', - httpStatusCode: 400, - }, - correlationCheckWebhook: { - message: 'If "method" is webhook, "systemWebhookId" must be set.', - code: 'CORRELATION_CHECK_WEBHOOK', - id: 'b0c15051-de2d-29ef-260c-9585cddd701a', - httpStatusCode: 400, - }, - emailAddressNotSet: { - message: 'Email address is not set.', - code: 'EMAIL_ADDRESS_NOT_SET', - id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f', - httpStatusCode: 400, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - isActive: { - type: 'boolean', - }, - name: { - type: 'string', - minLength: 1, - maxLength: 255, - }, - method: { - type: 'string', - enum: ['email', 'webhook'], - }, - userId: { - type: 'string', - format: 'misskey:id', - }, - systemWebhookId: { - type: 'string', - format: 'misskey:id', - }, - }, - required: [ - 'isActive', - 'name', - 'method', - ], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - private abuseReportNotificationService: AbuseReportNotificationService, - private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - if (ps.method === 'email') { - const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId }); - if (!ps.userId || !userProfile) { - throw new ApiError(meta.errors.correlationCheckEmail); - } - - if (!userProfile.email || !userProfile.emailVerified) { - throw new ApiError(meta.errors.emailAddressNotSet); - } - } - - if (ps.method === 'webhook' && !ps.systemWebhookId) { - throw new ApiError(meta.errors.correlationCheckWebhook); - } - - const userId = ps.method === 'email' ? ps.userId : null; - const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null; - const result = await this.abuseReportNotificationService.createRecipient( - { - isActive: ps.isActive, - name: ps.name, - method: ps.method, - userId: userId ?? null, - systemWebhookId: systemWebhookId ?? null, - }, - me, - ); - - return this.abuseReportNotificationRecipientEntityService.pack(result); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts deleted file mode 100644 index b6dc44e09c..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; - -export const meta = { - tags: ['admin', 'abuse-report', 'notification-recipient'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:abuse-report:notification-recipient', -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - }, - required: [ - 'id', - ], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private abuseReportNotificationService: AbuseReportNotificationService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.abuseReportNotificationService.deleteRecipient( - ps.id, - me, - ); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts deleted file mode 100644 index dad9161a8a..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { - AbuseReportNotificationRecipientEntityService, -} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; - -export const meta = { - tags: ['admin', 'abuse-report', 'notification-recipient'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'read:admin:abuse-report:notification-recipient', - - res: { - type: 'array', - items: { - type: 'object', - ref: 'AbuseReportNotificationRecipient', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - method: { - type: 'array', - items: { - type: 'string', - enum: ['email', 'webhook'], - }, - }, - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private abuseReportNotificationService: AbuseReportNotificationService, - private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService, - ) { - super(meta, paramDef, async (ps) => { - const recipients = await this.abuseReportNotificationService.fetchRecipients({ method: ps.method }); - return this.abuseReportNotificationRecipientEntityService.packMany(recipients); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts deleted file mode 100644 index 557798f946..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { - AbuseReportNotificationRecipientEntityService, -} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['admin', 'abuse-report', 'notification-recipient'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'read:admin:abuse-report:notification-recipient', - - res: { - type: 'object', - ref: 'AbuseReportNotificationRecipient', - }, - - errors: { - noSuchRecipient: { - message: 'No such recipient.', - code: 'NO_SUCH_RECIPIENT', - id: '013de6a8-f757-04cb-4d73-cc2a7e3368e4', - kind: 'server', - httpStatusCode: 404, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - }, - required: ['id'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private abuseReportNotificationService: AbuseReportNotificationService, - private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService, - ) { - super(meta, paramDef, async (ps) => { - const recipients = await this.abuseReportNotificationService.fetchRecipients({ ids: [ps.id] }); - if (recipients.length === 0) { - throw new ApiError(meta.errors.noSuchRecipient); - } - - return this.abuseReportNotificationRecipientEntityService.pack(recipients[0]); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts deleted file mode 100644 index bd4b485217..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ApiError } from '@/server/api/error.js'; -import { - AbuseReportNotificationRecipientEntityService, -} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; -import { DI } from '@/di-symbols.js'; -import type { UserProfilesRepository } from '@/models/_.js'; - -export const meta = { - tags: ['admin', 'abuse-report', 'notification-recipient'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:abuse-report:notification-recipient', - - res: { - type: 'object', - ref: 'AbuseReportNotificationRecipient', - }, - - errors: { - correlationCheckEmail: { - message: 'If "method" is email, "userId" must be set.', - code: 'CORRELATION_CHECK_EMAIL', - id: '348bb8ae-575a-6fe9-4327-5811999def8f', - httpStatusCode: 400, - }, - correlationCheckWebhook: { - message: 'If "method" is webhook, "systemWebhookId" must be set.', - code: 'CORRELATION_CHECK_WEBHOOK', - id: 'b0c15051-de2d-29ef-260c-9585cddd701a', - httpStatusCode: 400, - }, - emailAddressNotSet: { - message: 'Email address is not set.', - code: 'EMAIL_ADDRESS_NOT_SET', - id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f', - httpStatusCode: 400, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - isActive: { - type: 'boolean', - }, - name: { - type: 'string', - minLength: 1, - maxLength: 255, - }, - method: { - type: 'string', - enum: ['email', 'webhook'], - }, - userId: { - type: 'string', - format: 'misskey:id', - }, - systemWebhookId: { - type: 'string', - format: 'misskey:id', - }, - }, - required: [ - 'id', - 'isActive', - 'name', - 'method', - ], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - private abuseReportNotificationService: AbuseReportNotificationService, - private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - if (ps.method === 'email') { - const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId }); - if (!ps.userId || !userProfile) { - throw new ApiError(meta.errors.correlationCheckEmail); - } - - if (!userProfile.email || !userProfile.emailVerified) { - throw new ApiError(meta.errors.emailAddressNotSet); - } - } - - if (ps.method === 'webhook' && !ps.systemWebhookId) { - throw new ApiError(meta.errors.correlationCheckWebhook); - } - - const userId = ps.method === 'email' ? ps.userId : null; - const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null; - const result = await this.abuseReportNotificationService.updateRecipient( - { - id: ps.id, - isActive: ps.isActive, - name: ps.name, - method: ps.method, - userId: userId ?? null, - systemWebhookId: systemWebhookId ?? null, - }, - me, - ); - - return this.abuseReportNotificationRecipientEntityService.pack(result); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index 0dbfaae054..9bba16166f 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AbuseUserReportsRepository } from '@/models/_.js'; +import type { AbuseUserReportsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { AbuseUserReportEntityService } from '@/core/entities/AbuseUserReportEntityService.js'; @@ -15,7 +10,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'read:admin:abuse-user-reports', res: { type: 'array', @@ -62,30 +56,17 @@ export const meta = { reporter: { type: 'object', nullable: false, optional: false, - ref: 'UserDetailedNotMe', + ref: 'User', }, targetUser: { type: 'object', nullable: false, optional: false, - ref: 'UserDetailedNotMe', + ref: 'User', }, assignee: { type: 'object', - nullable: true, optional: false, - ref: 'UserDetailedNotMe', - }, - forwarded: { - type: 'boolean', - nullable: false, optional: false, - }, - resolvedAs: { - type: 'string', - nullable: true, optional: false, - enum: ['accept', 'reject', null], - }, - moderationNote: { - type: 'string', - nullable: false, optional: false, + nullable: true, optional: true, + ref: 'User', }, }, }, @@ -101,12 +82,14 @@ export const paramDef = { state: { type: 'string', nullable: true, default: null }, reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, + forwarded: { type: 'boolean', default: false }, }, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.abuseUserReportsRepository) private abuseUserReportsRepository: AbuseUserReportsRepository, @@ -132,7 +115,7 @@ export default class extends Endpoint { // eslint- case 'remote': query.andWhere('report.targetUserHost IS NOT NULL'); break; } - const reports = await query.limit(ps.limit).getMany(); + const reports = await query.take(ps.limit).getMany(); return await this.abuseUserReportEntityService.packMany(reports); }); diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index 06047b58a6..8a3541dffe 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -1,40 +1,19 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; +import { IsNull } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MiMeta, UsersRepository } from '@/models/_.js'; +import type { UsersRepository } from '@/models/index.js'; import { SignupService } from '@/core/SignupService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { localUsernameSchema, passwordSchema } from '@/models/User.js'; +import { localUsernameSchema, passwordSchema } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; -import { ApiError } from '@/server/api/error.js'; -import { Packed } from '@/misc/json-schema.js'; export const meta = { tags: ['admin'], - errors: { - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '1fb7cb09-d46a-4fff-b8df-057708cce513', - }, - - wrongInitialPassword: { - message: 'Initial password is incorrect.', - code: 'INCORRECT_INITIAL_PASSWORD', - id: '97147c55-1ae1-4f6f-91d6-e1c3e0e76d62', - }, - }, - res: { type: 'object', optional: false, nullable: false, - ref: 'MeDetailed', + ref: 'User', properties: { token: { type: 'string', @@ -49,45 +28,26 @@ export const paramDef = { properties: { username: localUsernameSchema, password: passwordSchema, - setupPassword: { type: 'string', nullable: true }, }, required: ['username', 'password'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.meta) - private serverSettings: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, private userEntityService: UserEntityService, private signupService: SignupService, ) { - super(meta, paramDef, async (ps, _me, token) => { + super(meta, paramDef, async (ps, _me) => { const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null; - - if (this.serverSettings.rootUserId == null && me == null && token == null) { - // 初回セットアップの場合 - if (this.config.setupPassword != null) { - // 初期パスワードが設定されている場合 - if (ps.setupPassword !== this.config.setupPassword) { - // 初期パスワードが違う場合 - throw new ApiError(meta.errors.wrongInitialPassword); - } - } else if (ps.setupPassword != null && ps.setupPassword.trim() !== '') { - // 初期パスワードが設定されていないのに初期パスワードが入力された場合 - throw new ApiError(meta.errors.wrongInitialPassword); - } - } else if ((this.serverSettings.rootUserId != null && (this.serverSettings.rootUserId !== me?.id)) || token !== null) { - // 初回セットアップではなく、管理者でない場合 or 外部トークンを使用している場合 - throw new ApiError(meta.errors.accessDenied); - } + const noUsers = (await this.usersRepository.countBy({ + host: IsNull(), + })) === 0; + if (!noUsers && !me?.isRoot) throw new Error('access denied'); const { account, secret } = await this.signupService.signup({ username: ps.username, @@ -96,11 +56,11 @@ export default class extends Endpoint { // eslint- }); const res = await this.userEntityService.pack(account, account, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, - }) as Packed<'MeDetailed'> & { token: string }; + }); - res.token = secret; + (res as any).token = secret; return res; }); diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index d04f52dd64..16232813a8 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -1,22 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/_.js'; +import type { UsersRepository } from '@/models/index.js'; import { QueueService } from '@/core/QueueService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { UserSuspendService } from '@/core/UserSuspendService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { DeleteAccountService } from '@/core/DeleteAccountService.js'; export const meta = { tags: ['admin'], requireCredential: true, requireAdmin: true, - kind: 'write:admin:account', } as const; export const paramDef = { @@ -27,13 +22,17 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, - private deleteAccoountService: DeleteAccountService, + private userEntityService: UserEntityService, + private queueService: QueueService, + private globalEventService: GlobalEventService, + private userSuspendService: UserSuspendService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy({ id: ps.userId }); @@ -42,7 +41,26 @@ export default class extends Endpoint { // eslint- throw new Error('user not found'); } - await this.deleteAccoountService.deleteAccount(user, me); + if (user.isRoot) { + throw new Error('cannot delete a root account'); + } + + if (this.userEntityService.isLocalUser(user)) { + // 物理削除する前にDelete activityを送信する + await this.userSuspendService.doPostSuspend(user).catch(err => {}); + + this.queueService.createDeleteAccountJob(user, { + soft: false, + }); + } else { + this.queueService.createDeleteAccountJob(user, { + soft: true, // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する + }); + } + + await this.usersRepository.update(user.id, { + isDeleted: true, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts deleted file mode 100644 index 12cd5cf295..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserProfilesRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireAdmin: true, - kind: 'read:admin:account', - - errors: { - userNotFound: { - message: 'No such user who has the email address.', - code: 'USER_NOT_FOUND', - id: 'cb865949-8af5-4062-a88c-ef55e8786d1d', - }, - }, - res: { - type: 'object', - optional: false, nullable: false, - ref: 'UserDetailedNotMe', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - email: { type: 'string' }, - }, - required: ['email'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - - private userEntityService: UserEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const profile = await this.userProfilesRepository.findOne({ - where: { email: ps.email }, - relations: ['user'], - }); - - if (profile == null) { - throw new ApiError(meta.errors.userNotFound); - } - - const res = await this.userEntityService.pack(profile.user!, null, { - schema: 'UserDetailedNotMe', - }); - - return res; - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts index 955154f4fb..917242db3f 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts @@ -1,27 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AdsRepository } from '@/models/_.js'; +import type { AdsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], requireCredential: true, requireModerator: true, - kind: 'write:admin:ad', - res: { - type: 'object', - optional: false, - nullable: false, - ref: 'Ad', - }, } as const; export const paramDef = { @@ -35,26 +22,25 @@ export const paramDef = { expiresAt: { type: 'integer' }, startsAt: { type: 'integer' }, imageUrl: { type: 'string', minLength: 1 }, - dayOfWeek: { type: 'integer' }, }, - required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'], + required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.adsRepository) private adsRepository: AdsRepository, private idService: IdService, - private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { - const ad = await this.adsRepository.insertOne({ - id: this.idService.gen(), + await this.adsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), expiresAt: new Date(ps.expiresAt), startsAt: new Date(ps.startsAt), - dayOfWeek: ps.dayOfWeek, url: ps.url, imageUrl: ps.imageUrl, priority: ps.priority, @@ -62,24 +48,6 @@ export default class extends Endpoint { // eslint- place: ps.place, memo: ps.memo, }); - - this.moderationLogService.log(me, 'createAd', { - adId: ad.id, - ad: ad, - }); - - return { - id: ad.id, - expiresAt: ad.expiresAt.toISOString(), - startsAt: ad.startsAt.toISOString(), - dayOfWeek: ad.dayOfWeek, - url: ad.url, - imageUrl: ad.imageUrl, - priority: ad.priority, - ratio: ad.ratio, - place: ad.place, - memo: ad.memo, - }; }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts index 501e13c6a7..f4c9885408 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts @@ -1,13 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AdsRepository } from '@/models/_.js'; +import type { AdsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -15,7 +9,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:ad', errors: { noSuchAd: { @@ -34,13 +27,12 @@ export const paramDef = { required: ['id'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.adsRepository) private adsRepository: AdsRepository, - - private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { const ad = await this.adsRepository.findOneBy({ id: ps.id }); @@ -48,11 +40,6 @@ export default class extends Endpoint { // eslint- if (ad == null) throw new ApiError(meta.errors.noSuchAd); await this.adsRepository.delete(ad.id); - - this.moderationLogService.log(me, 'deleteAd', { - adId: ad.id, - ad: ad, - }); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts index 6406709cda..0b6d006052 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AdsRepository } from '@/models/_.js'; +import type { AdsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; @@ -14,18 +9,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'read:admin:ad', - res: { - type: 'array', - optional: false, - nullable: false, - items: { - type: 'object', - optional: false, - nullable: false, - ref: 'Ad', - }, - }, } as const; export const paramDef = { @@ -34,13 +17,13 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, - publishing: { type: 'boolean', default: null, nullable: true }, }, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.adsRepository) private adsRepository: AdsRepository, @@ -49,25 +32,9 @@ export default class extends Endpoint { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId); - if (ps.publishing === true) { - query.andWhere('ad.expiresAt > :now', { now: new Date() }).andWhere('ad.startsAt <= :now', { now: new Date() }); - } else if (ps.publishing === false) { - query.andWhere('ad.expiresAt <= :now', { now: new Date() }).orWhere('ad.startsAt > :now', { now: new Date() }); - } - const ads = await query.limit(ps.limit).getMany(); + const ads = await query.take(ps.limit).getMany(); - return ads.map(ad => ({ - id: ad.id, - expiresAt: ad.expiresAt.toISOString(), - startsAt: ad.startsAt.toISOString(), - dayOfWeek: ad.dayOfWeek, - url: ad.url, - imageUrl: ad.imageUrl, - memo: ad.memo, - place: ad.place, - priority: ad.priority, - ratio: ad.ratio, - })); + return ads; }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts index 4e3d731aca..dbab7e9d4f 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts @@ -1,13 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AdsRepository } from '@/models/_.js'; +import type { AdsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -15,7 +9,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:ad', errors: { noSuchAd: { @@ -38,18 +31,16 @@ export const paramDef = { ratio: { type: 'integer' }, expiresAt: { type: 'integer' }, startsAt: { type: 'integer' }, - dayOfWeek: { type: 'integer' }, }, - required: ['id'], + required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.adsRepository) private adsRepository: AdsRepository, - - private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { const ad = await this.adsRepository.findOneBy({ id: ps.id }); @@ -63,17 +54,8 @@ export default class extends Endpoint { // eslint- ratio: ps.ratio, memo: ps.memo, imageUrl: ps.imageUrl, - expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined, - startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined, - dayOfWeek: ps.dayOfWeek, - }); - - const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id }); - - this.moderationLogService.log(me, 'updateAd', { - adId: ad.id, - before: ad, - after: updatedAd, + expiresAt: new Date(ps.expiresAt), + startsAt: new Date(ps.startsAt), }); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts index b8bfda73a4..751b6be7f4 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -1,18 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AnnouncementService } from '@/core/AnnouncementService.js'; +import type { AnnouncementsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['admin'], requireCredential: true, requireModerator: true, - kind: 'write:admin:announcements', res: { type: 'object', @@ -55,38 +51,31 @@ export const paramDef = { properties: { title: { type: 'string', minLength: 1 }, text: { type: 'string', minLength: 1 }, - imageUrl: { type: 'string', nullable: true, minLength: 0 }, - icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'], default: 'info' }, - display: { type: 'string', enum: ['normal', 'banner', 'dialog'], default: 'normal' }, - forExistingUsers: { type: 'boolean', default: false }, - silence: { type: 'boolean', default: false }, - needConfirmationToRead: { type: 'boolean', default: false }, - userId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, + imageUrl: { type: 'string', nullable: true, minLength: 1 }, }, required: ['title', 'text', 'imageUrl'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private announcementService: AnnouncementService, + @Inject(DI.announcementsRepository) + private announcementsRepository: AnnouncementsRepository, + + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - const { raw, packed } = await this.announcementService.create({ + const announcement = await this.announcementsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), updatedAt: null, title: ps.title, text: ps.text, - /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */ - imageUrl: ps.imageUrl || null, - icon: ps.icon, - display: ps.display, - forExistingUsers: ps.forExistingUsers, - silence: ps.silence, - needConfirmationToRead: ps.needConfirmationToRead, - userId: ps.userId, - }, me); + imageUrl: ps.imageUrl, + }).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0])); - return packed; + return Object.assign({}, announcement, { createdAt: announcement.createdAt.toISOString(), updatedAt: null }); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts index 6d1e1b0a10..18d50b8b2a 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts @@ -1,13 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AnnouncementsRepository } from '@/models/_.js'; +import type { AnnouncementsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -import { AnnouncementService } from '@/core/AnnouncementService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -15,7 +9,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:announcements', errors: { noSuchAnnouncement: { @@ -34,20 +27,19 @@ export const paramDef = { required: ['id'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.announcementsRepository) private announcementsRepository: AnnouncementsRepository, - - private announcementService: AnnouncementService, ) { super(meta, paramDef, async (ps, me) => { const announcement = await this.announcementsRepository.findOneBy({ id: ps.id }); if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); - await this.announcementService.delete(announcement, me); + await this.announcementsRepository.delete(announcement.id); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 7596bf44e3..9b20494129 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -1,22 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/_.js'; -import type { MiAnnouncement } from '@/models/Announcement.js'; +import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/index.js'; +import type { Announcement } from '@/models/entities/Announcement.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; -import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['admin'], requireCredential: true, requireModerator: true, - kind: 'read:admin:announcements', res: { type: 'array', @@ -68,14 +61,13 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, - userId: { type: 'string', format: 'misskey:id', nullable: true }, - status: { type: 'string', enum: ['all', 'active', 'archived'], default: 'active' }, }, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.announcementsRepository) private announcementsRepository: AnnouncementsRepository, @@ -84,26 +76,13 @@ export default class extends Endpoint { // eslint- private announcementReadsRepository: AnnouncementReadsRepository, private queryService: QueryService, - private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); - if (ps.status === 'archived') { - query.andWhere('announcement.isActive = false'); - } else if (ps.status === 'active') { - query.andWhere('announcement.isActive = true'); - } + const announcements = await query.take(ps.limit).getMany(); - if (ps.userId) { - query.andWhere('announcement.userId = :userId', { userId: ps.userId }); - } else { - query.andWhere('announcement.userId IS NULL'); - } - - const announcements = await query.limit(ps.limit).getMany(); - - const reads = new Map(); + const reads = new Map(); for (const announcement of announcements) { reads.set(announcement, await this.announcementReadsRepository.countBy({ @@ -113,18 +92,11 @@ export default class extends Endpoint { // eslint- return announcements.map(announcement => ({ id: announcement.id, - createdAt: this.idService.parse(announcement.id).date.toISOString(), + createdAt: announcement.createdAt.toISOString(), updatedAt: announcement.updatedAt?.toISOString() ?? null, title: announcement.title, text: announcement.text, imageUrl: announcement.imageUrl, - icon: announcement.icon, - display: announcement.display, - isActive: announcement.isActive, - forExistingUsers: announcement.forExistingUsers, - silence: announcement.silence, - needConfirmationToRead: announcement.needConfirmationToRead, - userId: announcement.userId, reads: reads.get(announcement)!, })); }); diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts index 6fce6e4e0a..12db1f78fb 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -1,13 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AnnouncementsRepository } from '@/models/_.js'; +import type { AnnouncementsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -import { AnnouncementService } from '@/core/AnnouncementService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -15,7 +9,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:announcements', errors: { noSuchAnnouncement: { @@ -33,42 +26,29 @@ export const paramDef = { title: { type: 'string', minLength: 1 }, text: { type: 'string', minLength: 1 }, imageUrl: { type: 'string', nullable: true, minLength: 0 }, - icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'] }, - display: { type: 'string', enum: ['normal', 'banner', 'dialog'] }, - forExistingUsers: { type: 'boolean' }, - silence: { type: 'boolean' }, - needConfirmationToRead: { type: 'boolean' }, - isActive: { type: 'boolean' }, }, - required: ['id'], + required: ['id', 'title', 'text', 'imageUrl'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.announcementsRepository) private announcementsRepository: AnnouncementsRepository, - - private announcementService: AnnouncementService, ) { super(meta, paramDef, async (ps, me) => { const announcement = await this.announcementsRepository.findOneBy({ id: ps.id }); if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); - await this.announcementService.update(announcement, { + await this.announcementsRepository.update(announcement.id, { updatedAt: new Date(), title: ps.title, text: ps.text, /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */ - imageUrl: ps.imageUrl || null, - display: ps.display, - icon: ps.icon, - forExistingUsers: ps.forExistingUsers, - silence: ps.silence, - needConfirmationToRead: ps.needConfirmationToRead, - isActive: ps.isActive, - }, me); + imageUrl: ps.imageUrl || null, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts deleted file mode 100644 index 0121c302ac..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; -import { IdService } from '@/core/IdService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requiredRolePolicy: 'canManageAvatarDecorations', - kind: 'write:admin:avatar-decorations', - - res: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - createdAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - updatedAt: { - type: 'string', - optional: false, nullable: true, - format: 'date-time', - }, - name: { - type: 'string', - optional: false, nullable: false, - }, - description: { - type: 'string', - optional: false, nullable: false, - }, - url: { - type: 'string', - optional: false, nullable: false, - }, - roleIdsThatCanBeUsedThisDecoration: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - name: { type: 'string', minLength: 1 }, - description: { type: 'string' }, - url: { type: 'string', minLength: 1 }, - roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: { - type: 'string', - } }, - }, - required: ['name', 'description', 'url'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private avatarDecorationService: AvatarDecorationService, - private idService: IdService, - ) { - super(meta, paramDef, async (ps, me) => { - const created = await this.avatarDecorationService.create({ - name: ps.name, - description: ps.description, - url: ps.url, - roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration, - }, me); - - return { - id: created.id, - createdAt: this.idService.parse(created.id).date.toISOString(), - updatedAt: null, - name: created.name, - description: created.description, - url: created.url, - roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration, - }; - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts deleted file mode 100644 index 13660d0b8c..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requiredRolePolicy: 'canManageAvatarDecorations', - kind: 'write:admin:avatar-decorations', - errors: { - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { type: 'string', format: 'misskey:id' }, - }, - required: ['id'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private avatarDecorationService: AvatarDecorationService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.avatarDecorationService.delete(ps.id, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts deleted file mode 100644 index d4d9a7235b..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { IdService } from '@/core/IdService.js'; -import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requiredRolePolicy: 'canManageAvatarDecorations', - kind: 'read:admin:avatar-decorations', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - createdAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - updatedAt: { - type: 'string', - optional: false, nullable: true, - format: 'date-time', - }, - name: { - type: 'string', - optional: false, nullable: false, - }, - description: { - type: 'string', - optional: false, nullable: false, - }, - url: { - type: 'string', - optional: false, nullable: false, - }, - roleIdsThatCanBeUsedThisDecoration: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - userId: { type: 'string', format: 'misskey:id', nullable: true }, - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private avatarDecorationService: AvatarDecorationService, - private idService: IdService, - ) { - super(meta, paramDef, async (ps, me) => { - const avatarDecorations = await this.avatarDecorationService.getAll(true); - - return avatarDecorations.map(avatarDecoration => ({ - id: avatarDecoration.id, - createdAt: this.idService.parse(avatarDecoration.id).date.toISOString(), - updatedAt: avatarDecoration.updatedAt?.toISOString() ?? null, - name: avatarDecoration.name, - description: avatarDecoration.description, - url: avatarDecoration.url, - roleIdsThatCanBeUsedThisDecoration: avatarDecoration.roleIdsThatCanBeUsedThisDecoration, - })); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts deleted file mode 100644 index 22476a6888..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requiredRolePolicy: 'canManageAvatarDecorations', - kind: 'write:admin:avatar-decorations', - - errors: { - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { type: 'string', format: 'misskey:id' }, - name: { type: 'string', minLength: 1 }, - description: { type: 'string' }, - url: { type: 'string', minLength: 1 }, - roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: { - type: 'string', - } }, - }, - required: ['id'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private avatarDecorationService: AvatarDecorationService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.avatarDecorationService.update(ps.id, { - name: ps.name, - description: ps.description, - url: ps.url, - roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration, - }, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/captcha/current.ts b/packages/backend/src/server/api/endpoints/admin/captcha/current.ts deleted file mode 100644 index 63ec740348..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/captcha/current.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js'; - -export const meta = { - tags: ['admin', 'captcha'], - - requireCredential: true, - requireAdmin: true, - - // 実態はmetaの取得であるため - kind: 'read:admin:meta', - - res: { - type: 'object', - properties: { - provider: { - type: 'string', - enum: supportedCaptchaProviders, - }, - hcaptcha: { - type: 'object', - properties: { - siteKey: { type: 'string', nullable: true }, - secretKey: { type: 'string', nullable: true }, - }, - }, - mcaptcha: { - type: 'object', - properties: { - siteKey: { type: 'string', nullable: true }, - secretKey: { type: 'string', nullable: true }, - instanceUrl: { type: 'string', nullable: true }, - }, - }, - recaptcha: { - type: 'object', - properties: { - siteKey: { type: 'string', nullable: true }, - secretKey: { type: 'string', nullable: true }, - }, - }, - turnstile: { - type: 'object', - properties: { - siteKey: { type: 'string', nullable: true }, - secretKey: { type: 'string', nullable: true }, - }, - }, - }, - }, -} as const; - -export const paramDef = {} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private captchaService: CaptchaService, - ) { - super(meta, paramDef, async () => { - return this.captchaService.get(); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/captcha/save.ts b/packages/backend/src/server/api/endpoints/admin/captcha/save.ts deleted file mode 100644 index 98ec278ebe..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/captcha/save.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { captchaErrorCodes, CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['admin', 'captcha'], - - requireCredential: true, - requireAdmin: true, - - // 実態はmetaの更新であるため - kind: 'write:admin:meta', - - errors: { - invalidProvider: { - message: 'Invalid provider.', - code: 'INVALID_PROVIDER', - id: '14bf7ae1-80cc-4363-acb2-4fd61d086af0', - httpStatusCode: 400, - }, - invalidParameters: { - message: 'Invalid parameters.', - code: 'INVALID_PARAMETERS', - id: '26654194-410e-44e2-b42e-460ff6f92476', - httpStatusCode: 400, - }, - noResponseProvided: { - message: 'No response provided.', - code: 'NO_RESPONSE_PROVIDED', - id: '40acbba8-0937-41fb-bb3f-474514d40afe', - httpStatusCode: 400, - }, - requestFailed: { - message: 'Request failed.', - code: 'REQUEST_FAILED', - id: '0f4fe2f1-2c15-4d6e-b714-efbfcde231cd', - httpStatusCode: 500, - }, - verificationFailed: { - message: 'Verification failed.', - code: 'VERIFICATION_FAILED', - id: 'c41c067f-24f3-4150-84b2-b5a3ae8c2214', - httpStatusCode: 400, - }, - unknown: { - message: 'unknown', - code: 'UNKNOWN', - id: 'f868d509-e257-42a9-99c1-42614b031a97', - httpStatusCode: 500, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - provider: { - type: 'string', - enum: supportedCaptchaProviders, - }, - captchaResult: { - type: 'string', nullable: true, - }, - sitekey: { - type: 'string', nullable: true, - }, - secret: { - type: 'string', nullable: true, - }, - instanceUrl: { - type: 'string', nullable: true, - }, - }, - required: ['provider'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private captchaService: CaptchaService, - ) { - super(meta, paramDef, async (ps) => { - const result = await this.captchaService.save(ps.provider, { - sitekey: ps.sitekey, - secret: ps.secret, - instanceUrl: ps.instanceUrl, - captchaResult: ps.captchaResult, - }); - - if (!result.success) { - switch (result.error.code) { - case captchaErrorCodes.invalidProvider: - throw new ApiError({ - ...meta.errors.invalidProvider, - message: result.error.message, - }); - case captchaErrorCodes.invalidParameters: - throw new ApiError({ - ...meta.errors.invalidParameters, - message: result.error.message, - }); - case captchaErrorCodes.noResponseProvided: - throw new ApiError({ - ...meta.errors.noResponseProvided, - message: result.error.message, - }); - case captchaErrorCodes.requestFailed: - throw new ApiError({ - ...meta.errors.requestFailed, - message: result.error.message, - }); - case captchaErrorCodes.verificationFailed: - throw new ApiError({ - ...meta.errors.verificationFailed, - message: result.error.message, - }); - default: - throw new ApiError(meta.errors.unknown); - } - } - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts index 9065a71f6a..d0485fddd8 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-account.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/_.js'; +import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DeleteAccountService } from '@/core/DeleteAccountService.js'; import { DI } from '@/di-symbols.js'; @@ -14,7 +9,9 @@ export const meta = { requireCredential: true, requireAdmin: true, - kind: 'write:admin:delete-account', + + res: { + }, } as const; export const paramDef = { @@ -25,21 +22,22 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, private deleteAccountService: DeleteAccountService, ) { - super(meta, paramDef, async (ps, me) => { + super(meta, paramDef, async (ps) => { const user = await this.usersRepository.findOneByOrFail({ id: ps.userId }); if (user.isDeleted) { return; } - await this.deleteAccountService.deleteAccount(user, me); + await this.deleteAccountService.deleteAccount(user); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts index d8341b3ad7..c193ed3fb3 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DriveService } from '@/core/DriveService.js'; import { DI } from '@/di-symbols.js'; @@ -14,7 +9,6 @@ export const meta = { requireCredential: true, requireAdmin: true, - kind: 'write:admin:delete-all-files-of-a-user', } as const; export const paramDef = { @@ -25,8 +19,9 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts index d420a929bd..a8964af449 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; @@ -12,7 +7,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:drive', } as const; export const paramDef = { @@ -21,8 +15,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private queueService: QueueService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts index d612572e2e..4f7e02fe92 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DriveService } from '@/core/DriveService.js'; import { DI } from '@/di-symbols.js'; @@ -15,7 +10,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:drive', } as const; export const paramDef = { @@ -24,8 +18,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts index 915d777e77..8a4498d5fa 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/files.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; @@ -15,7 +10,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'read:admin:drive', res: { type: 'array', @@ -47,8 +41,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -81,7 +76,7 @@ export default class extends Endpoint { // eslint- } } - const files = await query.limit(ps.limit).getMany(); + const files = await query.take(ps.limit).getMany(); return await this.driveFileEntityService.packMany(files, { detail: true, withUser: true, self: true }); }); diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts index b84a5c73f9..1d27ac2137 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts @@ -1,14 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository, UsersRepository } from '@/models/_.js'; +import type { DriveFilesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; -import { IdService } from '@/core/IdService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -16,7 +10,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'read:admin:drive', errors: { noSuchFile: { @@ -61,7 +54,7 @@ export const meta = { name: { type: 'string', optional: false, nullable: false, - example: '192.jpg', + example: 'lenna.jpg', }, type: { type: 'string', @@ -84,24 +77,6 @@ export const meta = { properties: { type: 'object', optional: false, nullable: false, - properties: { - width: { - type: 'number', - optional: true, nullable: false, - }, - height: { - type: 'number', - optional: true, nullable: false, - }, - orientation: { - type: 'number', - optional: true, nullable: false, - }, - avgColor: { - type: 'string', - optional: true, nullable: false, - }, - }, }, storedInternal: { type: 'boolean', @@ -162,26 +137,20 @@ export const meta = { } as const; export const paramDef = { + type: 'object', + properties: { + fileId: { type: 'string', format: 'misskey:id' }, + url: { type: 'string' }, + }, anyOf: [ - { - type: 'object', - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - }, - required: ['fileId'], - }, - { - type: 'object', - properties: { - url: { type: 'string' }, - }, - required: ['url'], - }, + { required: ['fileId'] }, + { required: ['url'] }, ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -190,14 +159,17 @@ export default class extends Endpoint { // eslint- private usersRepository: UsersRepository, private roleService: RoleService, - private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - const file = await this.driveFilesRepository.findOneBy( - 'fileId' in ps - ? { id: ps.fileId } - : [{ url: ps.url }, { thumbnailUrl: ps.url }, { webpublicUrl: ps.url }], - ); + const file = ps.fileId ? await this.driveFilesRepository.findOneBy({ id: ps.fileId }) : await this.driveFilesRepository.findOne({ + where: [{ + url: ps.url, + }, { + thumbnailUrl: ps.url, + }, { + webpublicUrl: ps.url, + }], + }); if (file == null) { throw new ApiError(meta.errors.noSuchFile); @@ -236,7 +208,7 @@ export default class extends Endpoint { // eslint- type: file.type, name: file.name, md5: file.md5, - createdAt: this.idService.parse(file.id).date.toISOString(), + createdAt: file.createdAt.toISOString(), requestIp: iAmModerator ? file.requestIp : null, requestHeaders: iAmModerator && !ownerIsModerator ? file.requestHeaders : null, }; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts index 1459351d37..6e604ed885 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts @@ -1,9 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -11,8 +6,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requiredRolePolicy: 'canManageCustomEmojis', - kind: 'write:admin:emoji', + requireRolePolicy: 'canManageCustomEmojis', } as const; export const paramDef = { @@ -28,8 +22,9 @@ export const paramDef = { required: ['ids', 'aliases'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private customEmojiService: CustomEmojiService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 3852146177..2fcf0da3f0 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -1,23 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import { FILE_TYPE_IMAGE } from '@/const.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], requireCredential: true, - requiredRolePolicy: 'canManageCustomEmojis', - kind: 'write:admin:emoji', + requireRolePolicy: 'canManageCustomEmojis', errors: { noSuchFile: { @@ -25,21 +18,6 @@ export const meta = { code: 'NO_SUCH_FILE', id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf', }, - unsupportedFileType: { - message: 'Unsupported file type.', - code: 'UNSUPPORTED_FILE_TYPE', - id: 'f7599d96-8750-af68-1633-9575d625c1a7', - }, - duplicateName: { - message: 'Duplicate name.', - code: 'DUPLICATE_NAME', - id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', - }, - }, - - res: { - type: 'object', - ref: 'EmojiDetailed', }, } as const; @@ -53,46 +31,38 @@ export const paramDef = { nullable: true, description: 'Use `null` to reset the category.', }, - aliases: { - type: 'array', - items: { - type: 'string', - }, - }, + aliases: { type: 'array', items: { + type: 'string', + } }, license: { type: 'string', nullable: true }, isSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, - roleIdsThatCanBeUsedThisEmojiAsReaction: { - type: 'array', - items: { - type: 'string', - }, - }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { + type: 'string', + } }, }, required: ['name', 'fileId'], } as const; // TODO: ロジックをサービスに切り出す +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + private customEmojiService: CustomEmojiService, - private emojiEntityService: EmojiEntityService, + + private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); - const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); - if (isDuplicate) throw new ApiError(meta.errors.duplicateName); - if (!FILE_TYPE_IMAGE.includes(driveFile.type)) throw new ApiError(meta.errors.unsupportedFileType); const emoji = await this.customEmojiService.add({ - originalUrl: driveFile.url, - publicUrl: driveFile.webpublicUrl ?? driveFile.url, - fileType: driveFile.webpublicType ?? driveFile.type, + driveFile, name: ps.name, category: ps.category ?? null, aliases: ps.aliases ?? [], @@ -101,9 +71,15 @@ export default class extends Endpoint { // eslint- isSensitive: ps.isSensitive ?? false, localOnly: ps.localOnly ?? false, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], - }, me); + }); - return this.emojiEntityService.packDetailed(emoji); + this.moderationLogService.insertModerationLog(me, 'addEmoji', { + emojiId: emoji.id, + }); + + return { + id: emoji.id, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index cf03859ce5..82dca9cc70 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -1,15 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { EmojisRepository } from '@/models/_.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { EmojisRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; import { DI } from '@/di-symbols.js'; import { DriveService } from '@/core/DriveService.js'; -import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { ApiError } from '../../../error.js'; @@ -17,8 +14,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requiredRolePolicy: 'canManageCustomEmojis', - kind: 'write:admin:emoji', + requireRolePolicy: 'canManageCustomEmojis', errors: { noSuchEmoji: { @@ -26,11 +22,6 @@ export const meta = { code: 'NO_SUCH_EMOJI', id: 'e2785b66-dca3-4087-9cac-b93c541cc425', }, - duplicateName: { - message: 'Duplicate name.', - code: 'DUPLICATE_NAME', - id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', - }, }, res: { @@ -56,50 +47,56 @@ export const paramDef = { // TODO: ロジックをサービスに切り出す +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.db) + private db: DataSource, + @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, + private emojiEntityService: EmojiEntityService, - private customEmojiService: CustomEmojiService, + private idService: IdService, + private globalEventService: GlobalEventService, private driveService: DriveService, ) { super(meta, paramDef, async (ps, me) => { const emoji = await this.emojisRepository.findOneBy({ id: ps.emojiId }); + if (emoji == null) { throw new ApiError(meta.errors.noSuchEmoji); } - let driveFile: MiDriveFile; + let driveFile: DriveFile; try { // Create file driveFile = await this.driveService.uploadFromUrl({ url: emoji.originalUrl, user: null, force: true }); } catch (e) { - // TODO: need to return Drive Error throw new ApiError(); } - // Duplication Check - const isDuplicate = await this.customEmojiService.checkDuplicate(emoji.name); - if (isDuplicate) throw new ApiError(meta.errors.duplicateName); - - const addedEmoji = await this.customEmojiService.add({ + const copied = await this.emojisRepository.insert({ + id: this.idService.genId(), + updatedAt: new Date(), + name: emoji.name, + host: null, + aliases: [], originalUrl: driveFile.url, publicUrl: driveFile.webpublicUrl ?? driveFile.url, - fileType: driveFile.webpublicType ?? driveFile.type, - name: emoji.name, - category: emoji.category, - aliases: emoji.aliases, - host: null, + type: driveFile.webpublicType ?? driveFile.type, license: emoji.license, - isSensitive: emoji.isSensitive, - localOnly: emoji.localOnly, - roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, - }, me); + }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); - return this.emojiEntityService.packDetailed(addedEmoji); + this.globalEventService.publishBroadcastStream('emojiAdded', { + emoji: await this.emojiEntityService.packDetailed(copied.id), + }); + + return { + id: copied.id, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts index 7993edcc07..9f8263629b 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts @@ -1,9 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -11,8 +6,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requiredRolePolicy: 'canManageCustomEmojis', - kind: 'write:admin:emoji', + requireRolePolicy: 'canManageCustomEmojis', } as const; export const paramDef = { @@ -25,13 +19,14 @@ export const paramDef = { required: ['ids'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - await this.customEmojiService.deleteBulk(ps.ids, me); + await this.customEmojiService.deleteBulk(ps.ids); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index 87ed3f5f18..429c819fe0 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -1,9 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -11,8 +6,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requiredRolePolicy: 'canManageCustomEmojis', - kind: 'write:admin:emoji', + requireRolePolicy: 'canManageCustomEmojis', errors: { noSuchEmoji: { @@ -31,13 +25,14 @@ export const paramDef = { required: ['id'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - await this.customEmojiService.delete(ps.id, me); + await this.customEmojiService.delete(ps.id); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts index 7ca931eb21..e26f0506ce 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; @@ -10,7 +5,7 @@ import { QueueService } from '@/core/QueueService.js'; export const meta = { secure: true, requireCredential: true, - requiredRolePolicy: 'canManageCustomEmojis', + requireRolePolicy: 'canManageCustomEmojis', } as const; export const paramDef = { @@ -21,8 +16,9 @@ export const paramDef = { required: ['fileId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private queueService: QueueService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index b44007962d..df3c28deff 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { EmojisRepository } from '@/models/_.js'; +import type { EmojisRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; @@ -16,8 +11,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requiredRolePolicy: 'canManageCustomEmojis', - kind: 'read:admin:emoji', + requireRolePolicy: 'canManageCustomEmojis', res: { type: 'array', @@ -78,8 +72,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, @@ -103,7 +98,7 @@ export default class extends Endpoint { // eslint- const emojis = await q .orderBy('emoji.id', 'DESC') - .limit(ps.limit) + .take(ps.limit) .getMany(); return this.emojiEntityService.packDetailedMany(emojis); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index 4342e178cc..4aa4ad82b4 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { EmojisRepository } from '@/models/_.js'; -import type { MiEmoji } from '@/models/Emoji.js'; +import type { EmojisRepository } from '@/models/index.js'; +import type { Emoji } from '@/models/entities/Emoji.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; @@ -16,8 +11,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requiredRolePolicy: 'canManageCustomEmojis', - kind: 'read:admin:emoji', + requireRolePolicy: 'canManageCustomEmojis', res: { type: 'array', @@ -72,8 +66,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, @@ -85,18 +80,18 @@ export default class extends Endpoint { // eslint- const q = this.queryService.makePaginationQuery(this.emojisRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId) .andWhere('emoji.host IS NULL'); - let emojis: MiEmoji[]; + let emojis: Emoji[]; if (ps.query) { //q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); - //const emojis = await q.limit(ps.limit).getMany(); + //const emojis = await q.take(ps.limit).getMany(); emojis = await q.getMany(); const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g); if (queryarry) { - emojis = emojis.filter(emoji => - queryarry.includes(`:${emoji.name}:`), + emojis = emojis.filter(emoji => + queryarry.includes(`:${emoji.name}:`) ); } else { emojis = emojis.filter(emoji => @@ -106,7 +101,7 @@ export default class extends Endpoint { // eslint- } emojis.splice(ps.limit + 1); } else { - emojis = await q.limit(ps.limit).getMany(); + emojis = await q.take(ps.limit).getMany(); } return this.emojiEntityService.packDetailedMany(emojis); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts index 161c3b9f37..83f882cac5 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts @@ -1,9 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -11,8 +6,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requiredRolePolicy: 'canManageCustomEmojis', - kind: 'write:admin:emoji', + requireRolePolicy: 'canManageCustomEmojis', } as const; export const paramDef = { @@ -28,8 +22,9 @@ export const paramDef = { required: ['ids', 'aliases'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private customEmojiService: CustomEmojiService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts index 2e700809d8..1d3a432bb7 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts @@ -1,9 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -11,8 +6,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requiredRolePolicy: 'canManageCustomEmojis', - kind: 'write:admin:emoji', + requireRolePolicy: 'canManageCustomEmojis', } as const; export const paramDef = { @@ -28,8 +22,9 @@ export const paramDef = { required: ['ids', 'aliases'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private customEmojiService: CustomEmojiService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts index ee87858b0e..453968c7a9 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts @@ -1,9 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -11,8 +6,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requiredRolePolicy: 'canManageCustomEmojis', - kind: 'write:admin:emoji', + requireRolePolicy: 'canManageCustomEmojis', } as const; export const paramDef = { @@ -30,8 +24,9 @@ export const paramDef = { required: ['ids'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private customEmojiService: CustomEmojiService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts index 7ab5916951..b90b9757be 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts @@ -1,9 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -11,8 +6,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requiredRolePolicy: 'canManageCustomEmojis', - kind: 'write:admin:emoji', + requireRolePolicy: 'canManageCustomEmojis', } as const; export const paramDef = { @@ -30,8 +24,9 @@ export const paramDef = { required: ['ids'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private customEmojiService: CustomEmojiService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 7bde10af46..fb22bdc477 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import type { DriveFilesRepository, MiEmoji } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -14,8 +9,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requiredRolePolicy: 'canManageCustomEmojis', - kind: 'write:admin:emoji', + requireRolePolicy: 'canManageCustomEmojis', errors: { noSuchEmoji: { @@ -37,50 +31,32 @@ export const meta = { } as const; export const paramDef = { - allOf: [ - { - anyOf: [ - { - type: 'object', - properties: { - id: { type: 'string', format: 'misskey:id' }, - }, - required: ['id'], - }, - { - type: 'object', - properties: { - name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, - }, - required: ['name'], - }, - ], + type: 'object', + properties: { + id: { type: 'string', format: 'misskey:id' }, + name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, + fileId: { type: 'string', format: 'misskey:id' }, + category: { + type: 'string', + nullable: true, + description: 'Use `null` to reset the category.', }, - { - type: 'object', - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - category: { - type: 'string', - nullable: true, - description: 'Use `null` to reset the category.', - }, - aliases: { type: 'array', items: { - type: 'string', - } }, - license: { type: 'string', nullable: true }, - isSensitive: { type: 'boolean' }, - localOnly: { type: 'boolean' }, - roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { - type: 'string', - } }, - }, - }, - ], + aliases: { type: 'array', items: { + type: 'string', + } }, + license: { type: 'string', nullable: true }, + isSensitive: { type: 'boolean' }, + localOnly: { type: 'boolean' }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { + type: 'string', + } }, + }, + required: ['id', 'name', 'aliases'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -89,35 +65,22 @@ export default class extends Endpoint { // eslint- ) { super(meta, paramDef, async (ps, me) => { let driveFile; + if (ps.fileId) { driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); } - - const required = 'id' in ps - ? { id: ps.id, name: 'name' in ps ? ps.name as string : undefined } - : { name: ps.name }; - - const error = await this.customEmojiService.update({ - ...required, - originalUrl: driveFile != null ? driveFile.url : undefined, - publicUrl: driveFile != null ? (driveFile.webpublicUrl ?? driveFile.url) : undefined, - fileType: driveFile != null ? (driveFile.webpublicType ?? driveFile.type) : undefined, - category: ps.category, + + await this.customEmojiService.update(ps.id, { + driveFile, + name: ps.name, + category: ps.category ?? null, aliases: ps.aliases, - license: ps.license, + license: ps.license ?? null, isSensitive: ps.isSensitive, localOnly: ps.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, - }, me); - - switch (error) { - case null: return; - case 'NO_SUCH_EMOJI': throw new ApiError(meta.errors.noSuchEmoji); - case 'SAME_NAME_EMOJI_EXISTS': throw new ApiError(meta.errors.sameNameEmojiExists); - } - // 網羅性チェック - const mustBeNever: never = error; + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts index 4a54c26009..38fe99b222 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DriveService } from '@/core/DriveService.js'; import { DI } from '@/di-symbols.js'; @@ -14,7 +9,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:federation', } as const; export const paramDef = { @@ -25,8 +19,9 @@ export const paramDef = { required: ['host'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts index 556e291025..b7f2858a77 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { InstancesRepository } from '@/models/_.js'; +import type { InstancesRepository } from '@/models/index.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; @@ -15,7 +10,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:federation', } as const; export const paramDef = { @@ -26,8 +20,9 @@ export const paramDef = { required: ['host'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts index 9e93310746..83f729953a 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository, UsersRepository } from '@/models/_.js'; +import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { QueueService } from '@/core/QueueService.js'; @@ -14,7 +9,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:federation', } as const; export const paramDef = { @@ -25,8 +19,9 @@ export const paramDef = { required: ['host'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts index fed7bfbbde..4fd74e591d 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts @@ -1,22 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { InstancesRepository } from '@/models/_.js'; +import type { InstancesRepository } from '@/models/index.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], requireCredential: true, requireModerator: true, - kind: 'write:admin:federation', } as const; export const paramDef = { @@ -24,20 +17,19 @@ export const paramDef = { properties: { host: { type: 'string' }, isSuspended: { type: 'boolean' }, - moderationNote: { type: 'string' }, }, - required: ['host'], + required: ['host', 'isSuspended'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, private utilityService: UtilityService, private federatedInstanceService: FederatedInstanceService, - private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { const instance = await this.instancesRepository.findOneBy({ host: this.utilityService.toPuny(ps.host) }); @@ -46,40 +38,9 @@ export default class extends Endpoint { // eslint- throw new Error('instance not found'); } - const isSuspendedBefore = instance.suspensionState !== 'none'; - let suspensionState: undefined | 'manuallySuspended' | 'none'; - - if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) { - suspensionState = ps.isSuspended ? 'manuallySuspended' : 'none'; - } - - await this.federatedInstanceService.update(instance.id, { - suspensionState, - moderationNote: ps.moderationNote, + this.federatedInstanceService.update(instance.id, { + isSuspended: ps.isSuspended, }); - - if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) { - if (ps.isSuspended) { - this.moderationLogService.log(me, 'suspendRemoteInstance', { - id: instance.id, - host: instance.host, - }); - } else { - this.moderationLogService.log(me, 'unsuspendRemoteInstance', { - id: instance.id, - host: instance.host, - }); - } - } - - if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) { - this.moderationLogService.log(me, 'updateRemoteInstanceNote', { - id: instance.id, - host: instance.host, - before: instance.moderationNote, - after: ps.moderationNote, - }); - } }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts deleted file mode 100644 index 3e42c91fed..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AbuseUserReportsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '@/server/api/error.js'; -import { AbuseReportService } from '@/core/AbuseReportService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'write:admin:resolve-abuse-user-report', - - errors: { - noSuchAbuseReport: { - message: 'No such abuse report.', - code: 'NO_SUCH_ABUSE_REPORT', - id: '8763e21b-d9bc-40be-acf6-54c1a6986493', - kind: 'server', - httpStatusCode: 404, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - reportId: { type: 'string', format: 'misskey:id' }, - }, - required: ['reportId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.abuseUserReportsRepository) - private abuseUserReportsRepository: AbuseUserReportsRepository, - private abuseReportService: AbuseReportService, - ) { - super(meta, paramDef, async (ps, me) => { - const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId }); - if (!report) { - throw new ApiError(meta.errors.noSuchAbuseReport); - } - - await this.abuseReportService.forward(report.id, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts index 90a3fa0200..8ffd2b01e7 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -11,19 +6,8 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, requireAdmin: true, - kind: 'read:admin:index-stats', tags: ['admin'], - res: { - type: 'array', - items: { - type: 'object', - properties: { - tablename: { type: 'string' }, - indexname: { type: 'string' }, - }, - }, - }, } as const; export const paramDef = { @@ -32,8 +16,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.db) private db: DataSource, diff --git a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts index eb85fca179..09d61bd741 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -11,25 +6,12 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, requireAdmin: true, - kind: 'read:admin:table-stats', tags: ['admin'], res: { type: 'object', optional: false, nullable: false, - additionalProperties: { - type: 'object', - properties: { - count: { - type: 'number', - }, - size: { - type: 'number', - }, - }, - required: ['count', 'size'], - }, example: { migrations: { count: 66, @@ -45,8 +27,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.db) private db: DataSource, diff --git a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts index b7781b8c99..bfcc8a700b 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts @@ -1,39 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UserIpsRepository } from '@/models/_.js'; +import type { UserIpsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['admin'], requireCredential: true, requireModerator: true, - kind: 'read:admin:user-ips', - res: { - type: 'array', - optional: false, - nullable: false, - items: { - type: 'object', - optional: false, - nullable: false, - properties: { - ip: { type: 'string' }, - createdAt: { - type: 'string', - optional: false, - nullable: false, - format: 'date-time', - }, - }, - }, - }, } as const; export const paramDef = { @@ -44,18 +18,17 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userIpsRepository) private userIpsRepository: UserIpsRepository, - - private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const ips = await this.userIpsRepository.find({ where: { userId: ps.userId }, - order: { id: 'DESC' }, + order: { createdAt: 'DESC' }, take: 30, }); diff --git a/packages/backend/src/server/api/endpoints/admin/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts deleted file mode 100644 index e52b177e2b..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistrationTicketsRepository } from '@/models/_.js'; -import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js'; -import { IdService } from '@/core/IdService.js'; -import { DI } from '@/di-symbols.js'; -import { generateInviteCode } from '@/misc/generate-invite-code.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'write:admin:invite-codes', - - errors: { - invalidDateTime: { - message: 'Invalid date-time format', - code: 'INVALID_DATE_TIME', - id: 'f1380b15-3760-4c6c-a1db-5c3aaf1cbd49', - }, - }, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'InviteCode', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - count: { type: 'integer', minimum: 1, maximum: 100, default: 1 }, - expiresAt: { type: 'string', nullable: true }, - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.registrationTicketsRepository) - private registrationTicketsRepository: RegistrationTicketsRepository, - - private inviteCodeEntityService: InviteCodeEntityService, - private idService: IdService, - private moderationLogService: ModerationLogService, - ) { - super(meta, paramDef, async (ps, me) => { - if (ps.expiresAt && isNaN(Date.parse(ps.expiresAt))) { - throw new ApiError(meta.errors.invalidDateTime); - } - - const ticketsPromises = []; - - for (let i = 0; i < ps.count; i++) { - ticketsPromises.push(this.registrationTicketsRepository.insertOne({ - id: this.idService.gen(), - createdBy: me, - createdById: me.id, - expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null, - code: generateInviteCode(), - })); - } - - const tickets = await Promise.all(ticketsPromises); - - this.moderationLogService.log(me, 'createInvitation', { - invitations: tickets, - }); - - return await this.inviteCodeEntityService.packMany(tickets, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/invite/list.ts b/packages/backend/src/server/api/endpoints/admin/invite/list.ts deleted file mode 100644 index e33a9a1aec..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/invite/list.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistrationTicketsRepository } from '@/models/_.js'; -import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'read:admin:invite-codes', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'InviteCode', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, - offset: { type: 'integer', default: 0 }, - type: { type: 'string', enum: ['unused', 'used', 'expired', 'all'], default: 'all' }, - sort: { type: 'string', enum: ['+createdAt', '-createdAt', '+usedAt', '-usedAt'] }, - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.registrationTicketsRepository) - private registrationTicketsRepository: RegistrationTicketsRepository, - - private inviteCodeEntityService: InviteCodeEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registrationTicketsRepository.createQueryBuilder('ticket') - .leftJoinAndSelect('ticket.createdBy', 'createdBy') - .leftJoinAndSelect('ticket.usedBy', 'usedBy'); - - switch (ps.type) { - case 'unused': query.andWhere('ticket.usedBy IS NULL'); break; - case 'used': query.andWhere('ticket.usedBy IS NOT NULL'); break; - case 'expired': query.andWhere('ticket.expiresAt < :now', { now: new Date() }); break; - } - - switch (ps.sort) { - case '+createdAt': query.orderBy('ticket.id', 'DESC'); break; - case '-createdAt': query.orderBy('ticket.id', 'ASC'); break; - case '+usedAt': query.orderBy('ticket.usedAt', 'DESC', 'NULLS LAST'); break; - case '-usedAt': query.orderBy('ticket.usedAt', 'ASC', 'NULLS FIRST'); break; - default: query.orderBy('ticket.id', 'DESC'); break; - } - - query.limit(ps.limit); - query.offset(ps.offset); - - const tickets = await query.getMany(); - - return await this.inviteCodeEntityService.packMany(tickets, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 924163afbb..4cc1b6011f 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -1,22 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; +import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; -import { SystemAccountService } from '@/core/SystemAccountService.js'; export const meta = { tags: ['meta'], requireCredential: true, requireAdmin: true, - kind: 'read:admin:meta', res: { type: 'object', @@ -26,10 +20,6 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - cacheRemoteSensitiveFiles: { - type: 'boolean', - optional: false, nullable: false, - }, emailRequiredForSignup: { type: 'boolean', optional: false, nullable: false, @@ -42,18 +32,6 @@ export const meta = { type: 'string', optional: false, nullable: true, }, - enableMcaptcha: { - type: 'boolean', - optional: false, nullable: false, - }, - mcaptchaSiteKey: { - type: 'string', - optional: false, nullable: true, - }, - mcaptchaInstanceUrl: { - type: 'string', - optional: false, nullable: true, - }, enableRecaptcha: { type: 'boolean', optional: false, nullable: false, @@ -70,14 +48,6 @@ export const meta = { type: 'string', optional: false, nullable: true, }, - enableTestcaptcha: { - type: 'boolean', - optional: false, nullable: false, - }, - googleAnalyticsMeasurementId: { - type: 'string', - optional: false, nullable: true, - }, swPublickey: { type: 'string', optional: false, nullable: true, @@ -107,14 +77,6 @@ export const meta = { type: 'string', optional: false, nullable: true, }, - app192IconUrl: { - type: 'string', - optional: false, nullable: true, - }, - app512IconUrl: { - type: 'string', - optional: false, nullable: true, - }, enableEmail: { type: 'boolean', optional: false, nullable: false, @@ -127,69 +89,35 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - silencedHosts: { - type: 'array', - optional: true, - nullable: false, - items: { - type: 'string', - optional: false, - nullable: false, - }, - }, - mediaSilencedHosts: { - type: 'array', - optional: false, - nullable: false, - items: { - type: 'string', - optional: false, - nullable: false, - }, + userStarForReactionFallback: { + type: 'boolean', + optional: true, nullable: false, }, pinnedUsers: { type: 'array', - optional: false, nullable: false, + optional: true, nullable: false, items: { type: 'string', + optional: false, nullable: false, }, }, hiddenTags: { type: 'array', - optional: false, nullable: false, + optional: true, nullable: false, items: { type: 'string', + optional: false, nullable: false, }, }, blockedHosts: { type: 'array', - optional: false, nullable: false, + optional: true, nullable: false, items: { type: 'string', + optional: false, nullable: false, }, }, sensitiveWords: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - }, - }, - prohibitedWords: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - }, - }, - prohibitedWordsForNameOfUser: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - }, - }, - bannedEmailDomains: { type: 'array', optional: true, nullable: false, items: { @@ -202,148 +130,129 @@ export const meta = { optional: false, nullable: false, items: { type: 'string', + optional: false, nullable: false, }, }, hcaptchaSecretKey: { type: 'string', - optional: false, nullable: true, - }, - mcaptchaSecretKey: { - type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, recaptchaSecretKey: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, turnstileSecretKey: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, sensitiveMediaDetection: { type: 'string', - optional: false, nullable: false, + optional: true, nullable: false, }, sensitiveMediaDetectionSensitivity: { type: 'string', - optional: false, nullable: false, + optional: true, nullable: false, }, setSensitiveFlagAutomatically: { type: 'boolean', - optional: false, nullable: false, + optional: true, nullable: false, }, enableSensitiveMediaDetectionForVideos: { type: 'boolean', - optional: false, nullable: false, + optional: true, nullable: false, }, proxyAccountId: { type: 'string', - optional: false, nullable: false, + optional: true, nullable: true, format: 'id', }, + summaryProxy: { + type: 'string', + optional: true, nullable: true, + }, email: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, smtpSecure: { type: 'boolean', - optional: false, nullable: false, + optional: true, nullable: false, }, smtpHost: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, smtpPort: { type: 'number', - optional: false, nullable: true, + optional: true, nullable: true, }, smtpUser: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, smtpPass: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, swPrivateKey: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, useObjectStorage: { type: 'boolean', - optional: false, nullable: false, + optional: true, nullable: false, }, objectStorageBaseUrl: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, objectStorageBucket: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, objectStoragePrefix: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, objectStorageEndpoint: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, objectStorageRegion: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, objectStoragePort: { type: 'number', - optional: false, nullable: true, + optional: true, nullable: true, }, objectStorageAccessKey: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, objectStorageSecretKey: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, }, objectStorageUseSSL: { type: 'boolean', - optional: false, nullable: false, + optional: true, nullable: false, }, objectStorageUseProxy: { type: 'boolean', - optional: false, nullable: false, + optional: true, nullable: false, }, objectStorageSetPublicRead: { type: 'boolean', - optional: false, nullable: false, + optional: true, nullable: false, }, enableIpLogging: { type: 'boolean', - optional: false, nullable: false, + optional: true, nullable: false, }, enableActiveEmailValidation: { type: 'boolean', - optional: false, nullable: false, - }, - enableVerifymailApi: { - type: 'boolean', - optional: false, nullable: false, - }, - verifymailAuthKey: { - type: 'string', - optional: false, nullable: true, - }, - enableTruemailApi: { - type: 'boolean', - optional: false, nullable: false, - }, - truemailInstance: { - type: 'string', - optional: false, nullable: true, - }, - truemailAuthKey: { - type: 'string', - optional: false, nullable: true, + optional: true, nullable: false, }, enableChartsForRemoteUser: { type: 'boolean', @@ -353,224 +262,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - enableStatsForFederatedInstances: { - type: 'boolean', - optional: false, nullable: false, - }, - enableServerMachineStats: { - type: 'boolean', - optional: false, nullable: false, - }, - enableIdenticonGeneration: { - type: 'boolean', - optional: false, nullable: false, - }, - manifestJsonOverride: { - type: 'string', - optional: false, nullable: false, - }, policies: { type: 'object', optional: false, nullable: false, }, - enableFanoutTimeline: { - type: 'boolean', - optional: false, nullable: false, - }, - enableFanoutTimelineDbFallback: { - type: 'boolean', - optional: false, nullable: false, - }, - perLocalUserUserTimelineCacheMax: { - type: 'number', - optional: false, nullable: false, - }, - perRemoteUserUserTimelineCacheMax: { - type: 'number', - optional: false, nullable: false, - }, - perUserHomeTimelineCacheMax: { - type: 'number', - optional: false, nullable: false, - }, - perUserListTimelineCacheMax: { - type: 'number', - optional: false, nullable: false, - }, - enableReactionsBuffering: { - type: 'boolean', - optional: false, nullable: false, - }, - notesPerOneAd: { - type: 'number', - optional: false, nullable: false, - }, - backgroundImageUrl: { - type: 'string', - optional: false, nullable: true, - }, - deeplAuthKey: { - type: 'string', - optional: false, nullable: true, - }, - deeplIsPro: { - type: 'boolean', - optional: false, nullable: false, - }, - defaultDarkTheme: { - type: 'string', - optional: false, nullable: true, - }, - defaultLightTheme: { - type: 'string', - optional: false, nullable: true, - }, - description: { - type: 'string', - optional: false, nullable: true, - }, - disableRegistration: { - type: 'boolean', - optional: false, nullable: false, - }, - impressumUrl: { - type: 'string', - optional: false, nullable: true, - }, - maintainerEmail: { - type: 'string', - optional: false, nullable: true, - }, - maintainerName: { - type: 'string', - optional: false, nullable: true, - }, - name: { - type: 'string', - optional: false, nullable: true, - }, - shortName: { - type: 'string', - optional: false, nullable: true, - }, - objectStorageS3ForcePathStyle: { - type: 'boolean', - optional: false, nullable: false, - }, - privacyPolicyUrl: { - type: 'string', - optional: false, nullable: true, - }, - inquiryUrl: { - type: 'string', - optional: false, nullable: true, - }, - repositoryUrl: { - type: 'string', - optional: false, nullable: true, - }, - summalyProxy: { - type: 'string', - optional: false, nullable: true, - deprecated: true, - description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.', - }, - themeColor: { - type: 'string', - optional: false, nullable: true, - }, - tosUrl: { - type: 'string', - optional: false, nullable: true, - }, - uri: { - type: 'string', - optional: false, nullable: false, - }, - version: { - type: 'string', - optional: false, nullable: false, - }, - urlPreviewEnabled: { - type: 'boolean', - optional: false, nullable: false, - }, - urlPreviewAllowRedirect: { - type: 'boolean', - optional: false, nullable: false, - }, - urlPreviewTimeout: { - type: 'number', - optional: false, nullable: false, - }, - urlPreviewMaximumContentLength: { - type: 'number', - optional: false, nullable: false, - }, - urlPreviewRequireContentLength: { - type: 'boolean', - optional: false, nullable: false, - }, - urlPreviewUserAgent: { - type: 'string', - optional: false, nullable: true, - }, - urlPreviewSummaryProxyUrl: { - type: 'string', - optional: false, nullable: true, - }, - federation: { - type: 'string', - enum: ['all', 'specified', 'none'], - optional: false, nullable: false, - }, - federationHosts: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - }, - }, - deliverSuspendedSoftware: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - software: { - type: 'string', - optional: false, nullable: false, - }, - versionRange: { - type: 'string', - optional: false, nullable: false, - }, - }, - }, - }, - singleUserMode: { - type: 'boolean', - optional: false, nullable: false, - }, - ugcVisibilityForVisitor: { - type: 'string', - enum: ['all', 'local', 'none'], - optional: false, nullable: false, - }, - proxyRemoteFiles: { - type: 'boolean', - optional: false, nullable: false, - }, - signToActivityPubGet: { - type: 'boolean', - optional: false, nullable: false, - }, - allowExternalApRedirect: { - type: 'boolean', - optional: false, nullable: false, - }, }, }, } as const; @@ -582,48 +277,37 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.config) private config: Config, private metaService: MetaService, - private systemAccountService: SystemAccountService, ) { - super(meta, paramDef, async () => { + super(meta, paramDef, async (ps, me) => { const instance = await this.metaService.fetch(true); - const proxy = await this.systemAccountService.fetch('proxy'); - return { maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, version: this.config.version, name: instance.name, - shortName: instance.shortName, uri: this.config.url, description: instance.description, langs: instance.langs, tosUrl: instance.termsOfServiceUrl, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, - impressumUrl: instance.impressumUrl, - privacyPolicyUrl: instance.privacyPolicyUrl, - inquiryUrl: instance.inquiryUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, - enableMcaptcha: instance.enableMcaptcha, - mcaptchaSiteKey: instance.mcaptchaSitekey, - mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl, enableRecaptcha: instance.enableRecaptcha, recaptchaSiteKey: instance.recaptchaSiteKey, enableTurnstile: instance.enableTurnstile, turnstileSiteKey: instance.turnstileSiteKey, - enableTestcaptcha: instance.enableTestcaptcha, - googleAnalyticsMeasurementId: instance.googleAnalyticsMeasurementId, swPublickey: instance.swPublicKey, themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl, @@ -632,8 +316,6 @@ export default class extends Endpoint { // eslint- notFoundImageUrl: instance.notFoundImageUrl, infoImageUrl: instance.infoImageUrl, iconUrl: instance.iconUrl, - app192IconUrl: instance.app192IconUrl, - app512IconUrl: instance.app512IconUrl, backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, defaultLightTheme: instance.defaultLightTheme, @@ -642,25 +324,20 @@ export default class extends Endpoint { // eslint- enableServiceWorker: instance.enableServiceWorker, translatorAvailable: instance.deeplAuthKey != null, cacheRemoteFiles: instance.cacheRemoteFiles, - cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles, pinnedUsers: instance.pinnedUsers, hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, - silencedHosts: instance.silencedHosts, - mediaSilencedHosts: instance.mediaSilencedHosts, sensitiveWords: instance.sensitiveWords, - prohibitedWords: instance.prohibitedWords, - prohibitedWordsForNameOfUser: instance.prohibitedWordsForNameOfUser, preservedUsernames: instance.preservedUsernames, hcaptchaSecretKey: instance.hcaptchaSecretKey, - mcaptchaSecretKey: instance.mcaptchaSecretKey, recaptchaSecretKey: instance.recaptchaSecretKey, turnstileSecretKey: instance.turnstileSecretKey, sensitiveMediaDetection: instance.sensitiveMediaDetection, sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity, setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, - proxyAccountId: proxy.id, + proxyAccountId: instance.proxyAccountId, + summalyProxy: instance.summalyProxy, email: instance.email, smtpSecure: instance.smtpSecure, smtpHost: instance.smtpHost, @@ -685,43 +362,9 @@ export default class extends Endpoint { // eslint- deeplIsPro: instance.deeplIsPro, enableIpLogging: instance.enableIpLogging, enableActiveEmailValidation: instance.enableActiveEmailValidation, - enableVerifymailApi: instance.enableVerifymailApi, - verifymailAuthKey: instance.verifymailAuthKey, - enableTruemailApi: instance.enableTruemailApi, - truemailInstance: instance.truemailInstance, - truemailAuthKey: instance.truemailAuthKey, enableChartsForRemoteUser: instance.enableChartsForRemoteUser, enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances, - enableStatsForFederatedInstances: instance.enableStatsForFederatedInstances, - enableServerMachineStats: instance.enableServerMachineStats, - enableIdenticonGeneration: instance.enableIdenticonGeneration, - bannedEmailDomains: instance.bannedEmailDomains, policies: { ...DEFAULT_POLICIES, ...instance.policies }, - manifestJsonOverride: instance.manifestJsonOverride, - enableFanoutTimeline: instance.enableFanoutTimeline, - enableFanoutTimelineDbFallback: instance.enableFanoutTimelineDbFallback, - perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax, - perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax, - perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, - perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, - enableReactionsBuffering: instance.enableReactionsBuffering, - notesPerOneAd: instance.notesPerOneAd, - summalyProxy: instance.urlPreviewSummaryProxyUrl, - urlPreviewEnabled: instance.urlPreviewEnabled, - urlPreviewAllowRedirect: instance.urlPreviewAllowRedirect, - urlPreviewTimeout: instance.urlPreviewTimeout, - urlPreviewMaximumContentLength: instance.urlPreviewMaximumContentLength, - urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength, - urlPreviewUserAgent: instance.urlPreviewUserAgent, - urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl, - federation: instance.federation, - federationHosts: instance.federationHosts, - deliverSuspendedSoftware: instance.deliverSuspendedSoftware, - singleUserMode: instance.singleUserMode, - ugcVisibilityForVisitor: instance.ugcVisibilityForVisitor, - proxyRemoteFiles: instance.proxyRemoteFiles, - signToActivityPubGet: instance.signToActivityPubGet, - allowExternalApRedirect: instance.allowExternalApRedirect, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts index 1d32c6cc00..bee1ffbaee 100644 --- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { PromoNotesRepository } from '@/models/_.js'; +import type { PromoNotesRepository } from '@/models/index.js'; import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -15,7 +10,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:promo', errors: { noSuchNote: { @@ -41,8 +35,9 @@ export const paramDef = { required: ['noteId', 'expiresAt'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.promoNotesRepository) private promoNotesRepository: PromoNotesRepository, @@ -55,9 +50,9 @@ export default class extends Endpoint { // eslint- throw e; }); - const exist = await this.promoNotesRepository.exists({ where: { noteId: note.id } }); + const exist = await this.promoNotesRepository.findOneBy({ noteId: note.id }); - if (exist) { + if (exist != null) { throw new ApiError(meta.errors.alreadyPromoted); } diff --git a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts index 81cb4b8119..099e2ff220 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts @@ -1,40 +1,32 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { tags: ['admin'], requireCredential: true, requireModerator: true, - kind: 'write:admin:queue', } as const; export const paramDef = { type: 'object', - properties: { - queue: { type: 'string', enum: QUEUE_TYPES }, - state: { type: 'string', enum: ['*', 'completed', 'wait', 'active', 'paused', 'prioritized', 'delayed', 'failed'] }, - }, - required: ['queue', 'state'], + properties: {}, + required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private moderationLogService: ModerationLogService, private queueService: QueueService, ) { super(meta, paramDef, async (ps, me) => { - this.queueService.queueClear(ps.queue, ps.state); + this.queueService.destroy(); - this.moderationLogService.log(me, 'clearQueue'); + this.moderationLogService.insertModerationLog(me, 'clearQueue'); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts index f3e440b4cb..9442bda5eb 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -13,7 +8,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'read:admin:queue', res: { type: 'array', @@ -21,15 +15,16 @@ export const meta = { items: { type: 'array', optional: false, nullable: false, - prefixItems: [ - { - type: 'string', - }, - { - type: 'number', - }, - ], - unevaluatedItems: false, + items: { + anyOf: [ + { + type: 'string', + }, + { + type: 'number', + }, + ], + }, }, example: [[ 'example.com', @@ -44,8 +39,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject('queue:deliver') public deliverQueue: DeliverQueue, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts index e7589cba81..55a3410d49 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -13,7 +8,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'read:admin:queue', res: { type: 'array', @@ -21,15 +15,16 @@ export const meta = { items: { type: 'array', optional: false, nullable: false, - prefixItems: [ - { - type: 'string', - }, - { - type: 'number', - }, - ], - unevaluatedItems: false, + items: { + anyOf: [ + { + type: 'string', + }, + { + type: 'number', + }, + ], + }, }, example: [[ 'example.com', @@ -44,8 +39,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject('queue:inbox') public inboxQueue: InboxQueue, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts deleted file mode 100644 index a68e95bf3f..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'read:admin:queue', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - optional: false, nullable: false, - ref: 'QueueJob', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - queue: { type: 'string', enum: QUEUE_TYPES }, - state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed', 'paused'] } }, - search: { type: 'string' }, - }, - required: ['queue', 'state'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private queueService: QueueService, - ) { - super(meta, paramDef, async (ps, me) => { - return this.queueService.queueGetJobs(ps.queue, ps.state, ps.search); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/queue/promote-jobs.ts b/packages/backend/src/server/api/endpoints/admin/queue/promote-jobs.ts deleted file mode 100644 index d22385e261..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/promote-jobs.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'write:admin:queue', -} as const; - -export const paramDef = { - type: 'object', - properties: { - queue: { type: 'string', enum: QUEUE_TYPES }, - }, - required: ['queue'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private moderationLogService: ModerationLogService, - private queueService: QueueService, - ) { - super(meta, paramDef, async (ps, me) => { - this.queueService.queuePromoteJobs(ps.queue); - - this.moderationLogService.log(me, 'promoteQueue'); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts new file mode 100644 index 0000000000..4e57e6613e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { QueueService } from '@/core/QueueService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + type: { type: 'string', enum: ['deliver', 'inbox'] }, + }, + required: ['type'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + private moderationLogService: ModerationLogService, + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + let delayedQueues; + + switch (ps.type) { + case 'deliver': + delayedQueues = await this.queueService.deliverQueue.getDelayed(); + for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) { + const queue = delayedQueues[queueIndex]; + await queue.promote(); + } + break; + + case 'inbox': + delayedQueues = await this.queueService.inboxQueue.getDelayed(); + for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) { + const queue = delayedQueues[queueIndex]; + await queue.promote(); + } + break; + } + + this.moderationLogService.insertModerationLog(me, 'promoteQueue'); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts deleted file mode 100644 index 0098160165..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'read:admin:queue', - - res: { - type: 'object', - optional: false, nullable: false, - properties: { - name: { - type: 'string', - optional: false, nullable: false, - enum: QUEUE_TYPES, - }, - qualifiedName: { - type: 'string', - optional: false, nullable: false, - }, - counts: { - type: 'object', - optional: false, nullable: false, - additionalProperties: { - type: 'number', - }, - }, - isPaused: { - type: 'boolean', - optional: false, nullable: false, - }, - metrics: { - type: 'object', - optional: false, nullable: false, - properties: { - completed: { - optional: false, nullable: false, - ref: 'QueueMetrics', - }, - failed: { - optional: false, nullable: false, - ref: 'QueueMetrics', - }, - }, - }, - db: { - type: 'object', - optional: false, nullable: false, - properties: { - version: { - type: 'string', - optional: false, nullable: false, - }, - mode: { - type: 'string', - optional: false, nullable: false, - enum: ['cluster', 'standalone', 'sentinel'], - }, - runId: { - type: 'string', - optional: false, nullable: false, - }, - processId: { - type: 'string', - optional: false, nullable: false, - }, - port: { - type: 'number', - optional: false, nullable: false, - }, - os: { - type: 'string', - optional: false, nullable: false, - }, - uptime: { - type: 'number', - optional: false, nullable: false, - }, - memory: { - type: 'object', - optional: false, nullable: false, - properties: { - total: { - type: 'number', - optional: false, nullable: false, - }, - used: { - type: 'number', - optional: false, nullable: false, - }, - fragmentationRatio: { - type: 'number', - optional: false, nullable: false, - }, - peak: { - type: 'number', - optional: false, nullable: false, - }, - }, - }, - clients: { - type: 'object', - optional: false, nullable: false, - properties: { - blocked: { - type: 'number', - optional: false, nullable: false, - }, - connected: { - type: 'number', - optional: false, nullable: false, - }, - }, - }, - }, - } - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - queue: { type: 'string', enum: QUEUE_TYPES }, - }, - required: ['queue'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private queueService: QueueService, - ) { - super(meta, paramDef, async (ps, me) => { - return this.queueService.queueGetQueue(ps.queue); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/queue/queues.ts b/packages/backend/src/server/api/endpoints/admin/queue/queues.ts deleted file mode 100644 index 8d27e38c84..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/queues.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'read:admin:queue', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - name: { - type: 'string', - optional: false, nullable: false, - enum: QUEUE_TYPES, - }, - counts: { - type: 'object', - optional: false, nullable: false, - additionalProperties: { - type: 'number', - }, - }, - isPaused: { - type: 'boolean', - optional: false, nullable: false, - }, - metrics: { - type: 'object', - optional: false, nullable: false, - properties: { - completed: { - optional: false, nullable: false, - ref: 'QueueMetrics', - }, - failed: { - optional: false, nullable: false, - ref: 'QueueMetrics', - }, - }, - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private queueService: QueueService, - ) { - super(meta, paramDef, async (ps, me) => { - return this.queueService.queueGetQueues(); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/queue/remove-job.ts b/packages/backend/src/server/api/endpoints/admin/queue/remove-job.ts deleted file mode 100644 index 2c73f689d0..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/remove-job.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'write:admin:queue', -} as const; - -export const paramDef = { - type: 'object', - properties: { - queue: { type: 'string', enum: QUEUE_TYPES }, - jobId: { type: 'string' }, - }, - required: ['queue', 'jobId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private moderationLogService: ModerationLogService, - private queueService: QueueService, - ) { - super(meta, paramDef, async (ps, me) => { - this.queueService.queueRemoveJob(ps.queue, ps.jobId); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/queue/retry-job.ts b/packages/backend/src/server/api/endpoints/admin/queue/retry-job.ts deleted file mode 100644 index b2603128f8..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/retry-job.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'write:admin:queue', -} as const; - -export const paramDef = { - type: 'object', - properties: { - queue: { type: 'string', enum: QUEUE_TYPES }, - jobId: { type: 'string' }, - }, - required: ['queue', 'jobId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private moderationLogService: ModerationLogService, - private queueService: QueueService, - ) { - super(meta, paramDef, async (ps, me) => { - this.queueService.queueRetryJob(ps.queue, ps.jobId); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts b/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts deleted file mode 100644 index 1735c22674..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'read:admin:queue', - - res: { - optional: false, nullable: false, - ref: 'QueueJob', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - queue: { type: 'string', enum: QUEUE_TYPES }, - jobId: { type: 'string' }, - }, - required: ['queue', 'jobId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private queueService: QueueService, - ) { - super(meta, paramDef, async (ps, me) => { - return this.queueService.queueGetJob(ps.queue, ps.jobId); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index d7f9e4eaa3..7f3732c970 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -1,18 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js'; +import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js'; export const meta = { tags: ['admin'], requireCredential: true, requireModerator: true, - kind: 'read:admin:emoji', res: { type: 'object', @@ -44,8 +38,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, @@ -53,8 +48,7 @@ export default class extends Endpoint { // eslint- @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, - @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, - @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, + @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, ) { super(meta, paramDef, async (ps, me) => { const deliverJobCounts = await this.deliverQueue.getJobCounts(); diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts index 3d7bc4567e..f2d4aa8996 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { URL } from 'node:url'; import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -14,7 +9,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:relays', errors: { invalidUrl: { @@ -60,8 +54,9 @@ export const paramDef = { required: ['inbox'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private relayService: RelayService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/relays/list.ts b/packages/backend/src/server/api/endpoints/admin/relays/list.ts index 587d5c3b03..910c90e78e 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/list.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { RelayService } from '@/core/RelayService.js'; @@ -12,7 +7,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'read:admin:relays', res: { type: 'array', @@ -52,8 +46,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private relayService: RelayService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts index 1f6e773cd4..5e26f61fa7 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { RelayService } from '@/core/RelayService.js'; @@ -12,7 +7,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:relays', } as const; export const paramDef = { @@ -23,8 +17,9 @@ export const paramDef = { required: ['inbox'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private relayService: RelayService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index fc246631c2..e9c3b0e69f 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -1,22 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js'; +import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], requireCredential: true, requireModerator: true, - kind: 'write:admin:reset-password', res: { type: 'object', @@ -40,28 +33,24 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - - private moderationLogService: ModerationLogService, ) { - super(meta, paramDef, async (ps, me) => { + super(meta, paramDef, async (ps) => { const user = await this.usersRepository.findOneBy({ id: ps.userId }); if (user == null) { throw new Error('user not found'); } - if (this.serverSettings.rootUserId === user.id) { + if (user.isRoot) { throw new Error('cannot reset password of root'); } @@ -76,12 +65,6 @@ export default class extends Endpoint { // eslint- password: hash, }); - this.moderationLogService.log(me, 'resetPassword', { - userId: user.id, - userUsername: user.username, - userHost: user.host, - }); - return { password: passwd, }; diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts index 554d324ff2..aead894611 100644 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts @@ -1,56 +1,62 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AbuseUserReportsRepository } from '@/models/_.js'; +import type { UsersRepository, AbuseUserReportsRepository } from '@/models/index.js'; +import { InstanceActorService } from '@/core/InstanceActorService.js'; +import { QueueService } from '@/core/QueueService.js'; +import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '@/server/api/error.js'; -import { AbuseReportService } from '@/core/AbuseReportService.js'; export const meta = { tags: ['admin'], requireCredential: true, requireModerator: true, - kind: 'write:admin:resolve-abuse-user-report', - - errors: { - noSuchAbuseReport: { - message: 'No such abuse report.', - code: 'NO_SUCH_ABUSE_REPORT', - id: 'ac3794dd-2ce4-d878-e546-73c60c06b398', - kind: 'server', - httpStatusCode: 404, - }, - }, } as const; export const paramDef = { type: 'object', properties: { reportId: { type: 'string', format: 'misskey:id' }, - resolvedAs: { type: 'string', enum: ['accept', 'reject', null], nullable: true }, + forward: { type: 'boolean', default: false }, }, required: ['reportId'], } as const; +// TODO: ロジックをサービスに切り出す + +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.abuseUserReportsRepository) private abuseUserReportsRepository: AbuseUserReportsRepository, - private abuseReportService: AbuseReportService, + + private queueService: QueueService, + private instanceActorService: InstanceActorService, + private apRendererService: ApRendererService, ) { super(meta, paramDef, async (ps, me) => { const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId }); - if (!report) { - throw new ApiError(meta.errors.noSuchAbuseReport); + + if (report == null) { + throw new Error('report not found'); } - await this.abuseReportService.resolve([{ reportId: report.id, resolvedAs: ps.resolvedAs ?? null }], me); + if (ps.forward && report.targetUserHost != null) { + const actor = await this.instanceActorService.getInstanceActor(); + const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId }); + + this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false); + } + + await this.abuseUserReportsRepository.update(report.id, { + resolved: true, + assigneeId: me.id, + forwarded: ps.forward && report.targetUserHost != null, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts index b6c7953781..b80aaba122 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository, UsersRepository } from '@/models/_.js'; +import type { RolesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { RoleService } from '@/core/RoleService.js'; @@ -15,7 +10,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:roles', errors: { noSuchRole: { @@ -54,8 +48,9 @@ export const paramDef = { ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -84,7 +79,7 @@ export default class extends Endpoint { // eslint- return; } - await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null, me); + await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts index f92f7ebaeb..916172f54a 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts @@ -1,25 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RolesRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { IdService } from '@/core/IdService.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; -import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin', 'role'], requireCredential: true, requireAdmin: true, - kind: 'write:admin:roles', - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'Role', - }, } as const; export const paramDef = { @@ -36,7 +27,6 @@ export const paramDef = { isAdministrator: { type: 'boolean' }, isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility asBadge: { type: 'boolean' }, - preserveAssignmentOnMoveAccount: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' }, displayOrder: { type: 'number' }, policies: { @@ -60,14 +50,41 @@ export const paramDef = { ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private globalEventService: GlobalEventService, + private idService: IdService, private roleEntityService: RoleEntityService, - private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const created = await this.roleService.create(ps, me); + const date = new Date(); + const created = await this.rolesRepository.insert({ + id: this.idService.genId(), + createdAt: date, + updatedAt: date, + lastUsedAt: date, + name: ps.name, + description: ps.description, + color: ps.color, + iconUrl: ps.iconUrl, + target: ps.target, + condFormula: ps.condFormula, + isPublic: ps.isPublic, + isAdministrator: ps.isAdministrator, + isModerator: ps.isModerator, + isExplorable: ps.isExplorable, + asBadge: ps.asBadge, + canEditMembersByModerator: ps.canEditMembersByModerator, + displayOrder: ps.displayOrder, + policies: ps.policies, + }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); + + this.globalEventService.publishInternalEvent('roleCreated', created); return await this.roleEntityService.pack(created, me); }); diff --git a/packages/backend/src/server/api/endpoints/admin/roles/delete.ts b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts index 638e2b15b9..b56ebdb3ee 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts @@ -1,21 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository } from '@/models/_.js'; +import type { RolesRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; -import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin', 'role'], requireCredential: true, requireAdmin: true, - kind: 'write:admin:roles', errors: { noSuchRole: { @@ -36,20 +30,24 @@ export const paramDef = { ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, - private roleService: RoleService, + private globalEventService: GlobalEventService, ) { - super(meta, paramDef, async (ps, me) => { + super(meta, paramDef, async (ps) => { const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); if (role == null) { throw new ApiError(meta.errors.noSuchRole); } - await this.roleService.delete(role, me); + await this.rolesRepository.delete({ + id: ps.roleId, + }); + this.globalEventService.publishInternalEvent('roleDeleted', role); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/list.ts b/packages/backend/src/server/api/endpoints/admin/roles/list.ts index 333fac6aa6..edaf638ea9 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/list.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository } from '@/models/_.js'; +import type { RolesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; @@ -14,17 +9,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'read:admin:roles', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Role', - }, - }, } as const; export const paramDef = { @@ -35,8 +19,9 @@ export const paramDef = { ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/roles/show.ts b/packages/backend/src/server/api/endpoints/admin/roles/show.ts index 13e5cbb995..01028a086f 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/show.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/show.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository } from '@/models/_.js'; +import type { RolesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; @@ -15,7 +10,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'read:admin:roles', errors: { noSuchRole: { @@ -24,12 +18,6 @@ export const meta = { id: '07dc7d34-c0d8-49b7-96c6-db3ce64ee0b3', }, }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'Role', - }, } as const; export const paramDef = { @@ -42,8 +30,9 @@ export const paramDef = { ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts index e7da3384b1..45c4f76943 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository, UsersRepository } from '@/models/_.js'; +import type { RolesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { RoleService } from '@/core/RoleService.js'; @@ -15,7 +10,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:roles', errors: { noSuchRole: { @@ -56,8 +50,9 @@ export const paramDef = { ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -82,7 +77,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchUser); } - await this.roleService.unassign(user.id, role.id, me); + await this.roleService.unassign(user.id, role.id); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts b/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts index 5cf49670be..5a34eee96c 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts @@ -1,20 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { MetaService } from '@/core/MetaService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin', 'role'], requireCredential: true, requireAdmin: true, - kind: 'write:admin:roles', } as const; export const paramDef = { @@ -29,27 +22,18 @@ export const paramDef = { ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private metaService: MetaService, private globalEventService: GlobalEventService, - private moderationLogService: ModerationLogService, ) { - super(meta, paramDef, async (ps, me) => { - const before = await this.metaService.fetch(true); - + super(meta, paramDef, async (ps) => { await this.metaService.update({ policies: ps.policies, }); - - const after = await this.metaService.fetch(true); - - this.globalEventService.publishInternalEvent('policiesUpdated', after.policies); - this.moderationLogService.log(me, 'updateServerSettings', { - before: before.policies, - after: after.policies, - }); + this.globalEventService.publishInternalEvent('policiesUpdated', ps.policies); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts index 175adcb63f..467f157a61 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -1,21 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository } from '@/models/_.js'; +import type { RolesRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; -import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin', 'role'], requireCredential: true, requireAdmin: true, - kind: 'write:admin:roles', errors: { noSuchRole: { @@ -41,7 +35,6 @@ export const paramDef = { isAdministrator: { type: 'boolean' }, isExplorable: { type: 'boolean' }, asBadge: { type: 'boolean' }, - preserveAssignmentOnMoveAccount: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' }, displayOrder: { type: 'number' }, policies: { @@ -50,24 +43,40 @@ export const paramDef = { }, required: [ 'roleId', + 'name', + 'description', + 'color', + 'iconUrl', + 'target', + 'condFormula', + 'isPublic', + 'isModerator', + 'isAdministrator', + 'asBadge', + 'canEditMembersByModerator', + 'displayOrder', + 'policies', ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, - private roleService: RoleService, + private globalEventService: GlobalEventService, ) { - super(meta, paramDef, async (ps, me) => { + super(meta, paramDef, async (ps) => { const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); if (role == null) { throw new ApiError(meta.errors.noSuchRole); } - await this.roleService.update(role, { + const date = new Date(); + await this.rolesRepository.update(ps.roleId, { + updatedAt: date, name: ps.name, description: ps.description, color: ps.color, @@ -79,11 +88,12 @@ export default class extends Endpoint { // eslint- isAdministrator: ps.isAdministrator, isExplorable: ps.isExplorable, asBadge: ps.asBadge, - preserveAssignmentOnMoveAccount: ps.preserveAssignmentOnMoveAccount, canEditMembersByModerator: ps.canEditMembersByModerator, displayOrder: ps.displayOrder, policies: ps.policies, - }, me); + }); + const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId }); + this.globalEventService.publishInternalEvent('roleUpdated', updated); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts index 198166bec2..35edca5460 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -1,24 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; -import type { RoleAssignmentsRepository, RolesRepository } from '@/models/_.js'; +import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { IdService } from '@/core/IdService.js'; import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin', 'role', 'users'], requireCredential: false, - requireModerator: true, - kind: 'read:admin:roles', + requireAdmin: true, errors: { noSuchRole: { @@ -27,20 +20,6 @@ export const meta = { id: '224eff5e-2488-4b18-b3e7-f50d94421648', }, }, - - res: { - type: 'array', - items: { - type: 'object', - properties: { - id: { type: 'string', format: 'misskey:id' }, - createdAt: { type: 'string', format: 'date-time' }, - user: { ref: 'UserDetailed' }, - expiresAt: { type: 'string', format: 'date-time', nullable: true }, - }, - required: ['id', 'createdAt', 'user'], - }, - }, } as const; export const paramDef = { @@ -54,8 +33,9 @@ export const paramDef = { required: ['roleId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, @@ -65,7 +45,6 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, private userEntityService: UserEntityService, - private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const role = await this.rolesRepository.findOneBy({ @@ -78,25 +57,21 @@ export default class extends Endpoint { // eslint- const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) .andWhere('assign.roleId = :roleId', { roleId: role.id }) - .andWhere(new Brackets(qb => { - qb - .where('assign.expiresAt IS NULL') - .orWhere('assign.expiresAt > :now', { now: new Date() }); + .andWhere(new Brackets(qb => { qb + .where('assign.expiresAt IS NULL') + .orWhere('assign.expiresAt > :now', { now: new Date() }); })) .innerJoinAndSelect('assign.user', 'user'); const assigns = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); - const _users = assigns.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' }) - .then(users => new Map(users.map(u => [u.id, u]))); return await Promise.all(assigns.map(async assign => ({ id: assign.id, - createdAt: this.idService.parse(assign.id).date.toISOString(), - user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }), - expiresAt: assign.expiresAt?.toISOString() ?? null, + createdAt: assign.createdAt, + user: await this.userEntityService.pack(assign.user!, me, { detail: true }), + expiresAt: assign.expiresAt, }))); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/send-email.ts b/packages/backend/src/server/api/endpoints/admin/send-email.ts index f01a7778a8..5ddc62f476 100644 --- a/packages/backend/src/server/api/endpoints/admin/send-email.ts +++ b/packages/backend/src/server/api/endpoints/admin/send-email.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { EmailService } from '@/core/EmailService.js'; @@ -12,7 +7,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:send-email', } as const; export const paramDef = { @@ -25,8 +19,9 @@ export const paramDef = { required: ['to', 'subject', 'text'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private emailService: EmailService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/server-info.ts b/packages/backend/src/server/api/endpoints/admin/server-info.ts index 80b6a4d32e..4ef4fdc665 100644 --- a/packages/backend/src/server/api/endpoints/admin/server-info.ts +++ b/packages/backend/src/server/api/endpoints/admin/server-info.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as os from 'node:os'; import si from 'systeminformation'; import { Inject, Injectable } from '@nestjs/common'; @@ -14,7 +9,6 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, requireModerator: true, - kind: 'read:admin:server-info', tags: ['admin', 'meta'], @@ -101,8 +95,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.db) private db: DataSource, diff --git a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts index 58c5f1f60a..24335a21cc 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ModerationLogsRepository } from '@/models/_.js'; +import type { ModerationLogsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { ModerationLogEntityService } from '@/core/entities/ModerationLogEntityService.js'; @@ -14,8 +9,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireAdmin: true, - kind: 'read:admin:show-moderation-log', + requireModerator: true, res: { type: 'array', @@ -50,7 +44,7 @@ export const meta = { user: { type: 'object', optional: false, nullable: false, - ref: 'UserDetailedNotMe', + ref: 'UserDetailed', }, }, }, @@ -63,14 +57,13 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, - type: { type: 'string', nullable: true }, - userId: { type: 'string', format: 'misskey:id', nullable: true }, }, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.moderationLogsRepository) private moderationLogsRepository: ModerationLogsRepository, @@ -81,15 +74,7 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.moderationLogsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId); - if (ps.type != null) { - query.andWhere('report.type = :type', { type: ps.type }); - } - - if (ps.userId != null) { - query.andWhere('report.userId = :userId', { userId: ps.userId }); - } - - const reports = await query.limit(ps.limit).getMany(); + const reports = await query.take(ps.limit).getMany(); return await this.moderationLogEntityService.packMany(reports); }); diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 1ba6853dbe..f49d2a0966 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -1,183 +1,19 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, SigninsRepository, UserProfilesRepository } from '@/models/_.js'; +import type { UsersRepository, SigninsRepository, UserProfilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; -import { IdService } from '@/core/IdService.js'; -import { notificationRecieveConfig } from '@/models/json-schema/user.js'; export const meta = { tags: ['admin'], requireCredential: true, requireModerator: true, - kind: 'read:admin:show-user', res: { type: 'object', nullable: false, optional: false, - properties: { - email: { - type: 'string', - optional: false, nullable: true, - }, - emailVerified: { - type: 'boolean', - optional: false, nullable: false, - }, - followedMessage: { - type: 'string', - optional: false, nullable: true, - }, - autoAcceptFollowed: { - type: 'boolean', - optional: false, nullable: false, - }, - noCrawle: { - type: 'boolean', - optional: false, nullable: false, - }, - preventAiLearning: { - type: 'boolean', - optional: false, nullable: false, - }, - alwaysMarkNsfw: { - type: 'boolean', - optional: false, nullable: false, - }, - autoSensitive: { - type: 'boolean', - optional: false, nullable: false, - }, - carefulBot: { - type: 'boolean', - optional: false, nullable: false, - }, - injectFeaturedNote: { - type: 'boolean', - optional: false, nullable: false, - }, - receiveAnnouncementEmail: { - type: 'boolean', - optional: false, nullable: false, - }, - mutedWords: { - type: 'array', - optional: false, nullable: false, - items: { - anyOf: [ - { - type: 'string', - }, - { - type: 'array', - items: { - type: 'string', - }, - }, - ], - }, - }, - mutedInstances: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - }, - }, - notificationRecieveConfig: { - type: 'object', - optional: false, nullable: false, - properties: { - note: { optional: true, ...notificationRecieveConfig }, - follow: { optional: true, ...notificationRecieveConfig }, - mention: { optional: true, ...notificationRecieveConfig }, - reply: { optional: true, ...notificationRecieveConfig }, - renote: { optional: true, ...notificationRecieveConfig }, - quote: { optional: true, ...notificationRecieveConfig }, - reaction: { optional: true, ...notificationRecieveConfig }, - pollEnded: { optional: true, ...notificationRecieveConfig }, - receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, - followRequestAccepted: { optional: true, ...notificationRecieveConfig }, - roleAssigned: { optional: true, ...notificationRecieveConfig }, - chatRoomInvitationReceived: { optional: true, ...notificationRecieveConfig }, - achievementEarned: { optional: true, ...notificationRecieveConfig }, - app: { optional: true, ...notificationRecieveConfig }, - test: { optional: true, ...notificationRecieveConfig }, - }, - }, - isModerator: { - type: 'boolean', - optional: false, nullable: false, - }, - isSilenced: { - type: 'boolean', - optional: false, nullable: false, - }, - isSuspended: { - type: 'boolean', - optional: false, nullable: false, - }, - isHibernated: { - type: 'boolean', - optional: false, nullable: false, - }, - lastActiveDate: { - type: 'string', - optional: false, nullable: true, - }, - moderationNote: { - type: 'string', - optional: false, nullable: false, - }, - signins: { - type: 'array', - optional: false, nullable: false, - items: { - ref: 'Signin', - }, - }, - policies: { - type: 'object', - optional: false, nullable: false, - ref: 'RolePolicies', - }, - roles: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - ref: 'Role', - }, - }, - roleAssigns: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - properties: { - createdAt: { - type: 'string', - optional: false, nullable: false, - }, - expiresAt: { - type: 'string', - optional: false, nullable: true, - }, - roleId: { - type: 'string', - optional: false, nullable: false, - }, - }, - }, - }, - }, }, } as const; @@ -189,8 +25,9 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -203,7 +40,6 @@ export default class extends Endpoint { // eslint- private roleService: RoleService, private roleEntityService: RoleEntityService, - private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const [user, profile] = await Promise.all([ @@ -225,13 +61,11 @@ export default class extends Endpoint { // eslint- const signins = await this.signinsRepository.findBy({ userId: user.id }); - const roleAssigns = await this.roleService.getUserAssigns(user.id); const roles = await this.roleService.getUserRoles(user.id); return { email: profile.email, emailVerified: profile.emailVerified, - followedMessage: profile.followedMessage, autoAcceptFollowed: profile.autoAcceptFollowed, noCrawle: profile.noCrawle, preventAiLearning: profile.preventAiLearning, @@ -242,21 +76,15 @@ export default class extends Endpoint { // eslint- receiveAnnouncementEmail: profile.receiveAnnouncementEmail, mutedWords: profile.mutedWords, mutedInstances: profile.mutedInstances, - notificationRecieveConfig: profile.notificationRecieveConfig, + mutingNotificationTypes: profile.mutingNotificationTypes, isModerator: isModerator, isSilenced: isSilenced, isSuspended: user.isSuspended, - isHibernated: user.isHibernated, - lastActiveDate: user.lastActiveDate ? user.lastActiveDate.toISOString() : null, + lastActiveDate: user.lastActiveDate, moderationNote: profile.moderationNote ?? '', signins, policies: await this.roleService.getUserPolicies(user.id), roles: await this.roleEntityService.packMany(roles, me), - roleAssigns: roleAssigns.map(a => ({ - createdAt: this.idService.parse(a.id).date.toISOString(), - expiresAt: a.expiresAt ? a.expiresAt.toISOString() : null, - roleId: a.roleId, - })), }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 2b2c8c60ab..426973f282 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/_.js'; +import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -16,7 +11,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'read:admin:show-user', res: { type: 'array', @@ -48,8 +42,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -71,13 +66,13 @@ export default class extends Endpoint { // eslint- break; } case 'moderator': { - const moderatorIds = await this.roleService.getModeratorIds({ includeAdmins: false }); + const moderatorIds = await this.roleService.getModeratorIds(false); if (moderatorIds.length === 0) return []; query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds }); break; } case 'adminOrModerator': { - const adminOrModeratorIds = await this.roleService.getModeratorIds({ includeAdmins: true }); + const adminOrModeratorIds = await this.roleService.getModeratorIds(); if (adminOrModeratorIds.length === 0) return []; query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds }); break; @@ -100,8 +95,8 @@ export default class extends Endpoint { // eslint- switch (ps.sort) { case '+follower': query.orderBy('user.followersCount', 'DESC'); break; case '-follower': query.orderBy('user.followersCount', 'ASC'); break; - case '+createdAt': query.orderBy('user.id', 'DESC'); break; - case '-createdAt': query.orderBy('user.id', 'ASC'); break; + case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; case '+updatedAt': query.orderBy('user.updatedAt', 'DESC', 'NULLS LAST'); break; case '-updatedAt': query.orderBy('user.updatedAt', 'ASC', 'NULLS FIRST'); break; case '+lastActiveDate': query.orderBy('user.lastActiveDate', 'DESC', 'NULLS LAST'); break; @@ -109,12 +104,12 @@ export default class extends Endpoint { // eslint- default: query.orderBy('user.id', 'ASC'); break; } - query.limit(ps.limit); - query.offset(ps.offset); + query.take(ps.limit); + query.skip(ps.offset); const users = await query.getMany(); - return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' }); + return await this.userEntityService.packMany(users, me, { detail: true }); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index bea1bdc4ed..eabbceac0e 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -1,21 +1,21 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - +import { IsNull, Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/_.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import type { RelationshipJobData } from '@/queue/types.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { tags: ['admin'], requireCredential: true, requireModerator: true, - kind: 'write:admin:suspend-user', } as const; export const paramDef = { @@ -26,14 +26,20 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + private userSuspendService: UserSuspendService, private roleService: RoleService, + private moderationLogService: ModerationLogService, + private queueService: QueueService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy({ id: ps.userId }); @@ -46,7 +52,40 @@ export default class extends Endpoint { // eslint- throw new Error('cannot suspend moderator account'); } - await this.userSuspendService.suspend(user, me); + await this.usersRepository.update(user.id, { + isSuspended: true, + }); + + this.moderationLogService.insertModerationLog(me, 'suspend', { + targetId: user.id, + }); + + (async () => { + await this.userSuspendService.doPostSuspend(user).catch(e => {}); + await this.unFollowAll(user).catch(e => {}); + })(); }); } + + @bindThis + private async unFollowAll(follower: User) { + const followings = await this.followingsRepository.find({ + where: { + followerId: follower.id, + followeeId: Not(IsNull()), + }, + }); + + const jobs: RelationshipJobData[] = []; + for (const following of followings) { + if (following.followeeId && following.followerId) { + jobs.push({ + from: { id: following.followerId }, + to: { id: following.followeeId }, + silent: true, + }); + } + } + this.queueService.createUnfollowJob(jobs); + } } diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts deleted file mode 100644 index 28071e7a33..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; -import { systemWebhookEventTypes } from '@/models/SystemWebhook.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; - -export const meta = { - tags: ['admin', 'system-webhook'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:system-webhook', - - res: { - type: 'object', - ref: 'SystemWebhook', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - isActive: { - type: 'boolean', - }, - name: { - type: 'string', - minLength: 1, - maxLength: 255, - }, - on: { - type: 'array', - items: { - type: 'string', - enum: systemWebhookEventTypes, - }, - }, - url: { - type: 'string', - minLength: 1, - maxLength: 1024, - }, - secret: { - type: 'string', - minLength: 1, - maxLength: 1024, - }, - }, - required: [ - 'isActive', - 'name', - 'on', - 'url', - 'secret', - ], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private systemWebhookService: SystemWebhookService, - private systemWebhookEntityService: SystemWebhookEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const result = await this.systemWebhookService.createSystemWebhook( - { - isActive: ps.isActive, - name: ps.name, - on: ps.on, - url: ps.url, - secret: ps.secret, - }, - me, - ); - - return this.systemWebhookEntityService.pack(result); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts deleted file mode 100644 index 9cdfc7e70f..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; - -export const meta = { - tags: ['admin', 'system-webhook'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:system-webhook', -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - }, - required: [ - 'id', - ], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private systemWebhookService: SystemWebhookService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.systemWebhookService.deleteSystemWebhook( - ps.id, - me, - ); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts deleted file mode 100644 index 7a440a774e..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; -import { systemWebhookEventTypes } from '@/models/SystemWebhook.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; - -export const meta = { - tags: ['admin', 'system-webhook'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:system-webhook', - - res: { - type: 'array', - items: { - type: 'object', - ref: 'SystemWebhook', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - isActive: { - type: 'boolean', - }, - on: { - type: 'array', - items: { - type: 'string', - enum: systemWebhookEventTypes, - }, - }, - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private systemWebhookService: SystemWebhookService, - private systemWebhookEntityService: SystemWebhookEntityService, - ) { - super(meta, paramDef, async (ps) => { - const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ - isActive: ps.isActive, - on: ps.on, - }); - return this.systemWebhookEntityService.packMany(webhooks); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts deleted file mode 100644 index 75862c96a7..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; -import { ApiError } from '@/server/api/error.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; - -export const meta = { - tags: ['admin', 'system-webhook'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:system-webhook', - - res: { - type: 'object', - ref: 'SystemWebhook', - }, - - errors: { - noSuchSystemWebhook: { - message: 'No such SystemWebhook.', - code: 'NO_SUCH_SYSTEM_WEBHOOK', - id: '38dd1ffe-04b4-6ff5-d8ba-4e6a6ae22c9d', - kind: 'server', - httpStatusCode: 404, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - }, - required: ['id'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private systemWebhookService: SystemWebhookService, - private systemWebhookEntityService: SystemWebhookEntityService, - ) { - super(meta, paramDef, async (ps) => { - const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [ps.id] }); - if (webhooks.length === 0) { - throw new ApiError(meta.errors.noSuchSystemWebhook); - } - - return this.systemWebhookEntityService.pack(webhooks[0]); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts deleted file mode 100644 index fb2ddf4b44..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { WebhookTestService } from '@/core/WebhookTestService.js'; -import { ApiError } from '@/server/api/error.js'; -import { systemWebhookEventTypes } from '@/models/SystemWebhook.js'; - -export const meta = { - tags: ['webhooks'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'read:admin:system-webhook', - - limit: { - duration: ms('15min'), - max: 60, - }, - - errors: { - noSuchWebhook: { - message: 'No such webhook.', - code: 'NO_SUCH_WEBHOOK', - id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - webhookId: { - type: 'string', - format: 'misskey:id', - }, - type: { - type: 'string', - enum: systemWebhookEventTypes, - }, - override: { - type: 'object', - properties: { - url: { type: 'string', nullable: false }, - secret: { type: 'string', nullable: false }, - }, - }, - }, - required: ['webhookId', 'type'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private webhookTestService: WebhookTestService, - ) { - super(meta, paramDef, async (ps) => { - try { - await this.webhookTestService.testSystemWebhook({ - webhookId: ps.webhookId, - type: ps.type, - override: ps.override, - }); - } catch (e) { - if (e instanceof WebhookTestService.NoSuchWebhookError) { - throw new ApiError(meta.errors.noSuchWebhook); - } - throw e; - } - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts deleted file mode 100644 index 8d68bb8f87..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; -import { systemWebhookEventTypes } from '@/models/SystemWebhook.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; - -export const meta = { - tags: ['admin', 'system-webhook'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:system-webhook', - - res: { - type: 'object', - ref: 'SystemWebhook', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - isActive: { - type: 'boolean', - }, - name: { - type: 'string', - minLength: 1, - maxLength: 255, - }, - on: { - type: 'array', - items: { - type: 'string', - enum: systemWebhookEventTypes, - }, - }, - url: { - type: 'string', - minLength: 1, - maxLength: 1024, - }, - secret: { - type: 'string', - minLength: 1, - maxLength: 1024, - }, - }, - required: [ - 'id', - 'isActive', - 'name', - 'on', - 'url', - 'secret', - ], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private systemWebhookService: SystemWebhookService, - private systemWebhookEntityService: SystemWebhookEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const result = await this.systemWebhookService.updateSystemWebhook( - { - id: ps.id, - isActive: ps.isActive, - name: ps.name, - on: ps.on, - url: ps.url, - secret: ps.secret, - }, - me, - ); - - return this.systemWebhookEntityService.pack(result); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts b/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts deleted file mode 100644 index ddab6f3a9d..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/_.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'write:admin:unset-user-avatar', -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private moderationLogService: ModerationLogService, - ) { - super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - if (user.avatarId == null) return; - - await this.usersRepository.update(user.id, { - avatar: null, - avatarId: null, - avatarUrl: null, - avatarBlurhash: null, - }); - - this.moderationLogService.log(me, 'unsetUserAvatar', { - userId: user.id, - userUsername: user.username, - userHost: user.host, - fileId: user.avatarId, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts b/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts deleted file mode 100644 index e16dad719c..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/_.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'write:admin:unset-user-banner', -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private moderationLogService: ModerationLogService, - ) { - super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - if (user.bannerId == null) return; - - await this.usersRepository.update(user.id, { - banner: null, - bannerId: null, - bannerUrl: null, - bannerBlurhash: null, - }); - - this.moderationLogService.log(me, 'unsetUserBanner', { - userId: user.id, - userUsername: user.username, - userHost: user.host, - fileId: user.bannerId, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts index b52c638cdb..2805c21a74 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts @@ -1,11 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/_.js'; +import type { UsersRepository } from '@/models/index.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; import { DI } from '@/di-symbols.js'; @@ -14,7 +10,6 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:unsuspend-user', } as const; export const paramDef = { @@ -25,13 +20,15 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, private userSuspendService: UserSuspendService, + private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy({ id: ps.userId }); @@ -40,7 +37,15 @@ export default class extends Endpoint { // eslint- throw new Error('user not found'); } - await this.userSuspendService.unsuspend(user, me); + await this.usersRepository.update(user.id, { + isSuspended: false, + }); + + this.moderationLogService.insertModerationLog(me, 'unsuspend', { + targetId: user.id, + }); + + this.userSuspendService.doPostUnsuspend(user); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts deleted file mode 100644 index 73d4b843f0..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AbuseUserReportsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '@/server/api/error.js'; -import { AbuseReportService } from '@/core/AbuseReportService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'write:admin:resolve-abuse-user-report', - - errors: { - noSuchAbuseReport: { - message: 'No such abuse report.', - code: 'NO_SUCH_ABUSE_REPORT', - id: '15f51cf5-46d1-4b1d-a618-b35bcbed0662', - kind: 'server', - httpStatusCode: 404, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - reportId: { type: 'string', format: 'misskey:id' }, - moderationNote: { type: 'string' }, - }, - required: ['reportId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.abuseUserReportsRepository) - private abuseUserReportsRepository: AbuseUserReportsRepository, - private abuseReportService: AbuseReportService, - ) { - super(meta, paramDef, async (ps, me) => { - const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId }); - if (!report) { - throw new ApiError(meta.errors.noSuchAbuseReport); - } - - await this.abuseReportService.update(report.id, { - moderationNote: ps.moderationNote, - }, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 578aa2b662..1de5e9efd3 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -1,12 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import type { MiMeta } from '@/models/Meta.js'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import type { Meta } from '@/models/entities/Meta.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; export const meta = { @@ -14,43 +11,24 @@ export const meta = { requireCredential: true, requireAdmin: true, - kind: 'write:admin:meta', } as const; export const paramDef = { type: 'object', properties: { disableRegistration: { type: 'boolean', nullable: true }, - pinnedUsers: { - type: 'array', nullable: true, items: { - type: 'string', - }, - }, - hiddenTags: { - type: 'array', nullable: true, items: { - type: 'string', - }, - }, - blockedHosts: { - type: 'array', nullable: true, items: { - type: 'string', - }, - }, - sensitiveWords: { - type: 'array', nullable: true, items: { - type: 'string', - }, - }, - prohibitedWords: { - type: 'array', nullable: true, items: { - type: 'string', - }, - }, - prohibitedWordsForNameOfUser: { - type: 'array', nullable: true, items: { - type: 'string', - }, - }, + pinnedUsers: { type: 'array', nullable: true, items: { + type: 'string', + } }, + hiddenTags: { type: 'array', nullable: true, items: { + type: 'string', + } }, + blockedHosts: { type: 'array', nullable: true, items: { + type: 'string', + } }, + sensitiveWords: { type: 'array', nullable: true, items: { + type: 'string', + } }, themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, mascotImageUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true }, @@ -58,44 +36,34 @@ export const paramDef = { infoImageUrl: { type: 'string', nullable: true }, notFoundImageUrl: { type: 'string', nullable: true }, iconUrl: { type: 'string', nullable: true }, - app192IconUrl: { type: 'string', nullable: true }, - app512IconUrl: { type: 'string', nullable: true }, backgroundImageUrl: { type: 'string', nullable: true }, logoImageUrl: { type: 'string', nullable: true }, name: { type: 'string', nullable: true }, - shortName: { type: 'string', nullable: true }, description: { type: 'string', nullable: true }, defaultLightTheme: { type: 'string', nullable: true }, defaultDarkTheme: { type: 'string', nullable: true }, cacheRemoteFiles: { type: 'boolean' }, - cacheRemoteSensitiveFiles: { type: 'boolean' }, emailRequiredForSignup: { type: 'boolean' }, enableHcaptcha: { type: 'boolean' }, hcaptchaSiteKey: { type: 'string', nullable: true }, hcaptchaSecretKey: { type: 'string', nullable: true }, - enableMcaptcha: { type: 'boolean' }, - mcaptchaSiteKey: { type: 'string', nullable: true }, - mcaptchaInstanceUrl: { type: 'string', nullable: true }, - mcaptchaSecretKey: { type: 'string', nullable: true }, enableRecaptcha: { type: 'boolean' }, recaptchaSiteKey: { type: 'string', nullable: true }, recaptchaSecretKey: { type: 'string', nullable: true }, enableTurnstile: { type: 'boolean' }, turnstileSiteKey: { type: 'string', nullable: true }, turnstileSecretKey: { type: 'string', nullable: true }, - enableTestcaptcha: { type: 'boolean' }, - googleAnalyticsMeasurementId: { type: 'string', nullable: true }, sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] }, sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, setSensitiveFlagAutomatically: { type: 'boolean' }, enableSensitiveMediaDetectionForVideos: { type: 'boolean' }, + proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true }, maintainerName: { type: 'string', nullable: true }, maintainerEmail: { type: 'string', nullable: true }, - langs: { - type: 'array', items: { - type: 'string', - }, - }, + langs: { type: 'array', items: { + type: 'string', + } }, + summalyProxy: { type: 'string', nullable: true }, deeplAuthKey: { type: 'string', nullable: true }, deeplIsPro: { type: 'boolean' }, enableEmail: { type: 'boolean' }, @@ -109,15 +77,12 @@ export const paramDef = { swPublicKey: { type: 'string', nullable: true }, swPrivateKey: { type: 'string', nullable: true }, tosUrl: { type: 'string', nullable: true }, - repositoryUrl: { type: 'string', nullable: true }, - feedbackUrl: { type: 'string', nullable: true }, - impressumUrl: { type: 'string', nullable: true }, - privacyPolicyUrl: { type: 'string', nullable: true }, - inquiryUrl: { type: 'string', nullable: true }, + repositoryUrl: { type: 'string' }, + feedbackUrl: { type: 'string' }, useObjectStorage: { type: 'boolean' }, objectStorageBaseUrl: { type: 'string', nullable: true }, objectStorageBucket: { type: 'string', nullable: true }, - objectStoragePrefix: { type: 'string', pattern: /^[a-zA-Z0-9-._]*$/.source, nullable: true }, + objectStoragePrefix: { type: 'string', nullable: true }, objectStorageEndpoint: { type: 'string', nullable: true }, objectStorageRegion: { type: 'string', nullable: true }, objectStoragePort: { type: 'integer', nullable: true }, @@ -129,94 +94,26 @@ export const paramDef = { objectStorageS3ForcePathStyle: { type: 'boolean' }, enableIpLogging: { type: 'boolean' }, enableActiveEmailValidation: { type: 'boolean' }, - enableVerifymailApi: { type: 'boolean' }, - verifymailAuthKey: { type: 'string', nullable: true }, - enableTruemailApi: { type: 'boolean' }, - truemailInstance: { type: 'string', nullable: true }, - truemailAuthKey: { type: 'string', nullable: true }, enableChartsForRemoteUser: { type: 'boolean' }, enableChartsForFederatedInstances: { type: 'boolean' }, - enableStatsForFederatedInstances: { type: 'boolean' }, - enableServerMachineStats: { type: 'boolean' }, - enableIdenticonGeneration: { type: 'boolean' }, serverRules: { type: 'array', items: { type: 'string' } }, - bannedEmailDomains: { type: 'array', items: { type: 'string' } }, preservedUsernames: { type: 'array', items: { type: 'string' } }, - manifestJsonOverride: { type: 'string' }, - enableFanoutTimeline: { type: 'boolean' }, - enableFanoutTimelineDbFallback: { type: 'boolean' }, - perLocalUserUserTimelineCacheMax: { type: 'integer' }, - perRemoteUserUserTimelineCacheMax: { type: 'integer' }, - perUserHomeTimelineCacheMax: { type: 'integer' }, - perUserListTimelineCacheMax: { type: 'integer' }, - enableReactionsBuffering: { type: 'boolean' }, - notesPerOneAd: { type: 'integer' }, - silencedHosts: { - type: 'array', - nullable: true, - items: { - type: 'string', - }, - }, - mediaSilencedHosts: { - type: 'array', - nullable: true, - items: { - type: 'string', - }, - }, - summalyProxy: { - type: 'string', nullable: true, - description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.', - }, - urlPreviewEnabled: { type: 'boolean' }, - urlPreviewAllowRedirect: { type: 'boolean' }, - urlPreviewTimeout: { type: 'integer' }, - urlPreviewMaximumContentLength: { type: 'integer' }, - urlPreviewRequireContentLength: { type: 'boolean' }, - urlPreviewUserAgent: { type: 'string', nullable: true }, - urlPreviewSummaryProxyUrl: { type: 'string', nullable: true }, - federation: { - type: 'string', - enum: ['all', 'none', 'specified'], - }, - federationHosts: { - type: 'array', - items: { - type: 'string', - }, - }, - deliverSuspendedSoftware: { - type: 'array', - items: { - type: 'object', - properties: { - software: { type: 'string' }, - versionRange: { type: 'string' }, - }, - required: ['software', 'versionRange'], - }, - }, - singleUserMode: { type: 'boolean' }, - ugcVisibilityForVisitor: { - type: 'string', - enum: ['all', 'local', 'none'], - }, - proxyRemoteFiles: { type: 'boolean' }, - signToActivityPubGet: { type: 'boolean' }, - allowExternalApRedirect: { type: 'boolean' }, }, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.db) + private db: DataSource, + private metaService: MetaService, private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { - const set = {} as Partial; + const set = {} as Partial; if (typeof ps.disableRegistration === 'boolean') { set.disableRegistration = ps.disableRegistration; @@ -237,28 +134,7 @@ export default class extends Endpoint { // eslint- if (Array.isArray(ps.sensitiveWords)) { set.sensitiveWords = ps.sensitiveWords.filter(Boolean); } - if (Array.isArray(ps.prohibitedWords)) { - set.prohibitedWords = ps.prohibitedWords.filter(Boolean); - } - if (Array.isArray(ps.prohibitedWordsForNameOfUser)) { - set.prohibitedWordsForNameOfUser = ps.prohibitedWordsForNameOfUser.filter(Boolean); - } - if (Array.isArray(ps.silencedHosts)) { - let lastValue = ''; - set.silencedHosts = ps.silencedHosts.sort().filter((h) => { - const lv = lastValue; - lastValue = h; - return h !== '' && h !== lv && !set.blockedHosts?.includes(h); - }); - } - if (Array.isArray(ps.mediaSilencedHosts)) { - let lastValue = ''; - set.mediaSilencedHosts = ps.mediaSilencedHosts.sort().filter((h) => { - const lv = lastValue; - lastValue = h; - return h !== '' && h !== lv && !set.blockedHosts?.includes(h); - }); - } + if (ps.themeColor !== undefined) { set.themeColor = ps.themeColor; } @@ -275,14 +151,6 @@ export default class extends Endpoint { // eslint- set.iconUrl = ps.iconUrl; } - if (ps.app192IconUrl !== undefined) { - set.app192IconUrl = ps.app192IconUrl; - } - - if (ps.app512IconUrl !== undefined) { - set.app512IconUrl = ps.app512IconUrl; - } - if (ps.serverErrorImageUrl !== undefined) { set.serverErrorImageUrl = ps.serverErrorImageUrl; } @@ -307,10 +175,6 @@ export default class extends Endpoint { // eslint- set.name = ps.name; } - if (ps.shortName !== undefined) { - set.shortName = ps.shortName; - } - if (ps.description !== undefined) { set.description = ps.description; } @@ -327,10 +191,6 @@ export default class extends Endpoint { // eslint- set.cacheRemoteFiles = ps.cacheRemoteFiles; } - if (ps.cacheRemoteSensitiveFiles !== undefined) { - set.cacheRemoteSensitiveFiles = ps.cacheRemoteSensitiveFiles; - } - if (ps.emailRequiredForSignup !== undefined) { set.emailRequiredForSignup = ps.emailRequiredForSignup; } @@ -347,22 +207,6 @@ export default class extends Endpoint { // eslint- set.hcaptchaSecretKey = ps.hcaptchaSecretKey; } - if (ps.enableMcaptcha !== undefined) { - set.enableMcaptcha = ps.enableMcaptcha; - } - - if (ps.mcaptchaSiteKey !== undefined) { - set.mcaptchaSitekey = ps.mcaptchaSiteKey; - } - - if (ps.mcaptchaInstanceUrl !== undefined) { - set.mcaptchaInstanceUrl = ps.mcaptchaInstanceUrl; - } - - if (ps.mcaptchaSecretKey !== undefined) { - set.mcaptchaSecretKey = ps.mcaptchaSecretKey; - } - if (ps.enableRecaptcha !== undefined) { set.enableRecaptcha = ps.enableRecaptcha; } @@ -387,16 +231,6 @@ export default class extends Endpoint { // eslint- set.turnstileSecretKey = ps.turnstileSecretKey; } - if (ps.enableTestcaptcha !== undefined) { - set.enableTestcaptcha = ps.enableTestcaptcha; - } - - if (ps.googleAnalyticsMeasurementId !== undefined) { - // 空文字列をnullにしたいので??は使わない - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - set.googleAnalyticsMeasurementId = ps.googleAnalyticsMeasurementId || null; - } - if (ps.sensitiveMediaDetection !== undefined) { set.sensitiveMediaDetection = ps.sensitiveMediaDetection; } @@ -413,6 +247,10 @@ export default class extends Endpoint { // eslint- set.enableSensitiveMediaDetectionForVideos = ps.enableSensitiveMediaDetectionForVideos; } + if (ps.proxyAccountId !== undefined) { + set.proxyAccountId = ps.proxyAccountId; + } + if (ps.maintainerName !== undefined) { set.maintainerName = ps.maintainerName; } @@ -425,6 +263,10 @@ export default class extends Endpoint { // eslint- set.langs = ps.langs.filter(Boolean); } + if (ps.summalyProxy !== undefined) { + set.summalyProxy = ps.summalyProxy; + } + if (ps.enableEmail !== undefined) { set.enableEmail = ps.enableEmail; } @@ -470,25 +312,13 @@ export default class extends Endpoint { // eslint- } if (ps.repositoryUrl !== undefined) { - set.repositoryUrl = URL.canParse(ps.repositoryUrl!) ? ps.repositoryUrl : null; + set.repositoryUrl = ps.repositoryUrl; } if (ps.feedbackUrl !== undefined) { set.feedbackUrl = ps.feedbackUrl; } - if (ps.impressumUrl !== undefined) { - set.impressumUrl = ps.impressumUrl; - } - - if (ps.privacyPolicyUrl !== undefined) { - set.privacyPolicyUrl = ps.privacyPolicyUrl; - } - - if (ps.inquiryUrl !== undefined) { - set.inquiryUrl = ps.inquiryUrl; - } - if (ps.useObjectStorage !== undefined) { set.useObjectStorage = ps.useObjectStorage; } @@ -561,38 +391,6 @@ export default class extends Endpoint { // eslint- set.enableActiveEmailValidation = ps.enableActiveEmailValidation; } - if (ps.enableVerifymailApi !== undefined) { - set.enableVerifymailApi = ps.enableVerifymailApi; - } - - if (ps.verifymailAuthKey !== undefined) { - if (ps.verifymailAuthKey === '') { - set.verifymailAuthKey = null; - } else { - set.verifymailAuthKey = ps.verifymailAuthKey; - } - } - - if (ps.enableTruemailApi !== undefined) { - set.enableTruemailApi = ps.enableTruemailApi; - } - - if (ps.truemailInstance !== undefined) { - if (ps.truemailInstance === '') { - set.truemailInstance = null; - } else { - set.truemailInstance = ps.truemailInstance; - } - } - - if (ps.truemailAuthKey !== undefined) { - if (ps.truemailAuthKey === '') { - set.truemailAuthKey = null; - } else { - set.truemailAuthKey = ps.truemailAuthKey; - } - } - if (ps.enableChartsForRemoteUser !== undefined) { set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser; } @@ -601,18 +399,6 @@ export default class extends Endpoint { // eslint- set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances; } - if (ps.enableStatsForFederatedInstances !== undefined) { - set.enableStatsForFederatedInstances = ps.enableStatsForFederatedInstances; - } - - if (ps.enableServerMachineStats !== undefined) { - set.enableServerMachineStats = ps.enableServerMachineStats; - } - - if (ps.enableIdenticonGeneration !== undefined) { - set.enableIdenticonGeneration = ps.enableIdenticonGeneration; - } - if (ps.serverRules !== undefined) { set.serverRules = ps.serverRules; } @@ -621,118 +407,8 @@ export default class extends Endpoint { // eslint- set.preservedUsernames = ps.preservedUsernames; } - if (ps.manifestJsonOverride !== undefined) { - set.manifestJsonOverride = ps.manifestJsonOverride; - } - - if (ps.enableFanoutTimeline !== undefined) { - set.enableFanoutTimeline = ps.enableFanoutTimeline; - } - - if (ps.enableFanoutTimelineDbFallback !== undefined) { - set.enableFanoutTimelineDbFallback = ps.enableFanoutTimelineDbFallback; - } - - if (ps.perLocalUserUserTimelineCacheMax !== undefined) { - set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax; - } - - if (ps.perRemoteUserUserTimelineCacheMax !== undefined) { - set.perRemoteUserUserTimelineCacheMax = ps.perRemoteUserUserTimelineCacheMax; - } - - if (ps.perUserHomeTimelineCacheMax !== undefined) { - set.perUserHomeTimelineCacheMax = ps.perUserHomeTimelineCacheMax; - } - - if (ps.perUserListTimelineCacheMax !== undefined) { - set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax; - } - - if (ps.enableReactionsBuffering !== undefined) { - set.enableReactionsBuffering = ps.enableReactionsBuffering; - } - - if (ps.notesPerOneAd !== undefined) { - set.notesPerOneAd = ps.notesPerOneAd; - } - - if (ps.bannedEmailDomains !== undefined) { - set.bannedEmailDomains = ps.bannedEmailDomains; - } - - if (ps.urlPreviewEnabled !== undefined) { - set.urlPreviewEnabled = ps.urlPreviewEnabled; - } - - if (ps.urlPreviewAllowRedirect !== undefined) { - set.urlPreviewAllowRedirect = ps.urlPreviewAllowRedirect; - } - - if (ps.urlPreviewTimeout !== undefined) { - set.urlPreviewTimeout = ps.urlPreviewTimeout; - } - - if (ps.urlPreviewMaximumContentLength !== undefined) { - set.urlPreviewMaximumContentLength = ps.urlPreviewMaximumContentLength; - } - - if (ps.urlPreviewRequireContentLength !== undefined) { - set.urlPreviewRequireContentLength = ps.urlPreviewRequireContentLength; - } - - if (ps.urlPreviewUserAgent !== undefined) { - const value = (ps.urlPreviewUserAgent ?? '').trim(); - set.urlPreviewUserAgent = value === '' ? null : ps.urlPreviewUserAgent; - } - - if (ps.summalyProxy !== undefined || ps.urlPreviewSummaryProxyUrl !== undefined) { - const value = ((ps.urlPreviewSummaryProxyUrl ?? ps.summalyProxy) ?? '').trim(); - set.urlPreviewSummaryProxyUrl = value === '' ? null : value; - } - - if (ps.federation !== undefined) { - set.federation = ps.federation; - } - - if (ps.deliverSuspendedSoftware !== undefined) { - set.deliverSuspendedSoftware = ps.deliverSuspendedSoftware; - } - - if (Array.isArray(ps.federationHosts)) { - set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase()); - } - - if (ps.singleUserMode !== undefined) { - set.singleUserMode = ps.singleUserMode; - } - - if (ps.ugcVisibilityForVisitor !== undefined) { - set.ugcVisibilityForVisitor = ps.ugcVisibilityForVisitor; - } - - if (ps.proxyRemoteFiles !== undefined) { - set.proxyRemoteFiles = ps.proxyRemoteFiles; - } - - if (ps.signToActivityPubGet !== undefined) { - set.signToActivityPubGet = ps.signToActivityPubGet; - } - - if (ps.allowExternalApRedirect !== undefined) { - set.allowExternalApRedirect = ps.allowExternalApRedirect; - } - - const before = await this.metaService.fetch(true); - await this.metaService.update(set); - - const after = await this.metaService.fetch(true); - - this.moderationLogService.log(me, 'updateServerSettings', { - before, - after, - }); + this.moderationLogService.insertModerationLog(me, 'updateMeta'); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/update-proxy-account.ts b/packages/backend/src/server/api/endpoints/admin/update-proxy-account.ts deleted file mode 100644 index 6c9612c71a..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/update-proxy-account.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { - descriptionSchema, -} from '@/models/User.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { SystemAccountService } from '@/core/SystemAccountService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'write:admin:account', - - res: { - type: 'object', - nullable: false, optional: false, - ref: 'UserDetailed', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - description: { ...descriptionSchema, nullable: true }, - }, -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private userEntityService: UserEntityService, - private moderationLogService: ModerationLogService, - private systemAccountService: SystemAccountService, - ) { - super(meta, paramDef, async (ps, me) => { - const proxy = await this.systemAccountService.updateCorrespondingUserProfile('proxy', { - description: ps.description, - }); - - const updated = await this.userEntityService.pack(proxy.id, proxy, { - schema: 'MeDetailed', - }); - - if (ps.description !== undefined) { - this.moderationLogService.log(me, 'updateProxyAccountDescription', { - before: null, //TODO - after: ps.description, - }); - } - - return updated; - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts index e9930422c0..33808ee70f 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts @@ -1,20 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], requireCredential: true, requireModerator: true, - kind: 'write:admin:user-note', } as const; export const paramDef = { @@ -26,16 +19,15 @@ export const paramDef = { required: ['userId', 'text'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - - private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy({ id: ps.userId }); @@ -44,19 +36,9 @@ export default class extends Endpoint { // eslint- throw new Error('user not found'); } - const currentProfile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - await this.userProfilesRepository.update({ userId: user.id }, { moderationNote: ps.text, }); - - this.moderationLogService.log(me, 'updateUserNote', { - userId: user.id, - userUsername: user.username, - userHost: user.host, - before: currentProfile.moderationNote, - after: ps.text, - }); }); } } diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts index ff8dd73605..79788be4e2 100644 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ b/packages/backend/src/server/api/endpoints/announcements.ts @@ -1,15 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import { Brackets } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; -import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js'; import { DI } from '@/di-symbols.js'; -import type { AnnouncementsRepository } from '@/models/_.js'; +import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js'; export const meta = { tags: ['meta'], @@ -22,7 +15,40 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, - ref: 'Announcement', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + updatedAt: { + type: 'string', + optional: false, nullable: true, + format: 'date-time', + }, + text: { + type: 'string', + optional: false, nullable: false, + }, + title: { + type: 'string', + optional: false, nullable: false, + }, + imageUrl: { + type: 'string', + optional: false, nullable: true, + }, + isRead: { + type: 'boolean', + optional: true, nullable: false, + }, + }, }, }, } as const; @@ -31,33 +57,45 @@ export const paramDef = { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + withUnreads: { type: 'boolean', default: false }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, - isActive: { type: 'boolean', default: true }, }, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.announcementsRepository) private announcementsRepository: AnnouncementsRepository, + @Inject(DI.announcementReadsRepository) + private announcementReadsRepository: AnnouncementReadsRepository, + private queryService: QueryService, - private announcementEntityService: AnnouncementEntityService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId) - .andWhere('announcement.isActive = :isActive', { isActive: ps.isActive }) - .andWhere(new Brackets(qb => { - if (me) qb.orWhere('announcement.userId = :meId', { meId: me.id }); - qb.orWhere('announcement.userId IS NULL'); - })); + const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); - const announcements = await query.limit(ps.limit).getMany(); + const announcements = await query.take(ps.limit).getMany(); - return this.announcementEntityService.packMany(announcements, me); + if (me) { + const reads = (await this.announcementReadsRepository.findBy({ + userId: me.id, + })).map(x => x.announcementId); + + for (const announcement of announcements) { + (announcement as any).isRead = reads.includes(announcement.id); + } + } + + return (ps.withUnreads ? announcements.filter((a: any) => !a.isRead) : announcements).map((a) => ({ + ...a, + createdAt: a.createdAt.toISOString(), + updatedAt: a.updatedAt?.toISOString() ?? null, + })); }); } } diff --git a/packages/backend/src/server/api/endpoints/announcements/show.ts b/packages/backend/src/server/api/endpoints/announcements/show.ts deleted file mode 100644 index 6312a0a54c..0000000000 --- a/packages/backend/src/server/api/endpoints/announcements/show.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { EntityNotFoundError } from 'typeorm'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AnnouncementService } from '@/core/AnnouncementService.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['meta'], - - requireCredential: false, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'Announcement', - }, - - errors: { - noSuchAnnouncement: { - message: 'No such announcement.', - code: 'NO_SUCH_ANNOUNCEMENT', - id: 'b57b5e1d-4f49-404a-9edb-46b00268f121', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - announcementId: { type: 'string', format: 'misskey:id' }, - }, - required: ['announcementId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private announcementService: AnnouncementService, - ) { - super(meta, paramDef, async (ps, me) => { - try { - return await this.announcementService.getAnnouncement(ps.announcementId, me); - } catch (err) { - if (err instanceof EntityNotFoundError) throw new ApiError(meta.errors.noSuchAnnouncement); - throw err; - } - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index c075608491..5754a9f12a 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; -import type { UserListsRepository, AntennasRepository } from '@/models/_.js'; +import type { UserListsRepository, AntennasRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -34,12 +29,6 @@ export const meta = { code: 'TOO_MANY_ANTENNAS', id: 'faf47050-e8b5-438c-913c-db2b1576fde4', }, - - emptyKeyword: { - message: 'Either keywords or excludeKeywords is required.', - code: 'EMPTY_KEYWORD', - id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a', - }, }, res: { @@ -53,7 +42,7 @@ export const paramDef = { type: 'object', properties: { name: { type: 'string', minLength: 1, maxLength: 100 }, - src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'users_blacklist'] }, + src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, userListId: { type: 'string', format: 'misskey:id', nullable: true }, keywords: { type: 'array', items: { type: 'array', items: { @@ -69,17 +58,16 @@ export const paramDef = { type: 'string', } }, caseSensitive: { type: 'boolean' }, - localOnly: { type: 'boolean' }, - excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, - excludeNotesInSensitiveChannel: { type: 'boolean' }, + notify: { type: 'boolean' }, }, - required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'], + required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, @@ -93,14 +81,14 @@ export default class extends Endpoint { // eslint- private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { - throw new ApiError(meta.errors.emptyKeyword); + if ((ps.keywords.length === 0) || ps.keywords[0].every(x => x === '')) { + throw new Error('invalid param'); } const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id, }); - if (currentAntennasCount >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) { + if (currentAntennasCount > (await this.roleService.getUserPolicies(me.id)).antennaLimit) { throw new ApiError(meta.errors.tooManyAntennas); } @@ -119,8 +107,9 @@ export default class extends Endpoint { // eslint- const now = new Date(); - const antenna = await this.antennasRepository.insertOne({ - id: this.idService.gen(now.getTime()), + const antenna = await this.antennasRepository.insert({ + id: this.idService.genId(), + createdAt: now, lastUsedAt: now, userId: me.id, name: ps.name, @@ -130,12 +119,10 @@ export default class extends Endpoint { // eslint- excludeKeywords: ps.excludeKeywords, users: ps.users, caseSensitive: ps.caseSensitive, - localOnly: ps.localOnly, - excludeBots: ps.excludeBots, withReplies: ps.withReplies, withFile: ps.withFile, - excludeNotesInSensitiveChannel: ps.excludeNotesInSensitiveChannel, - }); + notify: ps.notify, + }).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0])); this.globalEventService.publishInternalEvent('antennaCreated', antenna); diff --git a/packages/backend/src/server/api/endpoints/antennas/delete.ts b/packages/backend/src/server/api/endpoints/antennas/delete.ts index 2258954b56..5da7a2cb66 100644 --- a/packages/backend/src/server/api/endpoints/antennas/delete.ts +++ b/packages/backend/src/server/api/endpoints/antennas/delete.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AntennasRepository } from '@/models/_.js'; +import type { AntennasRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -34,8 +29,9 @@ export const paramDef = { required: ['antennaId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, diff --git a/packages/backend/src/server/api/endpoints/antennas/list.ts b/packages/backend/src/server/api/endpoints/antennas/list.ts index 83d29f9c8c..a0f8979574 100644 --- a/packages/backend/src/server/api/endpoints/antennas/list.ts +++ b/packages/backend/src/server/api/endpoints/antennas/list.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AntennasRepository } from '@/models/_.js'; +import type { AntennasRepository } from '@/models/index.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -33,8 +28,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index b2d9cea03c..e756a9b510 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -1,19 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NotesRepository, AntennasRepository } from '@/models/_.js'; +import type { NotesRepository, AntennasRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; +import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { trackPromise } from '@/misc/promise-tracker.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -55,9 +48,13 @@ export const paramDef = { required: ['antennaId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -67,13 +64,9 @@ export default class extends Endpoint { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, - private fanoutTimelineService: FanoutTimelineService, - private globalEventService: GlobalEventService, + private noteReadService: NoteReadService, ) { super(meta, paramDef, async (ps, me) => { - const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); - const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const antenna = await this.antennasRepository.findOneBy({ id: ps.antennaId, userId: me.id, @@ -83,19 +76,19 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchAntenna); } - // falseだった場合はアンテナの配信先が増えたことを通知したい - const needPublishEvent = !antenna.isActive; + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + const noteIdsRes = await this.redisClient.xrevrange( + `antennaTimeline:${antenna.id}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', + 'COUNT', limit); - antenna.isActive = true; - antenna.lastUsedAt = new Date(); - trackPromise(this.antennasRepository.update(antenna.id, antenna)); - - if (needPublishEvent) { - this.globalEventService.publishInternalEvent('antennaUpdated', antenna); + if (noteIdsRes.length === 0) { + return []; } - let noteIds = await this.fanoutTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId); - noteIds = noteIds.slice(0, ps.limit); + const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId); + if (noteIds.length === 0) { return []; } @@ -108,19 +101,22 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - // NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。 - // https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255 - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBaseNoteFilteringQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); const notes = await query.getMany(); - if (sinceId != null && untilId == null) { - notes.sort((a, b) => a.id < b.id ? -1 : 1); - } else { - notes.sort((a, b) => a.id > b.id ? -1 : 1); + notes.sort((a, b) => a.id > b.id ? -1 : 1); + + if (notes.length > 0) { + this.noteReadService.read(me.id, notes); } + this.antennasRepository.update(antenna.id, { + isActive: true, + lastUsedAt: new Date(), + }); + return await this.noteEntityService.packMany(notes, me); }); } diff --git a/packages/backend/src/server/api/endpoints/antennas/show.ts b/packages/backend/src/server/api/endpoints/antennas/show.ts index a40f187d0b..ef7ed5b72c 100644 --- a/packages/backend/src/server/api/endpoints/antennas/show.ts +++ b/packages/backend/src/server/api/endpoints/antennas/show.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AntennasRepository } from '@/models/_.js'; +import type { AntennasRepository } from '@/models/index.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -40,8 +35,9 @@ export const paramDef = { required: ['antennaId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 53fc4db1b7..5f980bdbeb 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AntennasRepository, UserListsRepository } from '@/models/_.js'; +import type { AntennasRepository, UserListsRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -32,12 +27,6 @@ export const meta = { code: 'NO_SUCH_USER_LIST', id: '1c6b35c9-943e-48c2-81e4-2844989407f7', }, - - emptyKeyword: { - message: 'Either keywords or excludeKeywords is required.', - code: 'EMPTY_KEYWORD', - id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4', - }, }, res: { @@ -52,7 +41,7 @@ export const paramDef = { properties: { antennaId: { type: 'string', format: 'misskey:id' }, name: { type: 'string', minLength: 1, maxLength: 100 }, - src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'users_blacklist'] }, + src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, userListId: { type: 'string', format: 'misskey:id', nullable: true }, keywords: { type: 'array', items: { type: 'array', items: { @@ -68,17 +57,16 @@ export const paramDef = { type: 'string', } }, caseSensitive: { type: 'boolean' }, - localOnly: { type: 'boolean' }, - excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, - excludeNotesInSensitiveChannel: { type: 'boolean' }, + notify: { type: 'boolean' }, }, - required: ['antennaId'], + required: ['antennaId', 'name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, @@ -90,11 +78,6 @@ export default class extends Endpoint { // eslint- private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - if (ps.keywords && ps.excludeKeywords) { - if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { - throw new ApiError(meta.errors.emptyKeyword); - } - } // Fetch the antenna const antenna = await this.antennasRepository.findOneBy({ id: ps.antennaId, @@ -107,7 +90,7 @@ export default class extends Endpoint { // eslint- let userList; - if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) { + if (ps.src === 'list' && ps.userListId) { userList = await this.userListsRepository.findOneBy({ id: ps.userListId, userId: me.id, @@ -121,18 +104,14 @@ export default class extends Endpoint { // eslint- await this.antennasRepository.update(antenna.id, { name: ps.name, src: ps.src, - userListId: ps.userListId !== undefined ? userList ? userList.id : null : undefined, + userListId: userList ? userList.id : null, keywords: ps.keywords, excludeKeywords: ps.excludeKeywords, users: ps.users, caseSensitive: ps.caseSensitive, - localOnly: ps.localOnly, - excludeBots: ps.excludeBots, withReplies: ps.withReplies, withFile: ps.withFile, - excludeNotesInSensitiveChannel: ps.excludeNotesInSensitiveChannel, - isActive: true, - lastUsedAt: new Date(), + notify: ps.notify, }); this.globalEventService.publishInternalEvent('antennaUpdated', await this.antennasRepository.findOneByOrFail({ id: antenna.id })); diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts index 14286bc23e..c45a86761c 100644 --- a/packages/backend/src/server/api/endpoints/ap/get.ts +++ b/packages/backend/src/server/api/endpoints/ap/get.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -11,9 +6,7 @@ import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; export const meta = { tags: ['federation'], - requireAdmin: true, requireCredential: true, - kind: 'read:federation', limit: { duration: ms('1hour'), @@ -37,8 +30,9 @@ export const paramDef = { required: ['uri'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private apResolverService: ApResolverService, ) { diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 4afed7dc5c..a103d4196a 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -1,32 +1,27 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MiNote } from '@/models/Note.js'; -import type { MiLocalUser, MiUser } from '@/models/User.js'; +import type { UsersRepository, NotesRepository } from '@/models/index.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { LocalUser, User } from '@/models/entities/User.js'; import { isActor, isPost, getApId } from '@/core/activitypub/type.js'; import type { SchemaType } from '@/misc/json-schema.js'; import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; +import { MetaService } from '@/core/MetaService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { ApiError } from '../../error.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; export const meta = { tags: ['federation'], requireCredential: true, - kind: 'read:account', limit: { duration: ms('1hour'), @@ -34,26 +29,6 @@ export const meta = { }, errors: { - federationNotAllowed: { - message: 'Federation for this host is not allowed.', - code: 'FEDERATION_NOT_ALLOWED', - id: '974b799e-1a29-4889-b706-18d4dd93e266', - }, - uriInvalid: { - message: 'URI is invalid.', - code: 'URI_INVALID', - id: '1a5eab56-e47b-48c2-8d5e-217b897d70db', - }, - requestFailed: { - message: 'Request failed.', - code: 'REQUEST_FAILED', - id: '81b539cf-4f57-4b29-bc98-032c33c0792e', - }, - responseInvalid: { - message: 'Response from remote server is invalid.', - code: 'RESPONSE_INVALID', - id: '70193c39-54f3-4813-82f0-70a680f7495b', - }, noSuchObject: { message: 'No such object.', code: 'NO_SUCH_OBJECT', @@ -106,12 +81,20 @@ export const paramDef = { required: ['uri'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + private utilityService: UtilityService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, + private metaService: MetaService, private apResolverService: ApResolverService, private apDbResolverService: ApDbResolverService, private apPersonService: ApPersonService, @@ -131,10 +114,10 @@ export default class extends Endpoint { // eslint- * URIからUserかNoteを解決する */ @bindThis - private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise | null> { - if (!this.utilityService.isFederationAllowedUri(uri)) { - throw new ApiError(meta.errors.federationNotAllowed); - } + private async fetchAny(uri: string, me: LocalUser | null | undefined): Promise | null> { + // ブロックしてたら中断 + const fetchedMeta = await this.metaService.fetch(); + if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null; let local = await this.mergePack(me, ...await Promise.all([ this.apDbResolverService.getUserFromApId(uri), @@ -142,45 +125,9 @@ export default class extends Endpoint { // eslint- ])); if (local != null) return local; - const host = this.utilityService.extractDbHost(uri); - - // local object, not found in db? fail - if (this.utilityService.isSelfHost(host)) return null; - // リモートから一旦オブジェクトフェッチ const resolver = this.apResolverService.createResolver(); - // allow ap/show exclusively to lookup URLs that are cross-origin or non-canonical (like https://alice.example.com/@bob@bob.example.com -> https://bob.example.com/@bob) - const object = await resolver.resolve(uri, FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId).catch((err) => { - if (err instanceof IdentifiableError) { - switch (err.id) { - // resolve - case 'b94fd5b1-0e3b-4678-9df2-dad4cd515ab2': - throw new ApiError(meta.errors.uriInvalid); - case '0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5': - case 'd592da9f-822f-4d91-83d7-4ceefabcf3d2': - throw new ApiError(meta.errors.requestFailed); - case '09d79f9e-64f1-4316-9cfa-e75c4d091574': - throw new ApiError(meta.errors.federationNotAllowed); - case '72180409-793c-4973-868e-5a118eb5519b': - throw new ApiError(meta.errors.responseInvalid); - - // resolveLocal - case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8': - throw new ApiError(meta.errors.uriInvalid); - case 'a9d946e5-d276-47f8-95fb-f04230289bb0': - case '06ae3170-1796-4d93-a697-2611ea6d83b6': - throw new ApiError(meta.errors.noSuchObject); - case '7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0': - throw new ApiError(meta.errors.responseInvalid); - } - } - - throw new ApiError(meta.errors.requestFailed); - }); - - if (object.id == null) { - throw new ApiError(meta.errors.responseInvalid); - } + const object = await resolver.resolve(uri) as any; // /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する // これはDBに存在する可能性があるため再度DB検索 @@ -192,20 +139,19 @@ export default class extends Endpoint { // eslint- if (local != null) return local; } - // 同一ユーザーの情報を再度処理するので、使用済みのresolverを再利用してはいけない return await this.mergePack( me, isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null, - isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, undefined, true) : null, + isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, true) : null, ); } @bindThis - private async mergePack(me: MiLocalUser | null | undefined, user: MiUser | null | undefined, note: MiNote | null | undefined): Promise | null> { + private async mergePack(me: LocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise | null> { if (user != null) { return { type: 'User', - object: await this.userEntityService.pack(user, me, { schema: 'UserDetailedNotMe' }), + object: await this.userEntityService.pack(user, me, { detail: true }), }; } else if (note != null) { try { diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts index ba847fc4f0..aaef02d03f 100644 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ b/packages/backend/src/server/api/endpoints/app/create.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AppsRepository } from '@/models/_.js'; +import type { AppsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { unique } from '@/misc/prelude/array.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; @@ -37,8 +32,9 @@ export const paramDef = { required: ['name', 'description', 'permission'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.appsRepository) private appsRepository: AppsRepository, @@ -54,15 +50,16 @@ export default class extends Endpoint { // eslint- const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1'))); // Create account - const app = await this.appsRepository.insertOne({ - id: this.idService.gen(), + const app = await this.appsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), userId: me ? me.id : null, name: ps.name, description: ps.description, permission, callbackUrl: ps.callbackUrl, secret: secret, - }); + }).then(x => this.appsRepository.findOneByOrFail(x.identifiers[0])); return await this.appEntityService.pack(app, null, { detail: true, diff --git a/packages/backend/src/server/api/endpoints/app/show.ts b/packages/backend/src/server/api/endpoints/app/show.ts index 3db9a0d0d4..eaafa8dc1b 100644 --- a/packages/backend/src/server/api/endpoints/app/show.ts +++ b/packages/backend/src/server/api/endpoints/app/show.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AppsRepository } from '@/models/_.js'; +import type { AppsRepository } from '@/models/index.js'; import { AppEntityService } from '@/core/entities/AppEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -36,8 +31,9 @@ export const paramDef = { required: ['appId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.appsRepository) private appsRepository: AppsRepository, diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts index 2e62f04df0..e69f9c12e2 100644 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ b/packages/backend/src/server/api/endpoints/auth/accept.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as crypto from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AuthSessionsRepository, AppsRepository, AccessTokensRepository } from '@/models/_.js'; +import type { AuthSessionsRepository, AppsRepository, AccessTokensRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DI } from '@/di-symbols.js'; @@ -36,8 +31,9 @@ export const paramDef = { required: ['token'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.appsRepository) private appsRepository: AppsRepository, @@ -62,14 +58,12 @@ export default class extends Endpoint { // eslint- const accessToken = secureRndstr(32); // Fetch exist access token - const exist = await this.accessTokensRepository.exists({ - where: { - appId: session.appId, - userId: me.id, - }, + const exist = await this.accessTokensRepository.findOneBy({ + appId: session.appId, + userId: me.id, }); - if (!exist) { + if (exist == null) { const app = await this.appsRepository.findOneByOrFail({ id: session.appId }); // Generate Hash @@ -80,7 +74,8 @@ export default class extends Endpoint { // eslint- const now = new Date(); await this.accessTokensRepository.insert({ - id: this.idService.gen(now.getTime()), + id: this.idService.genId(), + createdAt: now, lastUsedAt: now, appId: session.appId, userId: me.id, diff --git a/packages/backend/src/server/api/endpoints/auth/session/generate.ts b/packages/backend/src/server/api/endpoints/auth/session/generate.ts index f8ddfdb75c..6108d8202d 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { randomUUID } from 'node:crypto'; +import { v4 as uuid } from 'uuid'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AppsRepository, AuthSessionsRepository } from '@/models/_.js'; +import type { AppsRepository, AuthSessionsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; @@ -50,8 +45,9 @@ export const paramDef = { required: ['appSecret'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.config) private config: Config, @@ -75,14 +71,15 @@ export default class extends Endpoint { // eslint- } // Generate token - const token = randomUUID(); + const token = uuid(); // Create session token document - const doc = await this.authSessionsRepository.insertOne({ - id: this.idService.gen(), + const doc = await this.authSessionsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), appId: app.id, token: token, - }); + }).then(x => this.authSessionsRepository.findOneByOrFail(x.identifiers[0])); return { token: doc.token, diff --git a/packages/backend/src/server/api/endpoints/auth/session/show.ts b/packages/backend/src/server/api/endpoints/auth/session/show.ts index 13e02a2541..db3bf7aa63 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/show.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/show.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AuthSessionsRepository } from '@/models/_.js'; +import type { AuthSessionsRepository } from '@/models/index.js'; import { AuthSessionEntityService } from '@/core/entities/AuthSessionEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -53,8 +48,9 @@ export const paramDef = { required: ['token'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.authSessionsRepository) private authSessionsRepository: AuthSessionsRepository, diff --git a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts index b490c5832d..b1e7bbfded 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AppsRepository, AccessTokensRepository, AuthSessionsRepository } from '@/models/_.js'; +import type { UsersRepository, AppsRepository, AccessTokensRepository, AuthSessionsRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -62,9 +57,13 @@ export const paramDef = { required: ['appSecret', 'token'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.appsRepository) private appsRepository: AppsRepository, @@ -112,7 +111,7 @@ export default class extends Endpoint { // eslint- return { accessToken: accessToken.token, user: await this.userEntityService.pack(session.userId, null, { - schema: 'UserDetailedNotMe', + detail: true, }), }; }); diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index 5066215749..d9ba99f209 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, BlockingsRepository } from '@/models/_.js'; +import type { UsersRepository, BlockingsRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { DI } from '@/di-symbols.js'; @@ -60,8 +55,9 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -88,21 +84,19 @@ export default class extends Endpoint { // eslint- }); // Check if already blocking - const exist = await this.blockingsRepository.exists({ - where: { - blockerId: blocker.id, - blockeeId: blockee.id, - }, + const exist = await this.blockingsRepository.findOneBy({ + blockerId: blocker.id, + blockeeId: blockee.id, }); - if (exist) { + if (exist != null) { throw new ApiError(meta.errors.alreadyBlocking); } await this.userBlockingService.block(blocker, blockee); return await this.userEntityService.pack(blockee.id, blocker, { - schema: 'UserDetailedNotMe', + detail: true, }); }); } diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts index cebb307338..46dd26a45a 100644 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts @@ -1,17 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, BlockingsRepository } from '@/models/_.js'; +import type { UsersRepository, BlockingsRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { DI } from '@/di-symbols.js'; -import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['account'], @@ -60,8 +55,9 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -88,14 +84,12 @@ export default class extends Endpoint { // eslint- }); // Check not blocking - const exist = await this.blockingsRepository.exists({ - where: { - blockerId: blocker.id, - blockeeId: blockee.id, - }, + const exist = await this.blockingsRepository.findOneBy({ + blockerId: blocker.id, + blockeeId: blockee.id, }); - if (!exist) { + if (exist == null) { throw new ApiError(meta.errors.notBlocking); } @@ -103,7 +97,7 @@ export default class extends Endpoint { // eslint- await this.userBlockingService.unblock(blocker, blockee); return await this.userEntityService.pack(blockee.id, blocker, { - schema: 'UserDetailedNotMe', + detail: true, }); }); } diff --git a/packages/backend/src/server/api/endpoints/blocking/list.ts b/packages/backend/src/server/api/endpoints/blocking/list.ts index 8431fa6b34..969aae06f9 100644 --- a/packages/backend/src/server/api/endpoints/blocking/list.ts +++ b/packages/backend/src/server/api/endpoints/blocking/list.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { BlockingsRepository } from '@/models/_.js'; +import type { BlockingsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { BlockingEntityService } from '@/core/entities/BlockingEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -38,8 +33,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, @@ -52,7 +48,7 @@ export default class extends Endpoint { // eslint- .andWhere('blocking.blockerId = :meId', { meId: me.id }); const blockings = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.blockingEntityService.packMany(blockings, me); diff --git a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts deleted file mode 100644 index ab877bbe20..0000000000 --- a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { MoreThan } from 'typeorm'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { BubbleGameRecordsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; - -export const meta = { - allowGet: true, - cacheSec: 60, - - errors: { - }, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', format: 'misskey:id', - optional: false, nullable: false, - }, - score: { - type: 'integer', - optional: false, nullable: false, - }, - user: { - type: 'object', - optional: true, nullable: false, - ref: 'UserLite', - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - gameMode: { type: 'string' }, - }, - required: ['gameMode'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.bubbleGameRecordsRepository) - private bubbleGameRecordsRepository: BubbleGameRecordsRepository, - - private userEntityService: UserEntityService, - ) { - super(meta, paramDef, async (ps) => { - const records = await this.bubbleGameRecordsRepository.find({ - where: { - gameMode: ps.gameMode, - seededAt: MoreThan(new Date(Date.now() - 1000 * 60 * 60 * 24 * 7)), - }, - order: { - score: 'DESC', - }, - take: 10, - relations: ['user'], - }); - - const users = await this.userEntityService.packMany(records.map(r => r.user!), null); - - return records.map(r => ({ - id: r.id, - score: r.score, - user: users.find(u => u.id === r.user!.id), - })); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/bubble-game/register.ts b/packages/backend/src/server/api/endpoints/bubble-game/register.ts deleted file mode 100644 index 0a999e42cd..0000000000 --- a/packages/backend/src/server/api/endpoints/bubble-game/register.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { IdService } from '@/core/IdService.js'; -import type { BubbleGameRecordsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - requireCredential: true, - - kind: 'write:account', - - limit: { - duration: ms('1hour'), - max: 120, - minInterval: ms('30sec'), - }, - - errors: { - invalidSeed: { - message: 'Provided seed is invalid.', - code: 'INVALID_SEED', - id: 'eb627bc7-574b-4a52-a860-3c3eae772b88', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - score: { type: 'integer', minimum: 0 }, - seed: { type: 'string', minLength: 1, maxLength: 1024 }, - logs: { - type: 'array', - items: { - type: 'array', - items: { - type: 'number', - }, - }, - }, - gameMode: { type: 'string' }, - gameVersion: { type: 'integer' }, - }, - required: ['score', 'seed', 'logs', 'gameMode', 'gameVersion'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.bubbleGameRecordsRepository) - private bubbleGameRecordsRepository: BubbleGameRecordsRepository, - - private idService: IdService, - ) { - super(meta, paramDef, async (ps, me) => { - const seedDate = new Date(parseInt(ps.seed, 10)); - const now = new Date(); - - // シードが未来なのは通常のプレイではありえないので弾く - if (seedDate.getTime() > now.getTime()) { - throw new ApiError(meta.errors.invalidSeed); - } - - // シードが古すぎる(5時間以上前)のも弾く - if (seedDate.getTime() < now.getTime() - 1000 * 60 * 60 * 5) { - throw new ApiError(meta.errors.invalidSeed); - } - - await this.bubbleGameRecordsRepository.insert({ - id: this.idService.gen(now.getTime()), - seed: ps.seed, - seededAt: seedDate, - userId: me.id, - score: ps.score, - logs: ps.logs, - gameMode: ps.gameMode, - gameVersion: ps.gameVersion, - isVerified: false, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts index e3a6d2d670..69e2f2504c 100644 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ b/packages/backend/src/server/api/endpoints/channels/create.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository, DriveFilesRepository } from '@/models/_.js'; -import type { MiChannel } from '@/models/Channel.js'; +import type { ChannelsRepository, DriveFilesRepository } from '@/models/index.js'; +import type { Channel } from '@/models/entities/Channel.js'; import { IdService } from '@/core/IdService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -49,14 +44,13 @@ export const paramDef = { description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, bannerId: { type: 'string', format: 'misskey:id', nullable: true }, color: { type: 'string', minLength: 1, maxLength: 16 }, - isSensitive: { type: 'boolean', nullable: true }, - allowRenoteToExternal: { type: 'boolean', nullable: true }, }, required: ['name'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -80,16 +74,15 @@ export default class extends Endpoint { // eslint- } } - const channel = await this.channelsRepository.insertOne({ - id: this.idService.gen(), + const channel = await this.channelsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), userId: me.id, name: ps.name, description: ps.description ?? null, bannerId: banner ? banner.id : null, - isSensitive: ps.isSensitive ?? false, ...(ps.color !== undefined ? { color: ps.color } : {}), - allowRenoteToExternal: ps.allowRenoteToExternal ?? true, - } as MiChannel); + } as Channel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0])); return await this.channelEntityService.pack(channel, me); }); diff --git a/packages/backend/src/server/api/endpoints/channels/favorite.ts b/packages/backend/src/server/api/endpoints/channels/favorite.ts index a1ae9b80a7..c8544273a1 100644 --- a/packages/backend/src/server/api/endpoints/channels/favorite.ts +++ b/packages/backend/src/server/api/endpoints/channels/favorite.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/_.js'; +import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -36,8 +31,9 @@ export const paramDef = { required: ['channelId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, @@ -57,7 +53,8 @@ export default class extends Endpoint { // eslint- } await this.channelFavoritesRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), userId: me.id, channelId: channel.id, }); diff --git a/packages/backend/src/server/api/endpoints/channels/featured.ts b/packages/backend/src/server/api/endpoints/channels/featured.ts index a9a79ba8fc..1a8d1164c7 100644 --- a/packages/backend/src/server/api/endpoints/channels/featured.ts +++ b/packages/backend/src/server/api/endpoints/channels/featured.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository } from '@/models/_.js'; +import type { ChannelsRepository } from '@/models/index.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -31,8 +26,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, @@ -45,7 +41,7 @@ export default class extends Endpoint { // eslint- .andWhere('channel.isArchived = FALSE') .orderBy('channel.lastNotedAt', 'DESC'); - const channels = await query.limit(10).getMany(); + const channels = await query.take(10).getMany(); return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me))); }); diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts index 1812820ba2..f3ca66cfd2 100644 --- a/packages/backend/src/server/api/endpoints/channels/follow.ts +++ b/packages/backend/src/server/api/endpoints/channels/follow.ts @@ -1,13 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository } from '@/models/_.js'; +import type { ChannelFollowingsRepository, ChannelsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -36,12 +32,17 @@ export const paramDef = { required: ['channelId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, - private channelFollowingService: ChannelFollowingService, + + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const channel = await this.channelsRepository.findOneBy({ @@ -52,7 +53,12 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchChannel); } - await this.channelFollowingService.follow(me, channel); + await this.channelFollowingsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + followerId: me.id, + followeeId: channel.id, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/channels/followed.ts b/packages/backend/src/server/api/endpoints/channels/followed.ts index 294b5e4bc4..f49f3105d5 100644 --- a/packages/backend/src/server/api/endpoints/channels/followed.ts +++ b/packages/backend/src/server/api/endpoints/channels/followed.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelFollowingsRepository } from '@/models/_.js'; +import type { ChannelFollowingsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -38,8 +33,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.channelFollowingsRepository) private channelFollowingsRepository: ChannelFollowingsRepository, @@ -48,19 +44,11 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService - .makePaginationQuery( - this.channelFollowingsRepository.createQueryBuilder(), - ps.sinceId, - ps.untilId, - null, - null, - 'followeeId', - ) + const query = this.queryService.makePaginationQuery(this.channelFollowingsRepository.createQueryBuilder(), ps.sinceId, ps.untilId) .andWhere({ followerId: me.id }); const followings = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await Promise.all(followings.map(x => this.channelEntityService.pack(x.followeeId, me))); diff --git a/packages/backend/src/server/api/endpoints/channels/my-favorites.ts b/packages/backend/src/server/api/endpoints/channels/my-favorites.ts index d96e6c3ad2..60525ed060 100644 --- a/packages/backend/src/server/api/endpoints/channels/my-favorites.ts +++ b/packages/backend/src/server/api/endpoints/channels/my-favorites.ts @@ -1,11 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelFavoritesRepository } from '@/models/_.js'; +import type { ChannelFavoritesRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -34,13 +30,15 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.channelFavoritesRepository) private channelFavoritesRepository: ChannelFavoritesRepository, private channelEntityService: ChannelEntityService, + private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { const query = this.channelFavoritesRepository.createQueryBuilder('favorite') diff --git a/packages/backend/src/server/api/endpoints/channels/owned.ts b/packages/backend/src/server/api/endpoints/channels/owned.ts index daab685f1b..8fae972cb1 100644 --- a/packages/backend/src/server/api/endpoints/channels/owned.ts +++ b/packages/backend/src/server/api/endpoints/channels/owned.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository } from '@/models/_.js'; +import type { ChannelsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -38,8 +33,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, @@ -53,7 +49,7 @@ export default class extends Endpoint { // eslint- .andWhere({ userId: me.id }); const channels = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me))); diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts index ae32203603..a3b40b0bbd 100644 --- a/packages/backend/src/server/api/endpoints/channels/search.ts +++ b/packages/backend/src/server/api/endpoints/channels/search.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; -import type { ChannelsRepository } from '@/models/_.js'; +import type { ChannelsRepository } from '@/models/index.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; @@ -40,8 +35,9 @@ export const paramDef = { required: ['query'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, @@ -55,10 +51,9 @@ export default class extends Endpoint { // eslint- if (ps.query !== '') { if (ps.type === 'nameAndDescription') { - query.andWhere(new Brackets(qb => { - qb - .where('channel.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }) - .orWhere('channel.description ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); + query.andWhere(new Brackets(qb => { qb + .where('channel.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }) + .orWhere('channel.description ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); })); } else { query.andWhere('channel.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); @@ -66,7 +61,7 @@ export default class extends Endpoint { // eslint- } const channels = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me))); diff --git a/packages/backend/src/server/api/endpoints/channels/show.ts b/packages/backend/src/server/api/endpoints/channels/show.ts index 332ce2c9dc..070d14631e 100644 --- a/packages/backend/src/server/api/endpoints/channels/show.ts +++ b/packages/backend/src/server/api/endpoints/channels/show.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository } from '@/models/_.js'; +import type { ChannelsRepository } from '@/models/index.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -38,8 +33,9 @@ export const paramDef = { required: ['channelId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 46b050d4b4..c881074bab 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -1,18 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository, MiMeta, NotesRepository } from '@/models/_.js'; +import type { ChannelsRepository, Note, NotesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; -import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; -import { MiLocalUser } from '@/models/User.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -48,16 +42,16 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, - allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default }, required: ['channelId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, + @Inject(DI.redis) + private redisClient: Redis.Redis, @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -68,13 +62,9 @@ export default class extends Endpoint { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, - private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private activeUsersChart: ActiveUsersChart, ) { super(meta, paramDef, async (ps, me) => { - const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); - const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const channel = await this.channelsRepository.findOneBy({ id: ps.channelId, }); @@ -83,47 +73,70 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchChannel); } - if (me) this.activeUsersChart.read(me); + let timeline: Note[] = []; - if (!this.serverSettings.enableFanoutTimeline) { - return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me); + const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + let noteIdsRes: [string, string[]][] = []; + + if (!ps.sinceId && !ps.sinceDate) { + noteIdsRes = await this.redisClient.xrevrange( + `channelTimeline:${channel.id}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + '-', + 'COUNT', limit); } - return await this.fanoutTimelineEndpointService.timeline({ - untilId, - sinceId, - limit: ps.limit, - allowPartial: ps.allowPartial, - me, - useDbFallback: true, - redisTimelines: [`channelTimeline:${channel.id}`], - excludePureRenotes: false, - dbFallback: async (untilId, sinceId, limit) => { - return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me); - }, - }); + // redis から取得していないとき・取得数が足りないとき + if (noteIdsRes.length < limit) { + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.channelId = :channelId', { channelId: channel.id }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateMutedNoteQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + } + //#endregion + + timeline = await query.take(ps.limit).getMany(); + } else { + const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); + + if (noteIds.length === 0) { + return []; + } + + //#region Construct query + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateMutedNoteQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + } + //#endregion + + timeline = await query.getMany(); + timeline.sort((a, b) => a.id > b.id ? -1 : 1); + } + + if (me) this.activeUsersChart.read(me); + + return await this.noteEntityService.packMany(timeline, me); }); } - - private async getFromDb(ps: { - untilId: string | null, - sinceId: string | null, - limit: number, - channelId: string - }, me: MiLocalUser | null) { - //#region fallback to database - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere('note.channelId = :channelId', { channelId: ps.channelId }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - this.queryService.generateBaseNoteFilteringQuery(query, me); - //#endregion - - return await query.limit(ps.limit).getMany(); - } } diff --git a/packages/backend/src/server/api/endpoints/channels/unfavorite.ts b/packages/backend/src/server/api/endpoints/channels/unfavorite.ts index fc6b75e295..67fb1ea03e 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfavorite.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/_.js'; +import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -35,8 +30,9 @@ export const paramDef = { required: ['channelId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, diff --git a/packages/backend/src/server/api/endpoints/channels/unfollow.ts b/packages/backend/src/server/api/endpoints/channels/unfollow.ts index 48c5261135..f46ff9f286 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfollow.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfollow.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository } from '@/models/_.js'; +import type { ChannelFollowingsRepository, ChannelsRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -36,12 +31,15 @@ export const paramDef = { required: ['channelId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, - private channelFollowingService: ChannelFollowingService, + + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, ) { super(meta, paramDef, async (ps, me) => { const channel = await this.channelsRepository.findOneBy({ @@ -52,7 +50,10 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchChannel); } - await this.channelFollowingService.unfollow(me, channel); + await this.channelFollowingsRepository.delete({ + followerId: me.id, + followeeId: channel.id, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index dba2938b39..30d7f8b244 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository, ChannelsRepository } from '@/models/_.js'; +import type { DriveFilesRepository, ChannelsRepository } from '@/models/index.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; @@ -60,14 +55,13 @@ export const paramDef = { }, }, color: { type: 'string', minLength: 1, maxLength: 16 }, - isSensitive: { type: 'boolean', nullable: true }, - allowRenoteToExternal: { type: 'boolean', nullable: true }, }, required: ['channelId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, @@ -115,8 +109,6 @@ export default class extends Endpoint { // eslint- ...(ps.color !== undefined ? { color: ps.color } : {}), ...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}), ...(banner ? { bannerId: banner.id } : {}), - ...(typeof ps.isSensitive === 'boolean' ? { isSensitive: ps.isSensitive } : {}), - ...(typeof ps.allowRenoteToExternal === 'boolean' ? { allowRenoteToExternal: ps.allowRenoteToExternal } : {}), }); return await this.channelEntityService.pack(channel.id, me); diff --git a/packages/backend/src/server/api/endpoints/charts/active-users.ts b/packages/backend/src/server/api/endpoints/charts/active-users.ts index fd21e3d9fe..2ab58e4309 100644 --- a/packages/backend/src/server/api/endpoints/charts/active-users.ts +++ b/packages/backend/src/server/api/endpoints/charts/active-users.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -28,8 +23,9 @@ export const paramDef = { required: ['span'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private activeUsersChart: ActiveUsersChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/ap-request.ts b/packages/backend/src/server/api/endpoints/charts/ap-request.ts index cbe792376b..e40a53d82e 100644 --- a/packages/backend/src/server/api/endpoints/charts/ap-request.ts +++ b/packages/backend/src/server/api/endpoints/charts/ap-request.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -28,8 +23,9 @@ export const paramDef = { required: ['span'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private apRequestChart: ApRequestChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/drive.ts b/packages/backend/src/server/api/endpoints/charts/drive.ts index d32bc765a4..9a5aff4af9 100644 --- a/packages/backend/src/server/api/endpoints/charts/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/drive.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -28,8 +23,9 @@ export const paramDef = { required: ['span'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private driveChart: DriveChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/federation.ts b/packages/backend/src/server/api/endpoints/charts/federation.ts index dad21e9e8e..ed3a968681 100644 --- a/packages/backend/src/server/api/endpoints/charts/federation.ts +++ b/packages/backend/src/server/api/endpoints/charts/federation.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -28,8 +23,9 @@ export const paramDef = { required: ['span'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private federationChart: FederationChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/instance.ts b/packages/backend/src/server/api/endpoints/charts/instance.ts index 68aa12ac0e..c992d525c9 100644 --- a/packages/backend/src/server/api/endpoints/charts/instance.ts +++ b/packages/backend/src/server/api/endpoints/charts/instance.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -29,8 +24,9 @@ export const paramDef = { required: ['span', 'host'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private instanceChart: InstanceChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/notes.ts b/packages/backend/src/server/api/endpoints/charts/notes.ts index e1979cfe8b..5750cd5b78 100644 --- a/packages/backend/src/server/api/endpoints/charts/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/notes.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -28,8 +23,9 @@ export const paramDef = { required: ['span'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private notesChart: NotesChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/user/drive.ts b/packages/backend/src/server/api/endpoints/charts/user/drive.ts index dcb72084b7..5e372294b7 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/drive.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -29,8 +24,9 @@ export const paramDef = { required: ['span', 'userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private perUserDriveChart: PerUserDriveChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts index 0a019ce4fb..3f50918fa7 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/following.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { getJsonSchema } from '@/core/chart/core.js'; @@ -29,8 +24,9 @@ export const paramDef = { required: ['span', 'userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private perUserFollowingChart: PerUserFollowingChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/user/notes.ts b/packages/backend/src/server/api/endpoints/charts/user/notes.ts index 06b15bca18..0517b3283f 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/notes.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -29,8 +24,9 @@ export const paramDef = { required: ['span', 'userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private perUserNotesChart: PerUserNotesChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/user/pv.ts b/packages/backend/src/server/api/endpoints/charts/user/pv.ts index d359b491e2..8d1a9aee10 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/pv.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/pv.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -29,8 +24,9 @@ export const paramDef = { required: ['span', 'userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private perUserPvChart: PerUserPvChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts index 4355aa5348..f2ff413195 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -29,8 +24,9 @@ export const paramDef = { required: ['span', 'userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private perUserReactionsChart: PerUserReactionsChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/users.ts b/packages/backend/src/server/api/endpoints/charts/users.ts index 1f5f5fea54..1374f02046 100644 --- a/packages/backend/src/server/api/endpoints/charts/users.ts +++ b/packages/backend/src/server/api/endpoints/charts/users.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -28,8 +23,9 @@ export const paramDef = { required: ['span'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private usersChart: UsersChart, ) { diff --git a/packages/backend/src/server/api/endpoints/chat/history.ts b/packages/backend/src/server/api/endpoints/chat/history.ts deleted file mode 100644 index fdd9055106..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/history.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'read:chat', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatMessage', - }, - }, - - errors: { - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - room: { type: 'boolean', default: false }, - }, -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatEntityService: ChatEntityService, - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'read'); - - const history = ps.room ? await this.chatService.roomHistory(me.id, ps.limit) : await this.chatService.userHistory(me.id, ps.limit); - - const packedMessages = await this.chatEntityService.packMessagesDetailed(history, me); - - if (ps.room) { - const roomIds = history.map(m => m.toRoomId!); - const readStateMap = await this.chatService.getRoomReadStateMap(me.id, roomIds); - - for (const message of packedMessages) { - message.isRead = readStateMap[message.toRoomId!] ?? false; - } - } else { - const otherIds = history.map(m => m.fromUserId === me.id ? m.toUserId! : m.fromUserId!); - const readStateMap = await this.chatService.getUserReadStateMap(me.id, otherIds); - - for (const message of packedMessages) { - const otherId = message.fromUserId === me.id ? message.toUserId! : message.fromUserId!; - message.isRead = readStateMap[otherId] ?? false; - } - } - - return packedMessages; - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts b/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts deleted file mode 100644 index ad2b82e219..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '@/server/api/error.js'; -import { ChatService } from '@/core/ChatService.js'; -import type { DriveFilesRepository, MiUser } from '@/models/_.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - prohibitMoved: true, - - kind: 'write:chat', - - limit: { - duration: ms('1hour'), - max: 500, - }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatMessageLiteForRoom', - }, - - errors: { - noSuchRoom: { - message: 'No such room.', - code: 'NO_SUCH_ROOM', - id: '8098520d-2da5-4e8f-8ee1-df78b55a4ec6', - }, - - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: 'b6accbd3-1d7b-4d9f-bdb7-eb185bac06db', - }, - - contentRequired: { - message: 'Content required. You need to set text or fileId.', - code: 'CONTENT_REQUIRED', - id: '340517b7-6d04-42c0-bac1-37ee804e3594', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - text: { type: 'string', nullable: true, maxLength: 2000 }, - fileId: { type: 'string', format: 'misskey:id' }, - toRoomId: { type: 'string', format: 'misskey:id' }, - }, - required: ['toRoomId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - - private getterService: GetterService, - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'write'); - - const room = await this.chatService.findRoomById(ps.toRoomId); - if (room == null) { - throw new ApiError(meta.errors.noSuchRoom); - } - - let file = null; - if (ps.fileId != null) { - file = await this.driveFilesRepository.findOneBy({ - id: ps.fileId, - userId: me.id, - }); - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - // テキストが無いかつ添付ファイルも無かったらエラー - if (ps.text == null && file == null) { - throw new ApiError(meta.errors.contentRequired); - } - - return await this.chatService.createMessageToRoom(me, room, { - text: ps.text, - file: file, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts b/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts deleted file mode 100644 index fa34a7d558..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '@/server/api/error.js'; -import { ChatService } from '@/core/ChatService.js'; -import type { DriveFilesRepository, MiUser } from '@/models/_.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - prohibitMoved: true, - - kind: 'write:chat', - - limit: { - duration: ms('1hour'), - max: 500, - }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatMessageLiteFor1on1', - }, - - errors: { - recipientIsYourself: { - message: 'You can not send a message to yourself.', - code: 'RECIPIENT_IS_YOURSELF', - id: '17e2ba79-e22a-4cbc-bf91-d327643f4a7e', - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '11795c64-40ea-4198-b06e-3c873ed9039d', - }, - - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: '4372b8e2-185d-4146-8749-2f68864a3e5f', - }, - - contentRequired: { - message: 'Content required. You need to set text or fileId.', - code: 'CONTENT_REQUIRED', - id: '25587321-b0e6-449c-9239-f8925092942c', - }, - - youHaveBeenBlocked: { - message: 'You cannot send a message because you have been blocked by this user.', - code: 'YOU_HAVE_BEEN_BLOCKED', - id: 'c15a5199-7422-4968-941a-2a462c478f7d', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - text: { type: 'string', nullable: true, maxLength: 2000 }, - fileId: { type: 'string', format: 'misskey:id' }, - toUserId: { type: 'string', format: 'misskey:id' }, - }, - required: ['toUserId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - - private getterService: GetterService, - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'write'); - - let file = null; - if (ps.fileId != null) { - file = await this.driveFilesRepository.findOneBy({ - id: ps.fileId, - userId: me.id, - }); - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - // テキストが無いかつ添付ファイルも無かったらエラー - if (ps.text == null && file == null) { - throw new ApiError(meta.errors.contentRequired); - } - - // Myself - if (ps.toUserId === me.id) { - throw new ApiError(meta.errors.recipientIsYourself); - } - - const toUser = await this.getterService.getUser(ps.toUserId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw err; - }); - - return await this.chatService.createMessageToUser(me, toUser, { - text: ps.text, - file: file, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/delete.ts b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts deleted file mode 100644 index 52a054303b..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/messages/delete.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'write:chat', - - errors: { - noSuchMessage: { - message: 'No such message.', - code: 'NO_SUCH_MESSAGE', - id: '36b67f0e-66a6-414b-83df-992a55294f17', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - messageId: { type: 'string', format: 'misskey:id' }, - }, - required: ['messageId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'write'); - - const message = await this.chatService.findMyMessageById(me.id, ps.messageId); - if (message == null) { - throw new ApiError(meta.errors.noSuchMessage); - } - await this.chatService.deleteMessage(message); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/react.ts b/packages/backend/src/server/api/endpoints/chat/messages/react.ts deleted file mode 100644 index 2197e7bf80..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/messages/react.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'write:chat', - - errors: { - noSuchMessage: { - message: 'No such message.', - code: 'NO_SUCH_MESSAGE', - id: '9b5839b9-0ba0-4351-8c35-37082093d200', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - messageId: { type: 'string', format: 'misskey:id' }, - reaction: { type: 'string' }, - }, - required: ['messageId', 'reaction'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'write'); - - await this.chatService.react(ps.messageId, me.id, ps.reaction); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts deleted file mode 100644 index c0e344b889..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'read:chat', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatMessageLiteForRoom', - }, - }, - - errors: { - noSuchRoom: { - message: 'No such room.', - code: 'NO_SUCH_ROOM', - id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - roomId: { type: 'string', format: 'misskey:id' }, - }, - required: ['roomId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatEntityService: ChatEntityService, - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'read'); - - const room = await this.chatService.findRoomById(ps.roomId); - if (room == null) { - throw new ApiError(meta.errors.noSuchRoom); - } - - if (!await this.chatService.hasPermissionToViewRoomTimeline(me.id, room)) { - throw new ApiError(meta.errors.noSuchRoom); - } - - const messages = await this.chatService.roomTimeline(room.id, ps.limit, ps.sinceId, ps.untilId); - - this.chatService.readRoomChatMessage(me.id, room.id); - - return await this.chatEntityService.packMessagesLiteForRoom(messages); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/search.ts b/packages/backend/src/server/api/endpoints/chat/messages/search.ts deleted file mode 100644 index 682597f76d..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/messages/search.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'read:chat', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatMessage', - }, - }, - - errors: { - noSuchRoom: { - message: 'No such room.', - code: 'NO_SUCH_ROOM', - id: '460b3669-81b0-4dc9-a997-44442141bf83', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - query: { type: 'string', minLength: 1, maxLength: 256 }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - userId: { type: 'string', format: 'misskey:id', nullable: true }, - roomId: { type: 'string', format: 'misskey:id', nullable: true }, - }, - required: ['query'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatEntityService: ChatEntityService, - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'read'); - - if (ps.roomId != null) { - const room = await this.chatService.findRoomById(ps.roomId); - if (room == null) { - throw new ApiError(meta.errors.noSuchRoom); - } - - if (!(await this.chatService.isRoomMember(room, me.id))) { - throw new ApiError(meta.errors.noSuchRoom); - } - } - - const messages = await this.chatService.searchMessages(me.id, ps.query, ps.limit, { - userId: ps.userId, - roomId: ps.roomId, - }); - - return await this.chatEntityService.packMessagesDetailed(messages, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/show.ts b/packages/backend/src/server/api/endpoints/chat/messages/show.ts deleted file mode 100644 index 9a2bbb8742..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/messages/show.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; -import { ApiError } from '@/server/api/error.js'; -import { RoleService } from '@/core/RoleService.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'read:chat', - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatMessage', - }, - - errors: { - noSuchMessage: { - message: 'No such message.', - code: 'NO_SUCH_MESSAGE', - id: '3710865b-1848-4da9-8d61-cfed15510b93', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - messageId: { type: 'string', format: 'misskey:id' }, - }, - required: ['messageId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - private roleService: RoleService, - private chatEntityService: ChatEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'read'); - - const message = await this.chatService.findMessageById(ps.messageId); - if (message == null) { - throw new ApiError(meta.errors.noSuchMessage); - } - if (message.fromUserId !== me.id && message.toUserId !== me.id && !(await this.roleService.isModerator(me))) { - throw new ApiError(meta.errors.noSuchMessage); - } - return this.chatEntityService.packMessageDetailed(message, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts deleted file mode 100644 index adfcd232f9..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'write:chat', - - errors: { - noSuchMessage: { - message: 'No such message.', - code: 'NO_SUCH_MESSAGE', - id: 'c39ea42f-e3ca-428a-ad57-390e0a711595', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - messageId: { type: 'string', format: 'misskey:id' }, - reaction: { type: 'string' }, - }, - required: ['messageId', 'reaction'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'write'); - - await this.chatService.unreact(ps.messageId, me.id, ps.reaction); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts deleted file mode 100644 index a057e2e088..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'read:chat', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatMessageLiteFor1on1', - }, - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '11795c64-40ea-4198-b06e-3c873ed9039d', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatEntityService: ChatEntityService, - private chatService: ChatService, - private getterService: GetterService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'read'); - - const other = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw err; - }); - - const messages = await this.chatService.userTimeline(me.id, other.id, ps.limit, ps.sinceId, ps.untilId); - - this.chatService.readUserChatMessage(me.id, other.id); - - return await this.chatEntityService.packMessagesLiteFor1on1(messages); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/create.ts b/packages/backend/src/server/api/endpoints/chat/rooms/create.ts deleted file mode 100644 index 68a53f0886..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/rooms/create.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '@/server/api/error.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - prohibitMoved: true, - - kind: 'write:chat', - - limit: { - duration: ms('1day'), - max: 10, - }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatRoom', - }, - - errors: { - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - name: { type: 'string', maxLength: 256 }, - description: { type: 'string', maxLength: 1024 }, - }, - required: ['name'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - private chatEntityService: ChatEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'write'); - - const room = await this.chatService.createRoom(me, { - name: ps.name, - description: ps.description ?? '', - }); - return await this.chatEntityService.packRoom(room); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts b/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts deleted file mode 100644 index 1ea81448c1..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'write:chat', - - errors: { - noSuchRoom: { - message: 'No such room.', - code: 'NO_SUCH_ROOM', - id: 'd4e3753d-97bf-4a19-ab8e-21080fbc0f4b', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - roomId: { type: 'string', format: 'misskey:id' }, - }, - required: ['roomId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'write'); - - const room = await this.chatService.findRoomById(ps.roomId); - if (room == null) { - throw new ApiError(meta.errors.noSuchRoom); - } - - if (!await this.chatService.hasPermissionToDeleteRoom(me.id, room)) { - throw new ApiError(meta.errors.noSuchRoom); - } - - await this.chatService.deleteRoom(room, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts deleted file mode 100644 index b1f049f2b9..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '@/server/api/error.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - prohibitMoved: true, - - kind: 'write:chat', - - limit: { - duration: ms('1day'), - max: 50, - }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatRoomInvitation', - }, - - errors: { - noSuchRoom: { - message: 'No such room.', - code: 'NO_SUCH_ROOM', - id: '916f9507-49ba-4e90-b57f-1fd4deaa47a5', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - roomId: { type: 'string', format: 'misskey:id' }, - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['roomId', 'userId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - private chatEntityService: ChatEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'write'); - - const room = await this.chatService.findMyRoomById(me.id, ps.roomId); - if (room == null) { - throw new ApiError(meta.errors.noSuchRoom); - } - const invitation = await this.chatService.createRoomInvitation(me.id, room.id, ps.userId); - return await this.chatEntityService.packRoomInvitation(invitation, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts deleted file mode 100644 index 88ea234527..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'write:chat', - - errors: { - noSuchRoom: { - message: 'No such room.', - code: 'NO_SUCH_ROOM', - id: '5130557e-5a11-4cfb-9cc5-fe60cda5de0d', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - roomId: { type: 'string', format: 'misskey:id' }, - }, - required: ['roomId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'write'); - - await this.chatService.ignoreRoomInvitation(me.id, ps.roomId); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts deleted file mode 100644 index 8a02d1c704..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'read:chat', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatRoomInvitation', - }, - }, - - errors: { - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatEntityService: ChatEntityService, - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'read'); - - const invitations = await this.chatService.getReceivedRoomInvitationsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId); - return this.chatEntityService.packRoomInvitations(invitations, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts deleted file mode 100644 index 0702ba086c..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '@/server/api/error.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'read:chat', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatRoomInvitation', - }, - }, - - errors: { - noSuchRoom: { - message: 'No such room.', - code: 'NO_SUCH_ROOM', - id: 'a3c6b309-9717-4316-ae94-a69b53437237', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - roomId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, - required: ['roomId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - private chatEntityService: ChatEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'read'); - - const room = await this.chatService.findMyRoomById(me.id, ps.roomId); - if (room == null) { - throw new ApiError(meta.errors.noSuchRoom); - } - - const invitations = await this.chatService.getSentRoomInvitationsWithPagination(ps.roomId, ps.limit, ps.sinceId, ps.untilId); - return this.chatEntityService.packRoomInvitations(invitations, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/join.ts b/packages/backend/src/server/api/endpoints/chat/rooms/join.ts deleted file mode 100644 index 550b4da1a6..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/rooms/join.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'write:chat', - - errors: { - noSuchRoom: { - message: 'No such room.', - code: 'NO_SUCH_ROOM', - id: '84416476-5ce8-4a2c-b568-9569f1b10733', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - roomId: { type: 'string', format: 'misskey:id' }, - }, - required: ['roomId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'write'); - - await this.chatService.joinToRoom(me.id, ps.roomId); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts b/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts deleted file mode 100644 index ba9242c762..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'read:chat', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatRoomMembership', - }, - }, - - errors: { - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - private chatEntityService: ChatEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'read'); - - const memberships = await this.chatService.getMyMemberships(me.id, ps.limit, ps.sinceId, ps.untilId); - - return this.chatEntityService.packRoomMemberships(memberships, me, { - populateUser: false, - populateRoom: true, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts b/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts deleted file mode 100644 index f99b408d67..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'write:chat', - - errors: { - noSuchRoom: { - message: 'No such room.', - code: 'NO_SUCH_ROOM', - id: 'cb7f3179-50e8-4389-8c30-dbe2650a67c9', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - roomId: { type: 'string', format: 'misskey:id' }, - }, - required: ['roomId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'write'); - - await this.chatService.leaveRoom(me.id, ps.roomId); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/members.ts b/packages/backend/src/server/api/endpoints/chat/rooms/members.ts deleted file mode 100644 index f5ffa21d32..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/rooms/members.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ApiError } from '@/server/api/error.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'write:chat', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatRoomMembership', - }, - }, - - errors: { - noSuchRoom: { - message: 'No such room.', - code: 'NO_SUCH_ROOM', - id: '7b9fe84c-eafc-4d21-bf89-485458ed2c18', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - roomId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, - required: ['roomId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - private chatEntityService: ChatEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'read'); - - const room = await this.chatService.findRoomById(ps.roomId); - if (room == null) { - throw new ApiError(meta.errors.noSuchRoom); - } - - if (!(await this.chatService.isRoomMember(room, me.id))) { - throw new ApiError(meta.errors.noSuchRoom); - } - - const memberships = await this.chatService.getRoomMembershipsWithPagination(room.id, ps.limit, ps.sinceId, ps.untilId); - - return this.chatEntityService.packRoomMemberships(memberships, me, { - populateUser: true, - populateRoom: false, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts b/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts deleted file mode 100644 index ee60f92505..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'write:chat', - - errors: { - noSuchRoom: { - message: 'No such room.', - code: 'NO_SUCH_ROOM', - id: 'c2cde4eb-8d0f-42f1-8f2f-c4d6bfc8e5df', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - roomId: { type: 'string', format: 'misskey:id' }, - mute: { type: 'boolean' }, - }, - required: ['roomId', 'mute'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'write'); - - await this.chatService.muteRoom(me.id, ps.roomId, ps.mute); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts b/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts deleted file mode 100644 index accf7e1bee..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'read:chat', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatRoom', - }, - }, - - errors: { - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatEntityService: ChatEntityService, - private chatService: ChatService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'read'); - - const rooms = await this.chatService.getOwnedRoomsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId); - return this.chatEntityService.packRooms(rooms, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/show.ts b/packages/backend/src/server/api/endpoints/chat/rooms/show.ts deleted file mode 100644 index 50da210d81..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/rooms/show.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ApiError } from '@/server/api/error.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'read:chat', - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatRoom', - }, - - errors: { - noSuchRoom: { - message: 'No such room.', - code: 'NO_SUCH_ROOM', - id: '857ae02f-8759-4d20-9adb-6e95fffe4fd7', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - roomId: { type: 'string', format: 'misskey:id' }, - }, - required: ['roomId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - private chatEntityService: ChatEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'read'); - - const room = await this.chatService.findRoomById(ps.roomId); - if (room == null) { - throw new ApiError(meta.errors.noSuchRoom); - } - - return this.chatEntityService.packRoom(room, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/update.ts b/packages/backend/src/server/api/endpoints/chat/rooms/update.ts deleted file mode 100644 index 0cd62cb040..0000000000 --- a/packages/backend/src/server/api/endpoints/chat/rooms/update.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ChatService } from '@/core/ChatService.js'; -import { ApiError } from '@/server/api/error.js'; -import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; - -export const meta = { - tags: ['chat'], - - requireCredential: true, - - kind: 'write:chat', - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'ChatRoom', - }, - - errors: { - noSuchRoom: { - message: 'No such room.', - code: 'NO_SUCH_ROOM', - id: 'fcdb0f92-bda6-47f9-bd05-343e0e020932', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - roomId: { type: 'string', format: 'misskey:id' }, - name: { type: 'string', maxLength: 256 }, - description: { type: 'string', maxLength: 1024 }, - }, - required: ['roomId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private chatService: ChatService, - private chatEntityService: ChatEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.chatService.checkChatAvailability(me.id, 'write'); - - const room = await this.chatService.findMyRoomById(me.id, ps.roomId); - if (room == null) { - throw new ApiError(meta.errors.noSuchRoom); - } - - const updated = await this.chatService.updateRoom(room, { - name: ps.name, - description: ps.description, - }); - - return this.chatEntityService.packRoom(updated, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index d7c9ea3964..c3561e2a71 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -1,12 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ClipService } from '@/core/ClipService.js'; +import { IdService } from '@/core/IdService.js'; +import { DI } from '@/di-symbols.js'; +import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -59,27 +58,60 @@ export const paramDef = { required: ['clipId', 'noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private clipService: ClipService, + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.clipNotesRepository) + private clipNotesRepository: ClipNotesRepository, + + private idService: IdService, + private roleService: RoleService, + private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { - try { - await this.clipService.addNote(me, ps.clipId, ps.noteId); - } catch (e) { - if (e instanceof ClipService.NoSuchClipError) { - throw new ApiError(meta.errors.noSuchClip); - } else if (e instanceof ClipService.NoSuchNoteError) { - throw new ApiError(meta.errors.noSuchNote); - } else if (e instanceof ClipService.AlreadyAddedError) { - throw new ApiError(meta.errors.alreadyClipped); - } else if (e instanceof ClipService.TooManyClipNotesError) { - throw new ApiError(meta.errors.tooManyClipNotes); - } else { - throw e; - } + const clip = await this.clipsRepository.findOneBy({ + id: ps.clipId, + userId: me.id, + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); } + + const note = await this.getterService.getNote(ps.noteId).catch(e => { + if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw e; + }); + + const exist = await this.clipNotesRepository.findOneBy({ + noteId: note.id, + clipId: clip.id, + }); + + if (exist != null) { + throw new ApiError(meta.errors.alreadyClipped); + } + + const currentCount = await this.clipNotesRepository.countBy({ + clipId: clip.id, + }); + if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) { + throw new ApiError(meta.errors.tooManyClipNotes); + } + + await this.clipNotesRepository.insert({ + id: this.idService.genId(), + noteId: note.id, + clipId: clip.id, + }); + + await this.clipsRepository.update(clip.id, { + lastClippedAt: new Date(), + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts index b40706297d..5395a5c373 100644 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ b/packages/backend/src/server/api/endpoints/clips/create.ts @@ -1,14 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MiClip } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; +import type { ClipsRepository } from '@/models/index.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '@/server/api/error.js'; -import { ClipService } from '@/core/ClipService.js'; export const meta = { tags: ['clips'], @@ -39,29 +36,39 @@ export const paramDef = { properties: { name: { type: 'string', minLength: 1, maxLength: 100 }, isPublic: { type: 'boolean', default: false }, - description: { type: 'string', nullable: true, maxLength: 2048 }, + description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, }, required: ['name'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + private clipEntityService: ClipEntityService, - private clipService: ClipService, + private roleService: RoleService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - let clip: MiClip; - try { - // 空文字列をnullにしたいので??は使わない - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - clip = await this.clipService.create(me, ps.name, ps.isPublic, ps.description || null); - } catch (e) { - if (e instanceof ClipService.TooManyClipsError) { - throw new ApiError(meta.errors.tooManyClips); - } - throw e; + const currentCount = await this.clipsRepository.countBy({ + userId: me.id, + }); + if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) { + throw new ApiError(meta.errors.tooManyClips); } + + const clip = await this.clipsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + name: ps.name, + isPublic: ps.isPublic, + description: ps.description, + }).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0])); + return await this.clipEntityService.pack(clip, me); }); } diff --git a/packages/backend/src/server/api/endpoints/clips/delete.ts b/packages/backend/src/server/api/endpoints/clips/delete.ts index ca8ff2e1f1..077a9ec40f 100644 --- a/packages/backend/src/server/api/endpoints/clips/delete.ts +++ b/packages/backend/src/server/api/endpoints/clips/delete.ts @@ -1,11 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ClipService } from '@/core/ClipService.js'; +import type { ClipsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -32,20 +28,24 @@ export const paramDef = { required: ['clipId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private clipService: ClipService, + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, ) { super(meta, paramDef, async (ps, me) => { - try { - await this.clipService.delete(me, ps.clipId); - } catch (e) { - if (e instanceof ClipService.NoSuchClipError) { - throw new ApiError(meta.errors.noSuchClip); - } - throw e; + const clip = await this.clipsRepository.findOneBy({ + id: ps.clipId, + userId: me.id, + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); } + + await this.clipsRepository.delete(clip.id); }); } } diff --git a/packages/backend/src/server/api/endpoints/clips/favorite.ts b/packages/backend/src/server/api/endpoints/clips/favorite.ts index 11f8ec3e92..f08caaf8d7 100644 --- a/packages/backend/src/server/api/endpoints/clips/favorite.ts +++ b/packages/backend/src/server/api/endpoints/clips/favorite.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { ClipsRepository, ClipFavoritesRepository } from '@/models/_.js'; +import type { ClipsRepository, ClipFavoritesRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -42,8 +37,9 @@ export const paramDef = { required: ['clipId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, @@ -62,19 +58,18 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchClip); } - const exist = await this.clipFavoritesRepository.exists({ - where: { - clipId: clip.id, - userId: me.id, - }, + const exist = await this.clipFavoritesRepository.findOneBy({ + clipId: clip.id, + userId: me.id, }); - if (exist) { + if (exist != null) { throw new ApiError(meta.errors.alreadyFavorited); } await this.clipFavoritesRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), clipId: clip.id, userId: me.id, }); diff --git a/packages/backend/src/server/api/endpoints/clips/list.ts b/packages/backend/src/server/api/endpoints/clips/list.ts index 2e4a3ff820..3b8deab709 100644 --- a/packages/backend/src/server/api/endpoints/clips/list.ts +++ b/packages/backend/src/server/api/endpoints/clips/list.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ClipsRepository } from '@/models/_.js'; +import type { ClipsRepository } from '@/models/index.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -33,8 +28,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, diff --git a/packages/backend/src/server/api/endpoints/clips/my-favorites.ts b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts index 44719592d1..fc727e93bd 100644 --- a/packages/backend/src/server/api/endpoints/clips/my-favorites.ts +++ b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ClipFavoritesRepository } from '@/models/_.js'; +import type { ClipFavoritesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; @@ -34,8 +29,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.clipFavoritesRepository) private clipFavoritesRepository: ClipFavoritesRepository, diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts index 4869ffd402..dcb415b752 100644 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ b/packages/backend/src/server/api/endpoints/clips/notes.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NotesRepository, ClipsRepository, ClipNotesRepository } from '@/models/_.js'; +import type { NotesRepository, ClipsRepository, ClipNotesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -48,8 +43,9 @@ export const paramDef = { required: ['clipId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, @@ -85,18 +81,14 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .andWhere('clipNote.clipId = :clipId', { clipId: clip.id }); - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBlockedHostQueryForNote(query); - // this.queryService.generateSuspendedUserQueryForNote(query); // To avoid problems with removing notes, ignoring suspended user for now if (me) { - this.queryService.generateMutedUserQueryForNotes(query, me); - this.queryService.generateBlockedUserQueryForNotes(query, me); - this.queryService.generateMutedUserQueryForNotes(query, me, { noteColumn: 'renote' }); - this.queryService.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' }); + this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); } const notes = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.noteEntityService.packMany(notes, me); diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts index 33f9ecd25b..50c5d758bd 100644 --- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts @@ -1,12 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ClipService } from '@/core/ClipService.js'; +import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['account', 'notes', 'clips'], @@ -41,22 +38,37 @@ export const paramDef = { required: ['clipId', 'noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private clipService: ClipService, + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.clipNotesRepository) + private clipNotesRepository: ClipNotesRepository, + + private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { - try { - await this.clipService.removeNote(me, ps.clipId, ps.noteId); - } catch (e) { - if (e instanceof ClipService.NoSuchClipError) { - throw new ApiError(meta.errors.noSuchClip); - } else if (e instanceof ClipService.NoSuchNoteError) { - throw new ApiError(meta.errors.noSuchNote); - } - throw e; + const clip = await this.clipsRepository.findOneBy({ + id: ps.clipId, + userId: me.id, + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); } + + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + await this.clipNotesRepository.delete({ + noteId: note.id, + clipId: clip.id, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/clips/show.ts b/packages/backend/src/server/api/endpoints/clips/show.ts index 1078a1b176..99d630a9b5 100644 --- a/packages/backend/src/server/api/endpoints/clips/show.ts +++ b/packages/backend/src/server/api/endpoints/clips/show.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ClipsRepository } from '@/models/_.js'; +import type { ClipsRepository } from '@/models/index.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -40,8 +35,9 @@ export const paramDef = { required: ['clipId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, diff --git a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts index a458fda4a0..3da252a226 100644 --- a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { ClipsRepository, ClipFavoritesRepository } from '@/models/_.js'; +import type { ClipsRepository, ClipFavoritesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -41,8 +36,9 @@ export const paramDef = { required: ['clipId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts index 6ff3f9aada..70f1959353 100644 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ b/packages/backend/src/server/api/endpoints/clips/update.ts @@ -1,12 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { ClipsRepository } from '@/models/index.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; -import { ClipService } from '@/core/ClipService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -39,31 +35,38 @@ export const paramDef = { clipId: { type: 'string', format: 'misskey:id' }, name: { type: 'string', minLength: 1, maxLength: 100 }, isPublic: { type: 'boolean' }, - description: { type: 'string', nullable: true, maxLength: 2048 }, + description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, }, - required: ['clipId'], + required: ['clipId', 'name'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private clipService: ClipService, + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, private clipEntityService: ClipEntityService, ) { super(meta, paramDef, async (ps, me) => { - try { - // 空文字列をnullにしたいので??は使わない - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - await this.clipService.update(me, ps.clipId, ps.name, ps.isPublic, ps.description || null); - } catch (e) { - if (e instanceof ClipService.NoSuchClipError) { - throw new ApiError(meta.errors.noSuchClip); - } - throw e; + // Fetch the clip + const clip = await this.clipsRepository.findOneBy({ + id: ps.clipId, + userId: me.id, + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); } - return await this.clipEntityService.pack(ps.clipId, me); + await this.clipsRepository.update(clip.id, { + name: ps.name, + description: ps.description, + isPublic: ps.isPublic, + }); + + return await this.clipEntityService.pack(clip.id, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index eb45e29f9e..a6ece0311b 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -1,10 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { RoleService } from '@/core/RoleService.js'; @@ -37,13 +33,18 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + private metaService: MetaService, private driveFileEntityService: DriveFileEntityService, private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { + const instance = await this.metaService.fetch(true); + + // Calculate drive usage const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id); const policies = await this.roleService.getUserPolicies(me.id); diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts index 10c521332d..4609307774 100644 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -36,13 +31,14 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) }, - sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size', null] }, + sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size'] }, }, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -69,15 +65,15 @@ export default class extends Endpoint { // eslint- } switch (ps.sort) { - case '+createdAt': query.orderBy('file.id', 'DESC'); break; - case '-createdAt': query.orderBy('file.id', 'ASC'); break; + case '+createdAt': query.orderBy('file.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('file.createdAt', 'ASC'); break; case '+name': query.orderBy('file.name', 'DESC'); break; case '-name': query.orderBy('file.name', 'ASC'); break; case '+size': query.orderBy('file.size', 'DESC'); break; case '-size': query.orderBy('file.size', 'ASC'); break; } - const files = await query.limit(ps.limit).getMany(); + const files = await query.take(ps.limit).getMany(); return await this.driveFileEntityService.packMany(files, { detail: false, self: true }); }); diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index b86059b5e7..328d0e4643 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -1,16 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NotesRepository, DriveFilesRepository } from '@/models/_.js'; -import { QueryService } from '@/core/QueryService.js'; +import type { NotesRepository, DriveFilesRepository } from '@/models/index.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['drive', 'notes'], @@ -43,16 +36,14 @@ export const meta = { export const paramDef = { type: 'object', properties: { - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, fileId: { type: 'string', format: 'misskey:id' }, }, required: ['fileId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -61,24 +52,21 @@ export default class extends Endpoint { // eslint- private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, - private queryService: QueryService, - private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { // Fetch file const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId, - userId: await this.roleService.isModerator(me) ? undefined : me.id, + userId: me.id, }); if (file == null) { throw new ApiError(meta.errors.noSuchFile); } - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId); - query.andWhere(':file <@ note.fileIds', { file: [file.id] }); - - const notes = await query.limit(ps.limit).getMany(); + const notes = await this.notesRepository.createQueryBuilder('note') + .where(':file = ANY(note.fileIds)', { file: file.id }) + .getMany(); return await this.noteEntityService.packMany(notes, me, { detail: true, diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts index cc7920505f..290cd4d2ce 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -31,21 +26,20 @@ export const paramDef = { required: ['md5'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, ) { super(meta, paramDef, async (ps, me) => { - const exist = await this.driveFilesRepository.exists({ - where: { - md5: ps.md5, - userId: me.id, - }, + const file = await this.driveFilesRepository.findOneBy({ + md5: ps.md5, + userId: me.id, }); - return exist; + return file != null; }); } } diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index 7d5c0ccd4d..a1c1f9325e 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -1,16 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { MetaService } from '@/core/MetaService.js'; import { DriveService } from '@/core/DriveService.js'; -import { MiMeta } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -56,19 +52,6 @@ export const meta = { code: 'NO_FREE_SPACE', id: 'd08dbc37-a6a9-463a-8c47-96c32ab5f064', }, - - maxFileSizeExceeded: { - message: 'Cannot upload the file because it exceeds the maximum file size.', - code: 'MAX_FILE_SIZE_EXCEEDED', - id: 'b9d8c348-33f0-4673-b9a9-5d4da058977a', - httpStatusCode: 413, - }, - - unallowedFileType: { - message: 'Cannot upload the file because it is an unallowed file type.', - code: 'UNALLOWED_FILE_TYPE', - id: '4becd248-7f2c-48c4-a9f0-75edc4f9a1ea', - }, }, } as const; @@ -84,13 +67,15 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, private driveFileEntityService: DriveFileEntityService, + private metaService: MetaService, private driveService: DriveService, ) { super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => { @@ -107,6 +92,8 @@ export default class extends Endpoint { // eslint- } } + const instance = await this.metaService.fetch(); + try { // Create file const driveFile = await this.driveService.addFile({ @@ -117,8 +104,8 @@ export default class extends Endpoint { // eslint- folderId: ps.folderId, force: ps.force, sensitive: ps.isSensitive, - requestIp: this.serverSettings.enableIpLogging ? ip : null, - requestHeaders: this.serverSettings.enableIpLogging ? headers : null, + requestIp: instance.enableIpLogging ? ip : null, + requestHeaders: instance.enableIpLogging ? headers : null, }); return await this.driveFileEntityService.pack(driveFile, { self: true }); } catch (err) { @@ -128,8 +115,6 @@ export default class extends Endpoint { // eslint- if (err instanceof IdentifiableError) { if (err.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate); if (err.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace); - if (err.id === 'f9e4e5f3-4df4-40b5-b400-f236945f7073') throw new ApiError(meta.errors.maxFileSizeExceeded); - if (err.id === 'bd71c601-f9b0-4808-9137-a330647ced9b') throw new ApiError(meta.errors.unallowedFileType); } throw new ApiError(); } finally { diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts index fa6e11da49..2ced97ee02 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DriveService } from '@/core/DriveService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -44,8 +39,9 @@ export const paramDef = { required: ['fileId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -65,7 +61,11 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.accessDenied); } - await this.driveService.deleteFile(file, false, me); + // Delete + await this.driveService.deleteFile(file); + + // Publish fileDeleted event + this.globalEventService.publishDriveStream(me.id, 'fileDeleted', file.id); }); } } diff --git a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts index 090cff6875..d6d85f4e77 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -37,8 +32,9 @@ export const paramDef = { required: ['md5'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, diff --git a/packages/backend/src/server/api/endpoints/drive/files/find.ts b/packages/backend/src/server/api/endpoints/drive/files/find.ts index 502d42f9e0..858063eb4b 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -39,8 +34,9 @@ export const paramDef = { required: ['name'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -54,7 +50,7 @@ export default class extends Endpoint { // eslint- folderId: ps.folderId ?? IsNull(), }); - return await this.driveFileEntityService.packMany(files, { self: true }); + return await Promise.all(files.map(file => this.driveFileEntityService.pack(file, { self: true }))); }); } } diff --git a/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts b/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts deleted file mode 100644 index c8500895eb..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { DriveService } from '@/core/DriveService.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['drive'], - - requireCredential: true, - - kind: 'write:drive', - - errors: { - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 100, items: { type: 'string', format: 'misskey:id' } }, - folderId: { type: 'string', format: 'misskey:id', nullable: true }, - }, - required: ['fileIds'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private driveService: DriveService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.driveService.moveFiles(ps.fileIds, ps.folderId ?? null, me.id); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts index 9a2e2c73e8..271b33ef4b 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -43,26 +38,20 @@ export const meta = { } as const; export const paramDef = { + type: 'object', + properties: { + fileId: { type: 'string', format: 'misskey:id' }, + url: { type: 'string' }, + }, anyOf: [ - { - type: 'object', - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - }, - required: ['fileId'], - }, - { - type: 'object', - properties: { - url: { type: 'string' }, - }, - required: ['url'], - }, + { required: ['fileId'] }, + { required: ['url'] }, ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -71,11 +60,21 @@ export default class extends Endpoint { // eslint- private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const file = await this.driveFilesRepository.findOneBy( - 'fileId' in ps - ? { id: ps.fileId } - : [{ url: ps.url }, { webpublicUrl: ps.url }, { thumbnailUrl: ps.url }], - ); + let file: DriveFile | null = null; + + if (ps.fileId) { + file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + } else if (ps.url) { + file = await this.driveFilesRepository.findOne({ + where: [{ + url: ps.url, + }, { + webpublicUrl: ps.url, + }, { + thumbnailUrl: ps.url, + }], + }); + } if (file == null) { throw new ApiError(meta.errors.noSuchFile); diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index df1622cce0..3ecbba22b5 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -1,14 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; -import { DriveService } from '@/core/DriveService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -44,7 +40,7 @@ export const meta = { code: 'NO_SUCH_FOLDER', id: 'ea8fb7a5-af77-4a08-b608-c0218176cd73', }, - + restrictedByRole: { message: 'This feature is restricted by your role.', code: 'RESTRICTED_BY_ROLE', @@ -70,17 +66,23 @@ export const paramDef = { required: ['fileId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - private driveService: DriveService, + @Inject(DI.driveFoldersRepository) + private driveFoldersRepository: DriveFoldersRepository, + + private driveFileEntityService: DriveFileEntityService, private roleService: RoleService, + private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + const alwaysMarkNsfw = (await this.roleService.getUserPolicies(me.id)).alwaysMarkNsfw; if (file == null) { throw new ApiError(meta.errors.noSuchFile); } @@ -89,28 +91,49 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.accessDenied); } - let packedFile; + if (ps.name) file.name = ps.name; + if (!this.driveFileEntityService.validateFileName(file.name)) { + throw new ApiError(meta.errors.invalidFileName); + } - try { - packedFile = await this.driveService.updateFile(file, { - folderId: ps.folderId, - name: ps.name, - isSensitive: ps.isSensitive, - comment: ps.comment, - }, me); - } catch (e) { - if (e instanceof DriveService.InvalidFileNameError) { - throw new ApiError(meta.errors.invalidFileName); - } else if (e instanceof DriveService.NoSuchFolderError) { - throw new ApiError(meta.errors.noSuchFolder); - } else if (e instanceof DriveService.CannotUnmarkSensitiveError) { - throw new ApiError(meta.errors.restrictedByRole); + if (ps.comment !== undefined) file.comment = ps.comment; + + if (ps.isSensitive !== undefined && ps.isSensitive !== file.isSensitive && alwaysMarkNsfw && !ps.isSensitive) { + throw new ApiError(meta.errors.restrictedByRole); + } + + if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive; + + if (ps.folderId !== undefined) { + if (ps.folderId === null) { + file.folderId = null; } else { - throw e; + const folder = await this.driveFoldersRepository.findOneBy({ + id: ps.folderId, + userId: me.id, + }); + + if (folder == null) { + throw new ApiError(meta.errors.noSuchFolder); + } + + file.folderId = folder.id; } } - return packedFile; + await this.driveFilesRepository.update(file.id, { + name: file.name, + comment: file.comment, + folderId: file.folderId, + isSensitive: file.isSensitive, + }); + + const fileObj = await this.driveFileEntityService.pack(file, { self: true }); + + // Publish fileUpdated event + this.globalEventService.publishDriveStream(me.id, 'fileUpdated', fileObj); + + return fileObj; }); } } diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index b964ae95b8..c835587c4a 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -1,14 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import type { DriveFilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveService } from '@/core/DriveService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['drive'], @@ -40,9 +37,13 @@ export const paramDef = { required: ['url'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + private driveFileEntityService: DriveFileEntityService, private driveService: DriveService, private globalEventService: GlobalEventService, diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts index 8c4848f8e1..b41eaf4463 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFoldersRepository } from '@/models/_.js'; +import type { DriveFoldersRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -39,8 +34,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFoldersRepository) private driveFoldersRepository: DriveFoldersRepository, @@ -58,7 +54,7 @@ export default class extends Endpoint { // eslint- query.andWhere('folder.parentId IS NULL'); } - const folders = await query.limit(ps.limit).getMany(); + const folders = await query.take(ps.limit).getMany(); return await Promise.all(folders.map(folder => this.driveFolderEntityService.pack(folder))); }); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/create.ts b/packages/backend/src/server/api/endpoints/drive/folders/create.ts index 08d9d9cdc3..39c9c6bc58 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/create.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFoldersRepository } from '@/models/_.js'; +import type { DriveFoldersRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -49,8 +44,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFoldersRepository) private driveFoldersRepository: DriveFoldersRepository, @@ -75,12 +71,13 @@ export default class extends Endpoint { // eslint- } // Create folder - const folder = await this.driveFoldersRepository.insertOne({ - id: this.idService.gen(), + const folder = await this.driveFoldersRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), name: ps.name, parentId: parent !== null ? parent.id : null, userId: me.id, - }); + }).then(x => this.driveFoldersRepository.findOneByOrFail(x.identifiers[0])); const folderObj = await this.driveFolderEntityService.pack(folder); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts index 85d63873a4..d921bc1b17 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFoldersRepository, DriveFilesRepository } from '@/models/_.js'; +import type { DriveFoldersRepository, DriveFilesRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -40,8 +35,9 @@ export const paramDef = { required: ['folderId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, diff --git a/packages/backend/src/server/api/endpoints/drive/folders/find.ts b/packages/backend/src/server/api/endpoints/drive/folders/find.ts index eb45a30bc0..ee24db11f2 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/find.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFoldersRepository } from '@/models/_.js'; +import type { DriveFoldersRepository } from '@/models/index.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -37,8 +32,9 @@ export const paramDef = { required: ['name'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFoldersRepository) private driveFoldersRepository: DriveFoldersRepository, diff --git a/packages/backend/src/server/api/endpoints/drive/folders/show.ts b/packages/backend/src/server/api/endpoints/drive/folders/show.ts index a1c0df6697..c06263b902 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/show.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFoldersRepository } from '@/models/_.js'; +import type { DriveFoldersRepository } from '@/models/index.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -40,8 +35,9 @@ export const paramDef = { required: ['folderId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFoldersRepository) private driveFoldersRepository: DriveFoldersRepository, diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index 62b04e1df3..ff0a78b929 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFoldersRepository } from '@/models/_.js'; +import type { DriveFoldersRepository } from '@/models/index.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -55,8 +50,9 @@ export const paramDef = { required: ['folderId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFoldersRepository) private driveFoldersRepository: DriveFoldersRepository, @@ -95,14 +91,15 @@ export default class extends Endpoint { // eslint- // Check if the circular reference will occur const checkCircle = async (folderId: string): Promise => { - const folder2 = await this.driveFoldersRepository.findOneByOrFail({ + // Fetch folder + const folder2 = await this.driveFoldersRepository.findOneBy({ id: folderId, }); - if (folder2.id === folder.id) { + if (folder2!.id === folder!.id) { return true; - } else if (folder2.parentId) { - return await checkCircle(folder2.parentId); + } else if (folder2!.parentId) { + return await checkCircle(folder2!.parentId); } else { return false; } diff --git a/packages/backend/src/server/api/endpoints/drive/stream.ts b/packages/backend/src/server/api/endpoints/drive/stream.ts index f7c1ed39b5..61bcfea0c3 100644 --- a/packages/backend/src/server/api/endpoints/drive/stream.ts +++ b/packages/backend/src/server/api/endpoints/drive/stream.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -39,8 +34,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -60,7 +56,7 @@ export default class extends Endpoint { // eslint- } } - const files = await query.limit(ps.limit).getMany(); + const files = await query.take(ps.limit).getMany(); return await this.driveFileEntityService.packMany(files, { detail: false, self: true }); }); diff --git a/packages/backend/src/server/api/endpoints/email-address/available.ts b/packages/backend/src/server/api/endpoints/email-address/available.ts index 1d7dacd60e..0f13b14d01 100644 --- a/packages/backend/src/server/api/endpoints/email-address/available.ts +++ b/packages/backend/src/server/api/endpoints/email-address/available.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { EmailService } from '@/core/EmailService.js'; @@ -36,8 +31,9 @@ export const paramDef = { required: ['emailAddress'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private emailService: EmailService, ) { diff --git a/packages/backend/src/server/api/endpoints/emoji.ts b/packages/backend/src/server/api/endpoints/emoji.ts index ccfbda0d44..681d3e649e 100644 --- a/packages/backend/src/server/api/endpoints/emoji.ts +++ b/packages/backend/src/server/api/endpoints/emoji.ts @@ -1,13 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { EmojisRepository } from '@/models/_.js'; +import type { EmojisRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -34,9 +30,13 @@ export const paramDef = { required: ['name'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts index 46ef4eca1b..13cc709d31 100644 --- a/packages/backend/src/server/api/endpoints/emojis.ts +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -1,13 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { EmojisRepository } from '@/models/_.js'; +import type { EmojisRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -41,9 +37,13 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts index fe7e9c36f3..b38c97f60a 100644 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ b/packages/backend/src/server/api/endpoints/endpoint.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import endpoints from '../endpoints.js'; @@ -11,23 +6,6 @@ export const meta = { requireCredential: false, tags: ['meta'], - - res: { - type: 'object', - nullable: true, - properties: { - params: { - type: 'array', - items: { - type: 'object', - properties: { - name: { type: 'string' }, - type: { type: 'string' }, - }, - }, - }, - }, - }, } as const; export const paramDef = { @@ -38,8 +16,9 @@ export const paramDef = { required: ['endpoint'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( ) { super(meta, paramDef, async (ps) => { diff --git a/packages/backend/src/server/api/endpoints/endpoints.ts b/packages/backend/src/server/api/endpoints/endpoints.ts index 4aedf62a84..9e706db747 100644 --- a/packages/backend/src/server/api/endpoints/endpoints.ts +++ b/packages/backend/src/server/api/endpoints/endpoints.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import endpoints from '../endpoints.js'; @@ -34,8 +29,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( ) { super(meta, paramDef, async () => { diff --git a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts index 5ff099524d..6b6079ad51 100644 --- a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts +++ b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -23,8 +18,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private queueService: QueueService, ) { diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts index ce4dd13067..be1d6c8e58 100644 --- a/packages/backend/src/server/api/endpoints/federation/followers.ts +++ b/packages/backend/src/server/api/endpoints/federation/followers.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository } from '@/models/_.js'; +import type { FollowingsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -37,8 +32,9 @@ export const paramDef = { required: ['host'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @@ -51,7 +47,7 @@ export default class extends Endpoint { // eslint- .andWhere('following.followeeHost = :host', { host: ps.host }); const followings = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.followingEntityService.packMany(followings, me, { populateFollowee: true }); diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts index 1a793889c7..74656ce863 100644 --- a/packages/backend/src/server/api/endpoints/federation/following.ts +++ b/packages/backend/src/server/api/endpoints/federation/following.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository } from '@/models/_.js'; +import type { FollowingsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -37,8 +32,9 @@ export const paramDef = { required: ['host'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @@ -51,7 +47,7 @@ export default class extends Endpoint { // eslint- .andWhere('following.followerHost = :host', { host: ps.host }); const followings = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.followingEntityService.packMany(followings, me, { populateFollowee: true }); diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 41954129e6..061c6eb5be 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { InstancesRepository } from '@/models/_.js'; +import type { InstancesRepository } from '@/models/index.js'; import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; @@ -36,39 +31,19 @@ export const paramDef = { blocked: { type: 'boolean', nullable: true }, notResponding: { type: 'boolean', nullable: true }, suspended: { type: 'boolean', nullable: true }, - silenced: { type: 'boolean', nullable: true }, federating: { type: 'boolean', nullable: true }, subscribing: { type: 'boolean', nullable: true }, publishing: { type: 'boolean', nullable: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, offset: { type: 'integer', default: 0 }, - sort: { - type: 'string', - nullable: true, - enum: [ - '+pubSub', - '-pubSub', - '+notes', - '-notes', - '+users', - '-users', - '+following', - '-following', - '+followers', - '-followers', - '+firstRetrievedAt', - '-firstRetrievedAt', - '+latestRequestReceivedAt', - '-latestRequestReceivedAt', - null, - ], - }, + sort: { type: 'string' }, }, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, @@ -117,26 +92,9 @@ export default class extends Endpoint { // eslint- if (typeof ps.suspended === 'boolean') { if (ps.suspended) { - query.andWhere('instance.suspensionState != \'none\''); + query.andWhere('instance.isSuspended = TRUE'); } else { - query.andWhere('instance.suspensionState = \'none\''); - } - } - - if (typeof ps.silenced === 'boolean') { - const meta = await this.metaService.fetch(true); - - if (ps.silenced) { - if (meta.silencedHosts.length === 0) { - return []; - } - query.andWhere('instance.host IN (:...silences)', { - silences: meta.silencedHosts, - }); - } else if (meta.silencedHosts.length > 0) { - query.andWhere('instance.host NOT IN (:...silences)', { - silences: meta.silencedHosts, - }); + query.andWhere('instance.isSuspended = FALSE'); } } @@ -168,9 +126,9 @@ export default class extends Endpoint { // eslint- query.andWhere('instance.host like :host', { host: '%' + sqlLikeEscape(ps.host.toLowerCase()) + '%' }); } - const instances = await query.limit(ps.limit).offset(ps.offset).getMany(); + const instances = await query.take(ps.limit).skip(ps.offset).getMany(); - return await this.instanceEntityService.packMany(instances, me); + return await this.instanceEntityService.packMany(instances); }); } } diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts index 2972861a4b..66502748b3 100644 --- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts +++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { InstancesRepository } from '@/models/_.js'; +import type { InstancesRepository } from '@/models/index.js'; import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; @@ -16,9 +11,12 @@ export const meta = { requireCredential: false, res: { - type: 'object', - optional: false, nullable: true, - ref: 'FederationInstance', + oneOf: [{ + type: 'object', + ref: 'FederationInstance', + }, { + type: 'null', + }], }, } as const; @@ -30,8 +28,9 @@ export const paramDef = { required: ['host'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, @@ -43,7 +42,7 @@ export default class extends Endpoint { // eslint- const instance = await this.instancesRepository .findOneBy({ host: this.utilityService.toPuny(ps.host) }); - return instance ? await this.instanceEntityService.pack(instance, me) : null; + return instance ? await this.instanceEntityService.pack(instance) : null; }); } } diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts index 69900bff9a..19418e698c 100644 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ b/packages/backend/src/server/api/endpoints/federation/stats.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { IsNull, MoreThan, Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { FollowingsRepository, InstancesRepository } from '@/models/_.js'; +import type { FollowingsRepository, InstancesRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; @@ -18,38 +13,6 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, - - res: { - type: 'object', - optional: false, - nullable: false, - properties: { - topSubInstances: { - type: 'array', - optional: false, - nullable: false, - items: { - type: 'object', - optional: false, - nullable: false, - ref: 'FederationInstance', - }, - }, - otherFollowersCount: { type: 'number' }, - topPubInstances: { - type: 'array', - optional: false, - nullable: false, - items: { - type: 'object', - optional: false, - nullable: false, - ref: 'FederationInstance', - }, - }, - otherFollowingCount: { type: 'number' }, - }, - }, } as const; export const paramDef = { @@ -60,8 +23,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, @@ -107,9 +71,9 @@ export default class extends Endpoint { // eslint- const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0); return await awaitAll({ - topSubInstances: this.instanceEntityService.packMany(topSubInstances, me), + topSubInstances: this.instanceEntityService.packMany(topSubInstances), otherFollowersCount: Math.max(0, allSubCount - gotSubCount), - topPubInstances: this.instanceEntityService.packMany(topPubInstances, me), + topPubInstances: this.instanceEntityService.packMany(topPubInstances), otherFollowingCount: Math.max(0, allPubCount - gotPubCount), }); }); diff --git a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts index f8430ef431..4596e0c0b5 100644 --- a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts +++ b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; @@ -11,7 +6,7 @@ import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['federation'], - requireCredential: false, + requireCredential: true, } as const; export const paramDef = { @@ -22,8 +17,9 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private getterService: GetterService, private apPersonService: ApPersonService, diff --git a/packages/backend/src/server/api/endpoints/federation/users.ts b/packages/backend/src/server/api/endpoints/federation/users.ts index 71b1aeb07b..a028930f21 100644 --- a/packages/backend/src/server/api/endpoints/federation/users.ts +++ b/packages/backend/src/server/api/endpoints/federation/users.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/_.js'; +import type { UsersRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -37,8 +32,9 @@ export const paramDef = { required: ['host'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -51,10 +47,10 @@ export default class extends Endpoint { // eslint- .andWhere('user.host = :host', { host: ps.host }); const users = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); - return await this.userEntityService.packMany(users, me, { schema: 'UserDetailedNotMe' }); + return await this.userEntityService.packMany(users, me, { detail: true }); }); } } diff --git a/packages/backend/src/server/api/endpoints/fetch-external-resources.ts b/packages/backend/src/server/api/endpoints/fetch-external-resources.ts deleted file mode 100644 index f36136d53b..0000000000 --- a/packages/backend/src/server/api/endpoints/fetch-external-resources.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { createHash } from 'crypto'; -import ms from 'ms'; -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { HttpRequestService } from '@/core/HttpRequestService.js'; -import { ApiError } from '../error.js'; - -export const meta = { - tags: ['meta'], - - requireCredential: true, - secure: true, - - limit: { - duration: ms('1hour'), - max: 50, - }, - - errors: { - invalidSchema: { - message: 'External resource returned invalid schema.', - code: 'EXT_RESOURCE_RETURNED_INVALID_SCHEMA', - id: 'bb774091-7a15-4a70-9dc5-6ac8cf125856', - }, - hashUnmached: { - message: 'Hash did not match.', - code: 'EXT_RESOURCE_HASH_DIDNT_MATCH', - id: '693ba8ba-b486-40df-a174-72f8279b56a4', - }, - }, - - res: { - type: 'object', - properties: { - type: { - type: 'string', - }, - data: { - type: 'string', - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - url: { type: 'string' }, - hash: { type: 'string' }, - }, - required: ['url', 'hash'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private httpRequestService: HttpRequestService, - ) { - super(meta, paramDef, async (ps) => { - const res = await this.httpRequestService.getJson<{ - type: string; - data: string; - }>(ps.url); - - if (!res.data || !res.type) { - throw new ApiError(meta.errors.invalidSchema); - } - - const resHash = createHash('sha512').update(res.data.replace(/\r\n/g, '\n')).digest('hex'); - if (resHash !== ps.hash) { - throw new ApiError(meta.errors.hashUnmached); - } - - return { - type: res.type, - data: res.data, - }; - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index ba48b0119e..5849d3111f 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -1,11 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Parser from 'rss-parser'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; const rssParser = new Parser(); @@ -16,193 +13,6 @@ export const meta = { requireCredential: false, allowGet: true, cacheSec: 60 * 3, - - res: { - type: 'object', - properties: { - image: { - type: 'object', - optional: true, - properties: { - link: { - type: 'string', - optional: true, - }, - url: { - type: 'string', - optional: false, - }, - title: { - type: 'string', - optional: true, - }, - }, - }, - paginationLinks: { - type: 'object', - optional: true, - properties: { - self: { - type: 'string', - optional: true, - }, - first: { - type: 'string', - optional: true, - }, - next: { - type: 'string', - optional: true, - }, - last: { - type: 'string', - optional: true, - }, - prev: { - type: 'string', - optional: true, - }, - }, - }, - link: { - type: 'string', - optional: true, - }, - title: { - type: 'string', - optional: true, - }, - items: { - type: 'array', - optional: false, - items: { - type: 'object', - properties: { - link: { - type: 'string', - optional: true, - }, - guid: { - type: 'string', - optional: true, - }, - title: { - type: 'string', - optional: true, - }, - pubDate: { - type: 'string', - optional: true, - }, - creator: { - type: 'string', - optional: true, - }, - summary: { - type: 'string', - optional: true, - }, - content: { - type: 'string', - optional: true, - }, - isoDate: { - type: 'string', - optional: true, - }, - categories: { - type: 'array', - optional: true, - items: { - type: 'string', - }, - }, - contentSnippet: { - type: 'string', - optional: true, - }, - enclosure: { - type: 'object', - optional: true, - properties: { - url: { - type: 'string', - optional: false, - }, - length: { - type: 'number', - optional: true, - }, - type: { - type: 'string', - optional: true, - }, - }, - }, - }, - }, - }, - feedUrl: { - type: 'string', - optional: true, - }, - description: { - type: 'string', - optional: true, - }, - itunes: { - type: 'object', - optional: true, - additionalProperties: true, - properties: { - image: { - type: 'string', - optional: true, - }, - owner: { - type: 'object', - optional: true, - properties: { - name: { - type: 'string', - optional: true, - }, - email: { - type: 'string', - optional: true, - }, - }, - }, - author: { - type: 'string', - optional: true, - }, - summary: { - type: 'string', - optional: true, - }, - explicit: { - type: 'string', - optional: true, - }, - categories: { - type: 'array', - optional: true, - items: { - type: 'string', - }, - }, - keywords: { - type: 'array', - optional: true, - items: { - type: 'string', - }, - }, - }, - }, - }, - }, } as const; export const paramDef = { @@ -213,9 +23,13 @@ export const paramDef = { required: ['url'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.config) + private config: Config, + private httpRequestService: HttpRequestService, ) { super(meta, paramDef, async (ps, me) => { diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts index 64f13a577e..3172bdbfda 100644 --- a/packages/backend/src/server/api/endpoints/flash/create.ts +++ b/packages/backend/src/server/api/endpoints/flash/create.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { FlashsRepository } from '@/models/_.js'; +import type { FlashsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -27,12 +22,6 @@ export const meta = { errors: { }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'Flash', - }, } as const; export const paramDef = { @@ -44,13 +33,13 @@ export const paramDef = { permissions: { type: 'array', items: { type: 'string', } }, - visibility: { type: 'string', enum: ['public', 'private'], default: 'public' }, }, required: ['title', 'summary', 'script', 'permissions'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, @@ -59,16 +48,16 @@ export default class extends Endpoint { // eslint- private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - const flash = await this.flashsRepository.insertOne({ - id: this.idService.gen(), + const flash = await this.flashsRepository.insert({ + id: this.idService.genId(), userId: me.id, + createdAt: new Date(), updatedAt: new Date(), title: ps.title, summary: ps.summary, script: ps.script, permissions: ps.permissions, - visibility: ps.visibility, - }); + }).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0])); return await this.flashEntityService.pack(flash); }); diff --git a/packages/backend/src/server/api/endpoints/flash/delete.ts b/packages/backend/src/server/api/endpoints/flash/delete.ts index 6912450abf..e94ede9f68 100644 --- a/packages/backend/src/server/api/endpoints/flash/delete.ts +++ b/packages/backend/src/server/api/endpoints/flash/delete.ts @@ -1,14 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { FlashsRepository, UsersRepository } from '@/models/_.js'; +import type { FlashsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -41,40 +34,23 @@ export const paramDef = { required: ['flashId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private moderationLogService: ModerationLogService, - private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const flash = await this.flashsRepository.findOneBy({ id: ps.flashId }); - if (flash == null) { throw new ApiError(meta.errors.noSuchFlash); } - - if (!await this.roleService.isModerator(me) && flash.userId !== me.id) { + if (flash.userId !== me.id) { throw new ApiError(meta.errors.accessDenied); } await this.flashsRepository.delete(flash.id); - - if (flash.userId !== me.id) { - const user = await this.usersRepository.findOneByOrFail({ id: flash.userId }); - this.moderationLogService.log(me, 'deleteFlash', { - flashId: flash.id, - flashUserId: flash.userId, - flashUserUsername: user.username, - flash, - }); - } }); } } diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts index 9a0cb461f2..570aef96d2 100644 --- a/packages/backend/src/server/api/endpoints/flash/featured.ts +++ b/packages/backend/src/server/api/endpoints/flash/featured.ts @@ -1,14 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { FlashsRepository } from '@/models/_.js'; +import type { FlashsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { DI } from '@/di-symbols.js'; -import { FlashService } from '@/core/FlashService.js'; export const meta = { tags: ['flash'], @@ -28,25 +22,27 @@ export const meta = { export const paramDef = { type: 'object', - properties: { - offset: { type: 'integer', minimum: 0, default: 0 }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - }, + properties: {}, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private flashService: FlashService, + @Inject(DI.flashsRepository) + private flashsRepository: FlashsRepository, + private flashEntityService: FlashEntityService, ) { super(meta, paramDef, async (ps, me) => { - const result = await this.flashService.featured({ - offset: ps.offset, - limit: ps.limit, - }); - return await this.flashEntityService.packMany(result, me); + const query = this.flashsRepository.createQueryBuilder('flash') + .andWhere('flash.likedCount > 0') + .orderBy('flash.likedCount', 'DESC'); + + const flashs = await query.take(10).getMany(); + + return await this.flashEntityService.packMany(flashs, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/flash/like.ts b/packages/backend/src/server/api/endpoints/flash/like.ts index e4dc5b61c5..23de2f3970 100644 --- a/packages/backend/src/server/api/endpoints/flash/like.ts +++ b/packages/backend/src/server/api/endpoints/flash/like.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js'; +import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -48,8 +43,9 @@ export const paramDef = { required: ['flashId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, @@ -70,20 +66,19 @@ export default class extends Endpoint { // eslint- } // if already liked - const exist = await this.flashLikesRepository.exists({ - where: { - flashId: flash.id, - userId: me.id, - }, + const exist = await this.flashLikesRepository.findOneBy({ + flashId: flash.id, + userId: me.id, }); - if (exist) { + if (exist != null) { throw new ApiError(meta.errors.alreadyLiked); } // Create like await this.flashLikesRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), flashId: flash.id, userId: me.id, }); diff --git a/packages/backend/src/server/api/endpoints/flash/my-likes.ts b/packages/backend/src/server/api/endpoints/flash/my-likes.ts index 755cc5acfc..f7716ea74a 100644 --- a/packages/backend/src/server/api/endpoints/flash/my-likes.ts +++ b/packages/backend/src/server/api/endpoints/flash/my-likes.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FlashLikesRepository } from '@/models/_.js'; +import type { FlashLikesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { FlashLikeEntityService } from '@/core/entities/FlashLikeEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -48,8 +43,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.flashLikesRepository) private flashLikesRepository: FlashLikesRepository, @@ -63,7 +59,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('like.flash', 'flash'); const likes = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return this.flashLikeEntityService.packMany(likes, me); diff --git a/packages/backend/src/server/api/endpoints/flash/my.ts b/packages/backend/src/server/api/endpoints/flash/my.ts index 5746096232..baed7f000f 100644 --- a/packages/backend/src/server/api/endpoints/flash/my.ts +++ b/packages/backend/src/server/api/endpoints/flash/my.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FlashsRepository } from '@/models/_.js'; +import type { FlashsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -38,8 +33,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, @@ -52,7 +48,7 @@ export default class extends Endpoint { // eslint- .andWhere('flash.userId = :meId', { meId: me.id }); const flashs = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.flashEntityService.packMany(flashs); diff --git a/packages/backend/src/server/api/endpoints/flash/show.ts b/packages/backend/src/server/api/endpoints/flash/show.ts index a6fbd8e76e..14720a8c8d 100644 --- a/packages/backend/src/server/api/endpoints/flash/show.ts +++ b/packages/backend/src/server/api/endpoints/flash/show.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { FlashsRepository } from '@/models/_.js'; +import type { UsersRepository, FlashsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -38,9 +33,13 @@ export const paramDef = { required: ['flashId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, diff --git a/packages/backend/src/server/api/endpoints/flash/unlike.ts b/packages/backend/src/server/api/endpoints/flash/unlike.ts index 7869bcdf52..696512b06c 100644 --- a/packages/backend/src/server/api/endpoints/flash/unlike.ts +++ b/packages/backend/src/server/api/endpoints/flash/unlike.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js'; +import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -41,8 +36,9 @@ export const paramDef = { required: ['flashId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, diff --git a/packages/backend/src/server/api/endpoints/flash/update.ts b/packages/backend/src/server/api/endpoints/flash/update.ts index e378669f0a..78dfd4a06a 100644 --- a/packages/backend/src/server/api/endpoints/flash/update.ts +++ b/packages/backend/src/server/api/endpoints/flash/update.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { FlashsRepository } from '@/models/_.js'; +import type { FlashsRepository, DriveFilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -49,16 +44,19 @@ export const paramDef = { permissions: { type: 'array', items: { type: 'string', } }, - visibility: { type: 'string', enum: ['public', 'private'] }, }, - required: ['flashId'], + required: ['flashId', 'title', 'summary', 'script', 'permissions'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, ) { super(meta, paramDef, async (ps, me) => { const flash = await this.flashsRepository.findOneBy({ id: ps.flashId }); @@ -71,11 +69,10 @@ export default class extends Endpoint { // eslint- await this.flashsRepository.update(flash.id, { updatedAt: new Date(), - ...Object.fromEntries( - Object.entries(ps).filter( - ([key, val]) => (key !== 'flashId') && Object.hasOwn(paramDef.properties, key) - ) - ), + title: ps.title, + summary: ps.summary, + script: ps.script, + permissions: ps.permissions, }); }); } diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index db320e7129..4ad16de911 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository } from '@/models/_.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; @@ -19,7 +14,7 @@ export const meta = { limit: { duration: ms('1hour'), - max: 100, + max: 50, }, requireCredential: true, @@ -71,14 +66,17 @@ export const paramDef = { type: 'object', properties: { userId: { type: 'string', format: 'misskey:id' }, - withReplies: { type: 'boolean' }, }, required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @@ -100,11 +98,20 @@ export default class extends Endpoint { // eslint- throw err; }); + // Check if already following + const exist = await this.followingsRepository.findOneBy({ + followerId: follower.id, + followeeId: followee.id, + }); + + if (exist != null) { + throw new ApiError(meta.errors.alreadyFollowing); + } + try { - await this.userFollowingService.follow(follower, followee, { withReplies: ps.withReplies }); + await this.userFollowingService.follow(follower, followee); } catch (e) { if (e instanceof IdentifiableError) { - if (e.id === 'ec3f65c0-a9d1-47d9-8791-b2e7b9dcdced') throw new ApiError(meta.errors.alreadyFollowing); if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking); if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked); } diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts index ba146b6703..4f12db1273 100644 --- a/packages/backend/src/server/api/endpoints/following/delete.ts +++ b/packages/backend/src/server/api/endpoints/following/delete.ts @@ -1,17 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository } from '@/models/_.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; -import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['following', 'users'], @@ -60,9 +55,13 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @@ -85,14 +84,12 @@ export default class extends Endpoint { // eslint- }); // Check not following - const exist = await this.followingsRepository.exists({ - where: { - followerId: follower.id, - followeeId: followee.id, - }, + const exist = await this.followingsRepository.findOneBy({ + followerId: follower.id, + followeeId: followee.id, }); - if (!exist) { + if (exist == null) { throw new ApiError(meta.errors.notFollowing); } diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts index b45d21410b..22304cacda 100644 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -1,17 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository } from '@/models/_.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; -import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['following', 'users'], @@ -29,7 +24,7 @@ export const meta = { noSuchUser: { message: 'No such user.', code: 'NO_SUCH_USER', - id: 'b77e6ae6-a3e5-40da-9cc8-c240115479cc', + id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8', }, followerIsYourself: { @@ -41,7 +36,7 @@ export const meta = { notFollowing: { message: 'The other use is not following you.', code: 'NOT_FOLLOWING', - id: '918faac3-074f-41ae-9c43-ed5d2946770d', + id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09', }, }, @@ -60,9 +55,13 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @@ -96,7 +95,7 @@ export default class extends Endpoint { // eslint- await this.userFollowingService.unfollow(follower, followee); - return await this.userEntityService.pack(follower.id, me); + return await this.userEntityService.pack(followee.id, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/following/requests/accept.ts b/packages/backend/src/server/api/endpoints/following/requests/accept.ts index 2d1446681c..cca3e60614 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/accept.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/accept.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -38,8 +33,9 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private getterService: GetterService, private userFollowingService: UserFollowingService, diff --git a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts index 6d663d480c..7325e73cac 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts @@ -1,14 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { FollowingsRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -47,9 +44,13 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + private userEntityService: UserEntityService, private getterService: GetterService, private userFollowingService: UserFollowingService, diff --git a/packages/backend/src/server/api/endpoints/following/requests/list.ts b/packages/backend/src/server/api/endpoints/following/requests/list.ts index fa59e38976..d68248fab9 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/list.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; -import type { FollowRequestsRepository } from '@/models/_.js'; +import type { FollowRequestsRepository } from '@/models/index.js'; import { FollowRequestEntityService } from '@/core/entities/FollowRequestEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -54,8 +49,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, @@ -68,10 +64,10 @@ export default class extends Endpoint { // eslint- .andWhere('request.followeeId = :meId', { meId: me.id }); const requests = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); - return await this.followRequestEntityService.packMany(requests, me); + return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req))); }); } } diff --git a/packages/backend/src/server/api/endpoints/following/requests/reject.ts b/packages/backend/src/server/api/endpoints/following/requests/reject.ts index 4f78eae677..a8fdc44876 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/reject.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -33,8 +28,9 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private getterService: GetterService, private userFollowingService: UserFollowingService, diff --git a/packages/backend/src/server/api/endpoints/following/requests/sent.ts b/packages/backend/src/server/api/endpoints/following/requests/sent.ts deleted file mode 100644 index 6325f01bb8..0000000000 --- a/packages/backend/src/server/api/endpoints/following/requests/sent.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueryService } from '@/core/QueryService.js'; -import type { FollowRequestsRepository } from '@/models/_.js'; -import { FollowRequestEntityService } from '@/core/entities/FollowRequestEntityService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['following', 'account'], - - requireCredential: true, - - kind: 'read:following', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - follower: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - followee: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.followRequestsRepository) - private followRequestsRepository: FollowRequestsRepository, - - private followRequestEntityService: FollowRequestEntityService, - private queryService: QueryService, - ) { - super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId) - .andWhere('request.followerId = :meId', { meId: me.id }); - - const requests = await query - .limit(ps.limit) - .getMany(); - - return await this.followRequestEntityService.packMany(requests, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/following/update-all.ts b/packages/backend/src/server/api/endpoints/following/update-all.ts deleted file mode 100644 index c953feb393..0000000000 --- a/packages/backend/src/server/api/endpoints/following/update-all.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import ms from 'ms'; -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository } from '@/models/_.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { UserFollowingService } from '@/core/UserFollowingService.js'; -import { DI } from '@/di-symbols.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['following', 'users'], - - limit: { - duration: ms('1hour'), - max: 10, - }, - - requireCredential: true, - - kind: 'write:following', -} as const; - -export const paramDef = { - type: 'object', - properties: { - notify: { type: 'string', enum: ['normal', 'none'] }, - withReplies: { type: 'boolean' }, - }, -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - ) { - super(meta, paramDef, async (ps, me) => { - await this.followingsRepository.update({ - followerId: me.id, - }, { - notify: ps.notify != null ? (ps.notify === 'none' ? null : ps.notify) : undefined, - withReplies: ps.withReplies != null ? ps.withReplies : undefined, - }); - - return; - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/following/update.ts b/packages/backend/src/server/api/endpoints/following/update.ts deleted file mode 100644 index d62cf210ed..0000000000 --- a/packages/backend/src/server/api/endpoints/following/update.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import ms from 'ms'; -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository } from '@/models/_.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { UserFollowingService } from '@/core/UserFollowingService.js'; -import { DI } from '@/di-symbols.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['following', 'users'], - - limit: { - duration: ms('1hour'), - max: 100, - }, - - requireCredential: true, - - kind: 'write:following', - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '14318698-f67e-492a-99da-5353a5ac52be', - }, - - followeeIsYourself: { - message: 'Followee is yourself.', - code: 'FOLLOWEE_IS_YOURSELF', - id: '4c4cbaf9-962a-463b-8418-a5e365dbf2eb', - }, - - notFollowing: { - message: 'You are not following that user.', - code: 'NOT_FOLLOWING', - id: 'b8dc75cf-1cb5-46c9-b14b-5f1ffbd782c9', - }, - }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - notify: { type: 'string', enum: ['normal', 'none'] }, - withReplies: { type: 'boolean' }, - }, - required: ['userId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - - private userEntityService: UserEntityService, - private getterService: GetterService, - private userFollowingService: UserFollowingService, - ) { - super(meta, paramDef, async (ps, me) => { - const follower = me; - - // Check if the follower is yourself - if (me.id === ps.userId) { - throw new ApiError(meta.errors.followeeIsYourself); - } - - // Get followee - const followee = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw err; - }); - - // Check not following - const exist = await this.followingsRepository.findOneBy({ - followerId: follower.id, - followeeId: followee.id, - }); - - if (exist == null) { - throw new ApiError(meta.errors.notFollowing); - } - - await this.followingsRepository.update({ - id: exist.id, - }, { - notify: ps.notify != null ? (ps.notify === 'none' ? null : ps.notify) : undefined, - withReplies: ps.withReplies != null ? ps.withReplies : undefined, - }); - - return await this.userEntityService.pack(follower.id, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts index 7d2878e03f..9994ce90d7 100644 --- a/packages/backend/src/server/api/endpoints/gallery/featured.ts +++ b/packages/backend/src/server/api/endpoints/gallery/featured.ts @@ -1,14 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository } from '@/models/_.js'; +import type { GalleryPostsRepository } from '@/models/index.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; -import { FeaturedService } from '@/core/FeaturedService.js'; export const meta = { tags: ['gallery'], @@ -28,49 +22,26 @@ export const meta = { export const paramDef = { type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - untilId: { type: 'string', format: 'misskey:id' }, - }, + properties: {}, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - private galleryPostsRankingCache: string[] = []; - private galleryPostsRankingCacheLastFetchedAt = 0; - +export default class extends Endpoint { constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, private galleryPostEntityService: GalleryPostEntityService, - private featuredService: FeaturedService, ) { super(meta, paramDef, async (ps, me) => { - let postIds: string[]; - if (this.galleryPostsRankingCacheLastFetchedAt !== 0 && (Date.now() - this.galleryPostsRankingCacheLastFetchedAt < 1000 * 60 * 30)) { - postIds = this.galleryPostsRankingCache; - } else { - postIds = await this.featuredService.getGalleryPostsRanking(100); - this.galleryPostsRankingCache = postIds; - this.galleryPostsRankingCacheLastFetchedAt = Date.now(); - } - - postIds.sort((a, b) => a > b ? -1 : 1); - if (ps.untilId) { - postIds = postIds.filter(id => id < ps.untilId!); - } - postIds = postIds.slice(0, ps.limit); - - if (postIds.length === 0) { - return []; - } - const query = this.galleryPostsRepository.createQueryBuilder('post') - .where('post.id IN (:...postIds)', { postIds: postIds }); + .andWhere('post.createdAt > :date', { date: new Date(Date.now() - (1000 * 60 * 60 * 24 * 3)) }) + .andWhere('post.likedCount > 0') + .orderBy('post.likedCount', 'DESC'); - const posts = await query.getMany(); + const posts = await query.take(10).getMany(); return await this.galleryPostEntityService.packMany(posts, me); }); diff --git a/packages/backend/src/server/api/endpoints/gallery/popular.ts b/packages/backend/src/server/api/endpoints/gallery/popular.ts index 4ee252104a..55d3dabfb0 100644 --- a/packages/backend/src/server/api/endpoints/gallery/popular.ts +++ b/packages/backend/src/server/api/endpoints/gallery/popular.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository } from '@/models/_.js'; +import type { GalleryPostsRepository } from '@/models/index.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -31,8 +26,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -44,7 +40,7 @@ export default class extends Endpoint { // eslint- .andWhere('post.likedCount > 0') .orderBy('post.likedCount', 'DESC'); - const posts = await query.limit(10).getMany(); + const posts = await query.take(10).getMany(); return await this.galleryPostEntityService.packMany(posts, me); }); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts.ts b/packages/backend/src/server/api/endpoints/gallery/posts.ts index d398418ab4..e94003eb79 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository } from '@/models/_.js'; +import type { GalleryPostsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -34,8 +29,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -47,7 +43,7 @@ export default class extends Endpoint { // eslint- const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId) .innerJoinAndSelect('post.user', 'user'); - const posts = await query.limit(ps.limit).getMany(); + const posts = await query.take(ps.limit).getMany(); return await this.galleryPostEntityService.packMany(posts, me); }); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts index 504a9c789e..ca6bfa7e0f 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -1,14 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/_.js'; -import { MiGalleryPost } from '@/models/GalleryPost.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/index.js'; +import { GalleryPost } from '@/models/entities/GalleryPost.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; import { IdService } from '@/core/IdService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -51,8 +46,9 @@ export const paramDef = { required: ['title', 'fileIds'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -69,21 +65,22 @@ export default class extends Endpoint { // eslint- id: fileId, userId: me.id, }), - ))).filter(x => x != null); + ))).filter((file): file is DriveFile => file != null); if (files.length === 0) { throw new Error(); } - const post = await this.galleryPostsRepository.insertOne(new MiGalleryPost({ - id: this.idService.gen(), + const post = await this.galleryPostsRepository.insert(new GalleryPost({ + id: this.idService.genId(), + createdAt: new Date(), updatedAt: new Date(), title: ps.title, description: ps.description, userId: me.id, isSensitive: ps.isSensitive, fileIds: files.map(file => file.id), - })); + })).then(x => this.galleryPostsRepository.findOneByOrFail(x.identifiers[0])); return await this.galleryPostEntityService.pack(post, me); }); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts index b6b94db161..6cdcc17b39 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts @@ -1,14 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository, UsersRepository } from '@/models/_.js'; +import type { GalleryPostsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -24,12 +17,6 @@ export const meta = { code: 'NO_SUCH_POST', id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5', }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: 'c86e09de-1c48-43ac-a435-1c7e42ed4496', - }, }, } as const; @@ -41,40 +28,24 @@ export const paramDef = { required: ['postId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private moderationLogService: ModerationLogService, - private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId }); + const post = await this.galleryPostsRepository.findOneBy({ + id: ps.postId, + userId: me.id, + }); if (post == null) { throw new ApiError(meta.errors.noSuchPost); } - if (!await this.roleService.isModerator(me) && post.userId !== me.id) { - throw new ApiError(meta.errors.accessDenied); - } - await this.galleryPostsRepository.delete(post.id); - - if (post.userId !== me.id) { - const user = await this.usersRepository.findOneByOrFail({ id: post.userId }); - this.moderationLogService.log(me, 'deleteGalleryPost', { - postId: post.id, - postUserId: post.userId, - postUserUsername: user.username, - post, - }); - } }); } } diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts index 91e49e6463..6ac5fa8606 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -1,12 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/_.js'; -import { FeaturedService, GALLERY_POSTS_RANKING_WINDOW } from '@/core/FeaturedService.js'; +import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -49,8 +43,9 @@ export const paramDef = { required: ['postId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -58,7 +53,6 @@ export default class extends Endpoint { // eslint- @Inject(DI.galleryLikesRepository) private galleryLikesRepository: GalleryLikesRepository, - private featuredService: FeaturedService, private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { @@ -72,29 +66,23 @@ export default class extends Endpoint { // eslint- } // if already liked - const exist = await this.galleryLikesRepository.exists({ - where: { - postId: post.id, - userId: me.id, - }, + const exist = await this.galleryLikesRepository.findOneBy({ + postId: post.id, + userId: me.id, }); - if (exist) { + if (exist != null) { throw new ApiError(meta.errors.alreadyLiked); } // Create like await this.galleryLikesRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), postId: post.id, userId: me.id, }); - // ランキング更新 - if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) { - await this.featuredService.updateGalleryPostsRanking(post.id, 1); - } - this.galleryPostsRepository.increment({ id: post.id }, 'likedCount', 1); }); } diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts index bd69898229..f7e828142b 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository } from '@/models/_.js'; +import type { GalleryPostsRepository } from '@/models/index.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -38,8 +33,9 @@ export const paramDef = { required: ['postId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts index f44e2c7afc..513089217d 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts @@ -1,13 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository, GalleryLikesRepository } from '@/models/_.js'; -import { FeaturedService, GALLERY_POSTS_RANKING_WINDOW } from '@/core/FeaturedService.js'; -import { IdService } from '@/core/IdService.js'; +import type { GalleryPostsRepository, GalleryLikesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -43,17 +36,15 @@ export const paramDef = { required: ['postId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @Inject(DI.galleryLikesRepository) private galleryLikesRepository: GalleryLikesRepository, - - private featuredService: FeaturedService, - private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId }); @@ -73,11 +64,6 @@ export default class extends Endpoint { // eslint- // Delete like await this.galleryLikesRepository.delete(exist.id); - // ランキング更新 - if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) { - await this.featuredService.updateGalleryPostsRanking(post.id, -1); - } - this.galleryPostsRepository.decrement({ id: post.id }, 'likedCount', 1); }); } diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts index 5243ee9603..a2a10d8400 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/_.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/index.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -47,11 +42,12 @@ export const paramDef = { } }, isSensitive: { type: 'boolean', default: false }, }, - required: ['postId'], + required: ['postId', 'title', 'fileIds'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -62,19 +58,15 @@ export default class extends Endpoint { // eslint- private galleryPostEntityService: GalleryPostEntityService, ) { super(meta, paramDef, async (ps, me) => { - let files: Array | undefined; + const files = (await Promise.all(ps.fileIds.map(fileId => + this.driveFilesRepository.findOneBy({ + id: fileId, + userId: me.id, + }), + ))).filter((file): file is DriveFile => file != null); - if (ps.fileIds) { - files = (await Promise.all(ps.fileIds.map(fileId => - this.driveFilesRepository.findOneBy({ - id: fileId, - userId: me.id, - }), - ))).filter(x => x != null); - - if (files.length === 0) { - throw new Error(); - } + if (files.length === 0) { + throw new Error(); } await this.galleryPostsRepository.update({ @@ -85,7 +77,7 @@ export default class extends Endpoint { // eslint- title: ps.title, description: ps.description, isSensitive: ps.isSensitive, - fileIds: files ? files.map(file => file.id) : undefined, + fileIds: files.map(file => file.id), }); const post = await this.galleryPostsRepository.findOneByOrFail({ id: ps.postId }); diff --git a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts deleted file mode 100644 index 52acee1cfb..0000000000 --- a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { IsNull } from 'typeorm'; -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; -import { RoleService } from '@/core/RoleService.js'; - -export const meta = { - tags: ['users'], - - requireCredential: false, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - name: { - type: 'string', - optional: false, nullable: false, - }, - description: { - type: 'string', - optional: false, nullable: false, - }, - url: { - type: 'string', - optional: false, nullable: false, - }, - roleIdsThatCanBeUsedThisDecoration: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private avatarDecorationService: AvatarDecorationService, - private roleService: RoleService, - ) { - super(meta, paramDef, async (ps, me) => { - const decorations = await this.avatarDecorationService.getAll(true); - const allRoles = await this.roleService.getRoles(); - - return decorations.map(decoration => ({ - id: decoration.id, - name: decoration.name, - description: decoration.description, - url: decoration.url, - roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(role => role.id === roleId)), - })); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts index a57774be73..dea0f4799c 100644 --- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts +++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { MoreThan } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { USER_ONLINE_THRESHOLD } from '@/const.js'; -import type { UsersRepository } from '@/models/_.js'; +import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -14,18 +9,6 @@ export const meta = { tags: ['meta'], requireCredential: false, - allowGet: true, - cacheSec: 60 * 1, - res: { - type: 'object', - optional: false, nullable: false, - properties: { - count: { - type: 'number', - nullable: false, - }, - }, - }, } as const; export const paramDef = { @@ -34,8 +17,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/server/api/endpoints/hashtags/list.ts b/packages/backend/src/server/api/endpoints/hashtags/list.ts index 5cd3c6584d..226a11de0b 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/list.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/list.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { HashtagsRepository } from '@/models/_.js'; +import type { HashtagsRepository } from '@/models/index.js'; import { HashtagEntityService } from '@/core/entities/HashtagEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -37,8 +32,9 @@ export const paramDef = { required: ['sort'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.hashtagsRepository) private hashtagsRepository: HashtagsRepository, @@ -77,7 +73,7 @@ export default class extends Endpoint { // eslint- 'tag.attachedRemoteUsersCount', ]); - const tags = await query.limit(ps.limit).getMany(); + const tags = await query.take(ps.limit).getMany(); return this.hashtagEntityService.packMany(tags); }); diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index d4eb851054..4f5f979767 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { HashtagsRepository } from '@/models/_.js'; +import type { HashtagsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; @@ -34,8 +29,9 @@ export const paramDef = { required: ['query'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.hashtagsRepository) private hashtagsRepository: HashtagsRepository, @@ -43,10 +39,10 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { const hashtags = await this.hashtagsRepository.createQueryBuilder('tag') .where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' }) - .orderBy('tag.mentionedLocalUsersCount', 'DESC') + .orderBy('tag.count', 'DESC') .groupBy('tag.id') - .limit(ps.limit) - .offset(ps.offset) + .take(ps.limit) + .skip(ps.offset) .getMany(); return hashtags.map(tag => tag.name); diff --git a/packages/backend/src/server/api/endpoints/hashtags/show.ts b/packages/backend/src/server/api/endpoints/hashtags/show.ts index 940e3bd69d..06b0d6e9b2 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/show.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/show.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { HashtagsRepository } from '@/models/_.js'; +import type { HashtagsRepository } from '@/models/index.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { HashtagEntityService } from '@/core/entities/HashtagEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -39,8 +34,9 @@ export const paramDef = { required: ['tag'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.hashtagsRepository) private hashtagsRepository: HashtagsRepository, diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index cb8065e3a6..cf45cc6c24 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -1,20 +1,31 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - +import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { NotesRepository } from '@/models/index.js'; +import type { Note } from '@/models/entities/Note.js'; +import { safeForSql } from '@/misc/safe-for-sql.js'; +import { normalizeForSearch } from '@/misc/normalize-for-search.js'; +import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; -import { FeaturedService } from '@/core/FeaturedService.js'; -import { HashtagService } from '@/core/HashtagService.js'; + +/* +トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要 +ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる + +..が理想だけどPostgreSQLでどうするのか分からないので単に「直近Aの内に投稿されたユニーク投稿数が多いハッシュタグ」で妥協する +*/ + +const rangeA = 1000 * 60 * 60; // 60分 +//const rangeB = 1000 * 60 * 120; // 2時間 +//const coefficient = 1.25; // 「n倍」の部分 +//const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか + +const max = 5; export const meta = { tags: ['hashtags'], requireCredential: false, - allowGet: true, - cacheSec: 60 * 1, res: { type: 'array', @@ -50,21 +61,102 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private featuredService: FeaturedService, - private hashtagService: HashtagService, + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private metaService: MetaService, ) { super(meta, paramDef, async () => { - const ranking = await this.featuredService.getHashtagsRanking(10); + const instance = await this.metaService.fetch(true); + const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); - const charts = ranking.length === 0 ? {} : await this.hashtagService.getCharts(ranking, 20); + const now = new Date(); // 5分単位で丸めた現在日時 + now.setMinutes(Math.round(now.getMinutes() / 5) * 5, 0, 0); - const stats = ranking.map((tag, i) => ({ + const tagNotes = await this.notesRepository.createQueryBuilder('note') + .where('note.createdAt > :date', { date: new Date(now.getTime() - rangeA) }) + .andWhere(new Brackets(qb => { qb + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); + })) + .andWhere('note.tags != \'{}\'') + .select(['note.tags', 'note.userId']) + .cache(60000) // 1 min + .getMany(); + + if (tagNotes.length === 0) { + return []; + } + + const tags: { + name: string; + users: Note['userId'][]; + }[] = []; + + for (const note of tagNotes) { + for (const tag of note.tags) { + if (hiddenTags.includes(tag)) continue; + + const x = tags.find(x => x.name === tag); + if (x) { + if (!x.users.includes(note.userId)) { + x.users.push(note.userId); + } + } else { + tags.push({ + name: tag, + users: [note.userId], + }); + } + } + } + + // タグを人気順に並べ替え + const hots = tags + .sort((a, b) => b.users.length - a.users.length) + .map(tag => tag.name) + .slice(0, max); + + //#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する + const countPromises: Promise[] = []; + + const range = 20; + + // 10分 + const interval = 1000 * 60 * 10; + + for (let i = 0; i < range; i++) { + countPromises.push(Promise.all(hots.map(tag => this.notesRepository.createQueryBuilder('note') + .select('count(distinct note.userId)') + .where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`) + .andWhere('note.createdAt < :lt', { lt: new Date(now.getTime() - (interval * i)) }) + .andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - (interval * (i + 1))) }) + .cache(60000) // 1 min + .getRawOne() + .then(x => parseInt(x.count, 10)), + ))); + } + + const countsLog = await Promise.all(countPromises); + //#endregion + + const totalCounts = await Promise.all(hots.map(tag => this.notesRepository.createQueryBuilder('note') + .select('count(distinct note.userId)') + .where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`) + .andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - rangeA) }) + .cache(60000 * 60) // 60 min + .getRawOne() + .then(x => parseInt(x.count, 10)), + )); + + const stats = hots.map((tag, i) => ({ tag, - chart: charts[tag], - usersCount: Math.max(...charts[tag]), + chart: countsLog.map(counts => counts[i]), + usersCount: totalCounts[i], })); return stats; diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts index 30f0c1b0c8..dd3549020e 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/users.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts @@ -1,12 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/_.js'; -import { safeForSql } from "@/misc/safe-for-sql.js"; +import type { UsersRepository } from '@/models/index.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -39,18 +33,18 @@ export const paramDef = { required: ['tag', 'sort'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, - + private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { - if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection'); const query = this.usersRepository.createQueryBuilder('user') - .where(':tag <@ user.tags', { tag: [normalizeForSearch(ps.tag)] }) + .where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) }) .andWhere('user.isSuspended = FALSE'); const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5)); @@ -68,15 +62,15 @@ export default class extends Endpoint { // eslint- switch (ps.sort) { case '+follower': query.orderBy('user.followersCount', 'DESC'); break; case '-follower': query.orderBy('user.followersCount', 'ASC'); break; - case '+createdAt': query.orderBy('user.id', 'DESC'); break; - case '-createdAt': query.orderBy('user.id', 'ASC'); break; + case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break; case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break; } - const users = await query.limit(ps.limit).getMany(); + const users = await query.take(ps.limit).getMany(); - return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' }); + return await this.userEntityService.packMany(users, me, { detail: true }); }); } } diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index d324e3e64a..a3e3e02a12 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UserProfilesRepository } from '@/models/_.js'; +import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -14,7 +9,6 @@ export const meta = { tags: ['account'], requireCredential: true, - kind: "read:account", res: { type: 'object', @@ -29,7 +23,7 @@ export const meta = { id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a', kind: 'permission', }, - }, + } } as const; export const paramDef = { @@ -38,9 +32,13 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @@ -70,9 +68,9 @@ export default class extends Endpoint { // eslint- }); userProfile.loggedInDates = [...userProfile.loggedInDates, today]; } - - return await this.userEntityService.pack(userProfile.user!, userProfile.user!, { - schema: 'MeDetailed', + + return await this.userEntityService.pack(userProfile.user!, userProfile.user!, { + detail: true, includeSecrets: isSecure, userProfile, }); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts index 2a30e8b0c3..6c31075e05 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -1,33 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as OTPAuth from 'otpauth'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { UserProfilesRepository } from '@/models/_.js'; +import type { UserProfilesRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, secure: true, - - res: { - type: 'object', - properties: { - backupCodes: { - type: 'array', - optional: false, - items: { - type: 'string', - }, - }, - }, - }, } as const; export const paramDef = { @@ -38,9 +21,13 @@ export const paramDef = { required: ['token'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @@ -60,30 +47,23 @@ export default class extends Endpoint { // eslint- secret: OTPAuth.Secret.fromBase32(profile.twoFactorTempSecret), digits: 6, token, - window: 5, + window: 1, }); if (delta === null) { throw new Error('not verified'); } - const backupCodes = Array.from({ length: 5 }, () => new OTPAuth.Secret().base32); - await this.userProfilesRepository.update(me.id, { twoFactorSecret: profile.twoFactorTempSecret, - twoFactorBackupSecret: backupCodes, twoFactorEnabled: true, }); // Publish meUpdated event this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, })); - - return { - backupCodes: backupCodes, - }; }); } } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index 65eece5b97..e8985a9cd8 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -1,122 +1,163 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - +import { promisify } from 'node:util'; import bcrypt from 'bcryptjs'; +import cbor from 'cbor'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js'; -import { WebAuthnService } from '@/core/WebAuthnService.js'; -import { ApiError } from '@/server/api/error.js'; -import { UserAuthService } from '@/core/UserAuthService.js'; +import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; +import type { AttestationChallengesRepository, UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; + +const cborDecodeFirst = promisify(cbor.decodeFirst) as any; export const meta = { requireCredential: true, secure: true, - - errors: { - incorrectPassword: { - message: 'Incorrect password.', - code: 'INCORRECT_PASSWORD', - id: '0d7ec6d2-e652-443e-a7bf-9ee9a0cd77b0', - }, - - twoFactorNotEnabled: { - message: '2fa not enabled.', - code: 'TWO_FACTOR_NOT_ENABLED', - id: '798d6847-b1ed-4f9c-b1f9-163c42655995', - }, - }, - - res: { - type: 'object', - nullable: false, - optional: false, - properties: { - id: { type: 'string' }, - name: { type: 'string' }, - }, - }, } as const; export const paramDef = { type: 'object', properties: { + clientDataJSON: { type: 'string' }, + attestationObject: { type: 'string' }, password: { type: 'string' }, - token: { type: 'string', nullable: true }, + challengeId: { type: 'string' }, name: { type: 'string', minLength: 1, maxLength: 30 }, - credential: { type: 'object' }, }, - required: ['password', 'name', 'credential'], + required: ['clientDataJSON', 'attestationObject', 'password', 'challengeId', 'name'], } as const; // eslint-disable-next-line import/no-default-export @Injectable() export default class extends Endpoint { constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @Inject(DI.userSecurityKeysRepository) private userSecurityKeysRepository: UserSecurityKeysRepository, - private webAuthnService: WebAuthnService, - private userAuthService: UserAuthService, + @Inject(DI.attestationChallengesRepository) + private attestationChallengesRepository: AttestationChallengesRepository, + private userEntityService: UserEntityService, private globalEventService: GlobalEventService, + private twoFactorAuthenticationService: TwoFactorAuthenticationService, ) { super(meta, paramDef, async (ps, me) => { - const token = ps.token; + const rpIdHashReal = this.twoFactorAuthenticationService.hash(Buffer.from(this.config.hostname, 'utf-8')); + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - if (profile.twoFactorEnabled) { - if (token == null) { - throw new Error('authentication failed'); - } + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - try { - await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (e) { - throw new Error('authentication failed'); - } - } - - const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); - if (!passwordMatched) { - throw new ApiError(meta.errors.incorrectPassword); + if (!same) { + throw new Error('incorrect password'); } if (!profile.twoFactorEnabled) { - throw new ApiError(meta.errors.twoFactorNotEnabled); + throw new Error('2fa not enabled'); } - const keyInfo = await this.webAuthnService.verifyRegistration(me.id, ps.credential); - const keyId = keyInfo.credentialID; + const clientData = JSON.parse(ps.clientDataJSON); + + if (clientData.type !== 'webauthn.create') { + throw new Error('not a creation attestation'); + } + if (clientData.origin !== this.config.scheme + '://' + this.config.host) { + throw new Error('origin mismatch'); + } + + const clientDataJSONHash = this.twoFactorAuthenticationService.hash(Buffer.from(ps.clientDataJSON, 'utf-8')); + + const attestation = await cborDecodeFirst(ps.attestationObject); + + const rpIdHash = attestation.authData.slice(0, 32); + if (!rpIdHashReal.equals(rpIdHash)) { + throw new Error('rpIdHash mismatch'); + } + + const flags = attestation.authData[32]; + + // eslint:disable-next-line:no-bitwise + if (!(flags & 1)) { + throw new Error('user not present'); + } + + const authData = Buffer.from(attestation.authData); + const credentialIdLength = authData.readUInt16BE(53); + const credentialId = authData.slice(55, 55 + credentialIdLength); + const publicKeyData = authData.slice(55 + credentialIdLength); + const publicKey: Map = await cborDecodeFirst(publicKeyData); + if (publicKey.get(3) !== -7) { + throw new Error('alg mismatch'); + } + + const procedures = this.twoFactorAuthenticationService.getProcedures(); + + if (!(procedures as any)[attestation.fmt]) { + throw new Error('unsupported fmt'); + } + + const verificationData = (procedures as any)[attestation.fmt].verify({ + attStmt: attestation.attStmt, + authenticatorData: authData, + clientDataHash: clientDataJSONHash, + credentialId, + publicKey, + rpIdHash, + }); + if (!verificationData.valid) throw new Error('signature invalid'); + + const attestationChallenge = await this.attestationChallengesRepository.findOneBy({ + userId: me.id, + id: ps.challengeId, + registrationChallenge: true, + challenge: this.twoFactorAuthenticationService.hash(clientData.challenge).toString('hex'), + }); + + if (!attestationChallenge) { + throw new Error('non-existent challenge'); + } + + await this.attestationChallengesRepository.delete({ + userId: me.id, + id: ps.challengeId, + }); + + // Expired challenge (> 5min old) + if ( + new Date().getTime() - attestationChallenge.createdAt.getTime() >= + 5 * 60 * 1000 + ) { + throw new Error('expired challenge'); + } + + const credentialIdString = credentialId.toString('hex'); await this.userSecurityKeysRepository.insert({ - id: keyId, userId: me.id, + id: credentialIdString, + lastUsed: new Date(), name: ps.name, - publicKey: Buffer.from(keyInfo.credentialPublicKey).toString('base64url'), - counter: keyInfo.counter, - credentialDeviceType: keyInfo.credentialDeviceType, - credentialBackedUp: keyInfo.credentialBackedUp, - transports: keyInfo.transports, + publicKey: verificationData.publicKey.toString('hex'), }); // Publish meUpdated event this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, })); return { - id: keyId, + id: credentialIdString, name: ps.name, }; }); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts index bf039ccd16..0ee9f556a8 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js'; +import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -33,8 +28,9 @@ export const paramDef = { required: ['value'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @@ -74,7 +70,7 @@ export default class extends Endpoint { // eslint- // Publish meUpdated event this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, })); }); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index 9391aee5e0..19c77365c6 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -1,183 +1,25 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - +import { promisify } from 'node:util'; +import * as crypto from 'node:crypto'; import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserProfilesRepository } from '@/models/_.js'; +import type { UserProfilesRepository, AttestationChallengesRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; import { DI } from '@/di-symbols.js'; -import { WebAuthnService } from '@/core/WebAuthnService.js'; -import { ApiError } from '@/server/api/error.js'; -import { UserAuthService } from '@/core/UserAuthService.js'; + +const randomBytes = promisify(crypto.randomBytes); export const meta = { requireCredential: true, secure: true, - - errors: { - userNotFound: { - message: 'User not found.', - code: 'USER_NOT_FOUND', - id: '652f899f-66d4-490e-993e-6606c8ec04c3', - }, - - incorrectPassword: { - message: 'Incorrect password.', - code: 'INCORRECT_PASSWORD', - id: '38769596-efe2-4faf-9bec-abbb3f2cd9ba', - }, - - twoFactorNotEnabled: { - message: '2fa not enabled.', - code: 'TWO_FACTOR_NOT_ENABLED', - id: 'bf32b864-449b-47b8-974e-f9a5468546f1', - }, - }, - - res: { - type: 'object', - nullable: false, - optional: false, - properties: { - rp: { - type: 'object', - properties: { - id: { - type: 'string', - optional: true, - }, - }, - }, - user: { - type: 'object', - properties: { - id: { - type: 'string', - }, - name: { - type: 'string', - }, - displayName: { - type: 'string', - }, - }, - }, - challenge: { - type: 'string', - }, - pubKeyCredParams: { - type: 'array', - items: { - type: 'object', - properties: { - type: { - type: 'string', - }, - alg: { - type: 'number', - }, - }, - }, - }, - timeout: { - type: 'number', - nullable: true, - }, - excludeCredentials: { - type: 'array', - nullable: true, - items: { - type: 'object', - properties: { - id: { - type: 'string', - }, - type: { - type: 'string', - }, - transports: { - type: 'array', - items: { - type: 'string', - enum: [ - 'ble', - 'cable', - 'hybrid', - 'internal', - 'nfc', - 'smart-card', - 'usb', - ], - }, - }, - }, - }, - }, - authenticatorSelection: { - type: 'object', - nullable: true, - properties: { - authenticatorAttachment: { - type: 'string', - enum: [ - 'cross-platform', - 'platform', - ], - }, - requireResidentKey: { - type: 'boolean', - }, - userVerification: { - type: 'string', - enum: [ - 'discouraged', - 'preferred', - 'required', - ], - }, - }, - }, - attestation: { - type: 'string', - nullable: true, - enum: [ - 'direct', - 'enterprise', - 'indirect', - 'none', - null, - ], - }, - extensions: { - type: 'object', - nullable: true, - properties: { - appid: { - type: 'string', - nullable: true, - }, - credProps: { - type: 'boolean', - nullable: true, - }, - hmacCreateSecret: { - type: 'boolean', - nullable: true, - }, - }, - }, - }, - }, } as const; export const paramDef = { type: 'object', properties: { password: { type: 'string' }, - token: { type: 'string', nullable: true }, }, required: ['password'], } as const; @@ -189,48 +31,47 @@ export default class extends Endpoint { @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - private webAuthnService: WebAuthnService, - private userAuthService: UserAuthService, + @Inject(DI.attestationChallengesRepository) + private attestationChallengesRepository: AttestationChallengesRepository, + + private idService: IdService, + private twoFactorAuthenticationService: TwoFactorAuthenticationService, ) { super(meta, paramDef, async (ps, me) => { - const token = ps.token; - const profile = await this.userProfilesRepository.findOne({ - where: { - userId: me.id, - }, - relations: ['user'], - }); + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - if (profile == null) { - throw new ApiError(meta.errors.userNotFound); - } + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - if (profile.twoFactorEnabled) { - if (token == null) { - throw new Error('authentication failed'); - } - - try { - await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (e) { - throw new Error('authentication failed'); - } - } - - const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); - if (!passwordMatched) { - throw new ApiError(meta.errors.incorrectPassword); + if (!same) { + throw new Error('incorrect password'); } if (!profile.twoFactorEnabled) { - throw new ApiError(meta.errors.twoFactorNotEnabled); + throw new Error('2fa not enabled'); } - return await this.webAuthnService.initiateRegistration( - me.id, - profile.user?.username ?? me.id, - profile.user?.name ?? undefined, - ); + // 32 byte challenge + const entropy = await randomBytes(32); + const challenge = entropy.toString('base64') + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + + const challengeId = this.idService.genId(); + + await this.attestationChallengesRepository.insert({ + userId: me.id, + id: challengeId, + challenge: this.twoFactorAuthenticationService.hash(Buffer.from(challenge, 'utf-8')).toString('hex'), + createdAt: new Date(), + registrationChallenge: true, + }); + + return { + challengeId, + challenge, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index a54c598213..eb4d7f9c14 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -1,85 +1,44 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import bcrypt from 'bcryptjs'; import * as OTPAuth from 'otpauth'; import * as QRCode from 'qrcode'; import { Inject, Injectable } from '@nestjs/common'; -import type { UserProfilesRepository } from '@/models/_.js'; +import type { UserProfilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import { ApiError } from '@/server/api/error.js'; -import { UserAuthService } from '@/core/UserAuthService.js'; export const meta = { requireCredential: true, secure: true, - - errors: { - incorrectPassword: { - message: 'Incorrect password.', - code: 'INCORRECT_PASSWORD', - id: '78d6c839-20c9-4c66-b90a-fc0542168b48', - }, - }, - - res: { - type: 'object', - nullable: false, - optional: false, - properties: { - qr: { type: 'string' }, - url: { type: 'string' }, - secret: { type: 'string' }, - label: { type: 'string' }, - issuer: { type: 'string' }, - }, - }, } as const; export const paramDef = { type: 'object', properties: { password: { type: 'string' }, - token: { type: 'string', nullable: true }, }, required: ['password'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.config) private config: Config, @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - - private userAuthService: UserAuthService, ) { super(meta, paramDef, async (ps, me) => { - const token = ps.token; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - if (profile.twoFactorEnabled) { - if (token == null) { - throw new Error('authentication failed'); - } + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - try { - await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (e) { - throw new Error('authentication failed'); - } - } - - const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); - if (!passwordMatched) { - throw new ApiError(meta.errors.incorrectPassword); + if (!same) { + throw new Error('incorrect password'); } // Generate user's secret key diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index c350136eae..4b726aed80 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -1,44 +1,29 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js'; +import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '@/server/api/error.js'; -import { UserAuthService } from '@/core/UserAuthService.js'; export const meta = { requireCredential: true, secure: true, - - errors: { - incorrectPassword: { - message: 'Incorrect password.', - code: 'INCORRECT_PASSWORD', - id: '141c598d-a825-44c8-9173-cfb9d92be493', - }, - }, } as const; export const paramDef = { type: 'object', properties: { password: { type: 'string' }, - token: { type: 'string', nullable: true }, credentialId: { type: 'string' }, }, required: ['password', 'credentialId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userSecurityKeysRepository) private userSecurityKeysRepository: UserSecurityKeysRepository, @@ -47,28 +32,16 @@ export default class extends Endpoint { // eslint- private userProfilesRepository: UserProfilesRepository, private userEntityService: UserEntityService, - private userAuthService: UserAuthService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - const token = ps.token; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - if (profile.twoFactorEnabled) { - if (token == null) { - throw new Error('authentication failed'); - } + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - try { - await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (e) { - throw new Error('authentication failed'); - } - } - - const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); - if (!passwordMatched) { - throw new ApiError(meta.errors.incorrectPassword); + if (!same) { + throw new Error('incorrect password'); } // Make sure we only delete the user's own creds @@ -97,7 +70,7 @@ export default class extends Endpoint { // eslint- // Publish meUpdated event this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, })); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index b5a53cc889..e0e7ba6658 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -1,82 +1,54 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { UserProfilesRepository } from '@/models/_.js'; +import type { UserProfilesRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '@/server/api/error.js'; -import { UserAuthService } from '@/core/UserAuthService.js'; export const meta = { requireCredential: true, secure: true, - - errors: { - incorrectPassword: { - message: 'Incorrect password.', - code: 'INCORRECT_PASSWORD', - id: '7add0395-9901-4098-82f9-4f67af65f775', - }, - }, } as const; export const paramDef = { type: 'object', properties: { password: { type: 'string' }, - token: { type: 'string', nullable: true }, }, required: ['password'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, private userEntityService: UserEntityService, - private userAuthService: UserAuthService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - const token = ps.token; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - if (profile.twoFactorEnabled) { - if (token == null) { - throw new Error('authentication failed'); - } + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - try { - await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (e) { - throw new Error('authentication failed'); - } - } - - const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); - if (!passwordMatched) { - throw new ApiError(meta.errors.incorrectPassword); + if (!same) { + throw new Error('incorrect password'); } await this.userProfilesRepository.update(me.id, { twoFactorSecret: null, - twoFactorBackupSecret: null, twoFactorEnabled: false, usePasswordLessLogin: false, }); // Publish meUpdated event this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, })); }); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts index cfa07cc8d7..d98f60fa5f 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserSecurityKeysRepository } from '@/models/_.js'; +import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -25,7 +20,7 @@ export const meta = { }, accessDenied: { - message: 'You do not have edit privilege of this key.', + message: 'You do not have edit privilege of the channel.', code: 'ACCESS_DENIED', id: '1fb7cb09-d46a-4fff-b8df-057708cce513', }, @@ -41,12 +36,16 @@ export const paramDef = { required: ['name', 'credentialId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userSecurityKeysRepository) private userSecurityKeysRepository: UserSecurityKeysRepository, + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + private userEntityService: UserEntityService, private globalEventService: GlobalEventService, ) { @@ -62,14 +61,14 @@ export default class extends Endpoint { // eslint- if (key.userId !== me.id) { throw new ApiError(meta.errors.accessDenied); } - + await this.userSecurityKeysRepository.update(key.id, { name: ps.name, }); // Publish meUpdated event this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, })); diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index 055b5cc061..48fb03a8af 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -1,54 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AccessTokensRepository } from '@/models/_.js'; +import type { AccessTokensRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -import { IdService } from '@/core/IdService.js'; export const meta = { requireCredential: true, secure: true, - - res: { - type: 'array', - items: { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, - format: 'misskey:id', - }, - name: { - type: 'string', - optional: true, - }, - createdAt: { - type: 'string', - optional: false, - format: 'date-time', - }, - lastUsedAt: { - type: 'string', - optional: true, - format: 'date-time', - }, - permission: { - type: 'array', - optional: false, - uniqueItems: true, - items: { - type: 'string', - }, - }, - }, - }, - }, } as const; export const paramDef = { @@ -59,13 +17,12 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.accessTokensRepository) private accessTokensRepository: AccessTokensRepository, - - private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const query = this.accessTokensRepository.createQueryBuilder('token') @@ -73,8 +30,8 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('token.app', 'app'); switch (ps.sort) { - case '+createdAt': query.orderBy('token.id', 'DESC'); break; - case '-createdAt': query.orderBy('token.id', 'ASC'); break; + case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('token.createdAt', 'ASC'); break; case '+lastUsedAt': query.orderBy('token.lastUsedAt', 'DESC'); break; case '-lastUsedAt': query.orderBy('token.lastUsedAt', 'ASC'); break; default: query.orderBy('token.id', 'ASC'); break; @@ -85,9 +42,9 @@ export default class extends Endpoint { // eslint- return await Promise.all(tokens.map(token => ({ id: token.id, name: token.name ?? token.app?.name, - createdAt: this.idService.parse(token.id).date.toISOString(), - lastUsedAt: token.lastUsedAt?.toISOString(), - permission: token.app ? token.app.permission : token.permission, + createdAt: token.createdAt, + lastUsedAt: token.lastUsedAt, + permission: token.permission, }))); }); } diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts index 0b4faf5ef8..f5a946eb91 100644 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AccessTokensRepository } from '@/models/_.js'; +import type { AccessTokensRepository } from '@/models/index.js'; import { AppEntityService } from '@/core/entities/AppEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -14,40 +9,6 @@ export const meta = { requireCredential: true, secure: true, - - res: { - type: 'array', - items: { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - optional: false, - }, - name: { - type: 'string', - optional: false, - }, - callbackUrl: { - type: 'string', - optional: false, nullable: true, - }, - permission: { - type: 'array', - optional: false, - uniqueItems: true, - items: { - type: 'string', - }, - }, - isAuthorized: { - type: 'boolean', - optional: true, - }, - }, - }, - }, } as const; export const paramDef = { @@ -60,8 +21,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.accessTokensRepository) private accessTokensRepository: AccessTokensRepository, diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts index bb78d47149..873835a36c 100644 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ b/packages/backend/src/server/api/endpoints/i/change-password.ts @@ -1,14 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserProfilesRepository } from '@/models/_.js'; +import type { UserProfilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -import { UserAuthService } from '@/core/UserAuthService.js'; export const meta = { requireCredential: true, @@ -21,38 +15,24 @@ export const paramDef = { properties: { currentPassword: { type: 'string' }, newPassword: { type: 'string', minLength: 1 }, - token: { type: 'string', nullable: true }, }, required: ['currentPassword', 'newPassword'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - - private userAuthService: UserAuthService, ) { super(meta, paramDef, async (ps, me) => { - const token = ps.token; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - if (profile.twoFactorEnabled) { - if (token == null) { - throw new Error('authentication failed'); - } + // Compare password + const same = await bcrypt.compare(ps.currentPassword, profile.password!); - try { - await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (e) { - throw new Error('authentication failed'); - } - } - - const passwordMatched = await bcrypt.compare(ps.currentPassword, profile.password!); - - if (!passwordMatched) { + if (!same) { throw new Error('incorrect password'); } diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts index 0e42647ef7..4eef496385 100644 --- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts +++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts @@ -1,17 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AchievementService } from '@/core/AchievementService.js'; -import { ACHIEVEMENT_TYPES } from '@/models/UserProfile.js'; +import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js'; export const meta = { requireCredential: true, prohibitMoved: true, - kind: 'write:account', } as const; export const paramDef = { @@ -22,8 +15,9 @@ export const paramDef = { required: ['name'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private achievementService: AchievementService, ) { diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index bfa0b4605d..77a03d9811 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -1,15 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; +import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DeleteAccountService } from '@/core/DeleteAccountService.js'; import { DI } from '@/di-symbols.js'; -import { UserAuthService } from '@/core/UserAuthService.js'; export const meta = { requireCredential: true, @@ -21,13 +15,13 @@ export const paramDef = { type: 'object', properties: { password: { type: 'string' }, - token: { type: 'string', nullable: true }, }, required: ['password'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -35,32 +29,19 @@ export default class extends Endpoint { // eslint- @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - private userAuthService: UserAuthService, private deleteAccountService: DeleteAccountService, ) { super(meta, paramDef, async (ps, me) => { - const token = ps.token; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - - if (profile.twoFactorEnabled) { - if (token == null) { - throw new Error('authentication failed'); - } - - try { - await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (e) { - throw new Error('authentication failed'); - } - } - const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id }); if (userDetailed.isDeleted) { return; } - const passwordMatched = await bcrypt.compare(ps.password, profile.password!); - if (!passwordMatched) { + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); + + if (!same) { throw new Error('incorrect password'); } diff --git a/packages/backend/src/server/api/endpoints/i/export-antennas.ts b/packages/backend/src/server/api/endpoints/i/export-antennas.ts index 77fb4a895f..4182c1b247 100644 --- a/packages/backend/src/server/api/endpoints/i/export-antennas.ts +++ b/packages/backend/src/server/api/endpoints/i/export-antennas.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; diff --git a/packages/backend/src/server/api/endpoints/i/export-blocking.ts b/packages/backend/src/server/api/endpoints/i/export-blocking.ts index 7573018bec..4be88cbc2b 100644 --- a/packages/backend/src/server/api/endpoints/i/export-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/export-blocking.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -23,8 +18,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private queueService: QueueService, ) { diff --git a/packages/backend/src/server/api/endpoints/i/export-clips.ts b/packages/backend/src/server/api/endpoints/i/export-clips.ts deleted file mode 100644 index 10d1fdac73..0000000000 --- a/packages/backend/src/server/api/endpoints/i/export-clips.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueueService } from '@/core/QueueService.js'; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: ms('1day'), - max: 1, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private queueService: QueueService, - ) { - super(meta, paramDef, async (ps, me) => { - this.queueService.createExportClipsJob(me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/i/export-favorites.ts b/packages/backend/src/server/api/endpoints/i/export-favorites.ts index 5e03f70170..f522d4c409 100644 --- a/packages/backend/src/server/api/endpoints/i/export-favorites.ts +++ b/packages/backend/src/server/api/endpoints/i/export-favorites.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -23,8 +18,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private queueService: QueueService, ) { diff --git a/packages/backend/src/server/api/endpoints/i/export-following.ts b/packages/backend/src/server/api/endpoints/i/export-following.ts index 2e5ba14737..1741781c0f 100644 --- a/packages/backend/src/server/api/endpoints/i/export-following.ts +++ b/packages/backend/src/server/api/endpoints/i/export-following.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -26,8 +21,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private queueService: QueueService, ) { diff --git a/packages/backend/src/server/api/endpoints/i/export-mute.ts b/packages/backend/src/server/api/endpoints/i/export-mute.ts index 0384cf142b..8e8042b1f9 100644 --- a/packages/backend/src/server/api/endpoints/i/export-mute.ts +++ b/packages/backend/src/server/api/endpoints/i/export-mute.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -23,8 +18,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private queueService: QueueService, ) { diff --git a/packages/backend/src/server/api/endpoints/i/export-notes.ts b/packages/backend/src/server/api/endpoints/i/export-notes.ts index db4e78f667..ed54c9991c 100644 --- a/packages/backend/src/server/api/endpoints/i/export-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/export-notes.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -23,8 +18,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private queueService: QueueService, ) { diff --git a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts index 6cd662102c..5c2be38b71 100644 --- a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -23,8 +18,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private queueService: QueueService, ) { diff --git a/packages/backend/src/server/api/endpoints/i/favorites.ts b/packages/backend/src/server/api/endpoints/i/favorites.ts index 3558035eca..ce8ab4962a 100644 --- a/packages/backend/src/server/api/endpoints/i/favorites.ts +++ b/packages/backend/src/server/api/endpoints/i/favorites.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NoteFavoritesRepository } from '@/models/_.js'; +import type { NoteFavoritesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteFavoriteEntityService } from '@/core/entities/NoteFavoriteEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -38,8 +33,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.noteFavoritesRepository) private noteFavoritesRepository: NoteFavoritesRepository, @@ -53,7 +49,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('favorite.note', 'note'); const favorites = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.noteFavoriteEntityService.packMany(favorites, me); diff --git a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts index d492585ffa..d1b04cb655 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryLikesRepository } from '@/models/_.js'; +import type { GalleryLikesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { GalleryLikeEntityService } from '@/core/entities/GalleryLikeEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -49,8 +44,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.galleryLikesRepository) private galleryLikesRepository: GalleryLikesRepository, @@ -64,7 +60,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('like.post', 'post'); const likes = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.galleryLikeEntityService.packMany(likes, me); diff --git a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts index 73a6fcc98b..32d14293f7 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository } from '@/models/_.js'; +import type { GalleryPostsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -38,8 +33,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -52,7 +48,7 @@ export default class extends Endpoint { // eslint- .andWhere('post.userId = :meId', { meId: me.id }); const posts = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.galleryPostEntityService.packMany(posts, me); diff --git a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts new file mode 100644 index 0000000000..3179457817 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts @@ -0,0 +1,47 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { MutedNotesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['account'], + + requireCredential: true, + + kind: 'read:account', + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + count: { + type: 'number', + optional: false, nullable: false, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.mutedNotesRepository) + private mutedNotesRepository: MutedNotesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + return { + count: await this.mutedNotesRepository.countBy({ + userId: me.id, + reason: 'word', + }), + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts index ccec96ffbb..12ec5855d3 100644 --- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts +++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; -import type { AntennasRepository, DriveFilesRepository, UsersRepository, MiAntenna as _Antenna } from '@/models/_.js'; +import type { AntennasRepository, DriveFilesRepository, UsersRepository, Antenna as _Antenna } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { DownloadService } from '@/core/DownloadService.js'; @@ -16,7 +11,6 @@ import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, - requiredRolePolicy: 'canImportAntennas', prohibitMoved: true, limit: { @@ -72,14 +66,14 @@ export default class extends Endpoint { private downloadService: DownloadService, ) { super(meta, paramDef, async (ps, me) => { - const userExist = await this.usersRepository.exists({ where: { id: me.id } }); - if (!userExist) throw new ApiError(meta.errors.noSuchUser); + const users = await this.usersRepository.findOneBy({ id: me.id }); + if (users === null) throw new ApiError(meta.errors.noSuchUser); const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (file === null) throw new ApiError(meta.errors.noSuchFile); if (file.size === 0) throw new ApiError(meta.errors.emptyFile); const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url)); const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id }); - if (currentAntennasCount + antennas.length >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) { + if (currentAntennasCount + antennas.length > (await this.roleService.getUserPolicies(me.id)).antennaLimit) { throw new ApiError(meta.errors.tooManyAntennas); } this.queueService.createImportAntennasJob(me, antennas); diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts index 2fa450558b..32c16300fb 100644 --- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts @@ -1,21 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, - requiredRolePolicy: 'canImportBlocking', prohibitMoved: true, limit: { @@ -58,8 +52,9 @@ export const paramDef = { required: ['fileId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -76,7 +71,7 @@ export default class extends Endpoint { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts index 9186fca162..1926a1f503 100644 --- a/packages/backend/src/server/api/endpoints/i/import-following.ts +++ b/packages/backend/src/server/api/endpoints/i/import-following.ts @@ -1,21 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, - requiredRolePolicy: 'canImportFollowing', prohibitMoved: true, limit: { duration: ms('1hour'), @@ -53,13 +47,13 @@ export const paramDef = { type: 'object', properties: { fileId: { type: 'string', format: 'misskey:id' }, - withReplies: { type: 'boolean' }, }, required: ['fileId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -76,12 +70,12 @@ export default class extends Endpoint { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); - this.queueService.createImportFollowingJob(me, file.id, ps.withReplies); + this.queueService.createImportFollowingJob(me, file.id); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts index b6dbacd371..34f2627563 100644 --- a/packages/backend/src/server/api/endpoints/i/import-muting.ts +++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts @@ -1,21 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, - requiredRolePolicy: 'canImportMuting', prohibitMoved: true, limit: { @@ -58,8 +52,9 @@ export const paramDef = { required: ['fileId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -76,7 +71,7 @@ export default class extends Endpoint { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts index 5de0a70bbb..1b3cb5359d 100644 --- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts @@ -1,21 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, - requiredRolePolicy: 'canImportUserLists', prohibitMoved: true, limit: { duration: ms('1hour'), @@ -57,8 +51,9 @@ export const paramDef = { required: ['fileId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -75,7 +70,7 @@ export default class extends Endpoint { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index 7852b5a2e1..261dd527c0 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -1,15 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; +import type { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; + import { Endpoint } from '@/server/api/endpoint-base.js'; import { ApiError } from '@/server/api/error.js'; -import { MiLocalUser, MiRemoteUser } from '@/models/User.js'; +import { LocalUser, RemoteUser } from '@/models/entities/User.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; @@ -19,8 +17,6 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import * as Acct from '@/misc/acct.js'; -import { DI } from '@/di-symbols.js'; -import { MiMeta } from '@/models/_.js'; export const meta = { tags: ['users'], @@ -66,10 +62,6 @@ export const meta = { id: 'b234a14e-9ebe-4581-8000-074b3c215962', }, }, - - res: { - type: 'object', - }, } as const; export const paramDef = { @@ -80,11 +72,12 @@ export const paramDef = { required: ['moveToAccount'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, + @Inject(DI.config) + private config: Config, private remoteUserResolveService: RemoteUserResolveService, private apiLoggerService: ApiLoggerService, @@ -97,7 +90,7 @@ export default class extends Endpoint { // eslint- // check parameter if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchUser); // abort if user is the root - if (this.serverSettings.rootUserId === me.id) throw new ApiError(meta.errors.rootForbidden); + if (me.isRoot) throw new ApiError(meta.errors.rootForbidden); // abort if user has already moved if (me.movedToUri) throw new ApiError(meta.errors.alreadyMoved); @@ -108,7 +101,7 @@ export default class extends Endpoint { // eslint- this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`); throw new ApiError(meta.errors.noSuchUser); }); - const destination = await this.getterService.getUser(moveTo.id) as MiLocalUser | MiRemoteUser; + const destination = await this.getterService.getUser(moveTo.id) as LocalUser | RemoteUser; const newUri = this.userEntityService.getUserUri(destination); // update local db diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts deleted file mode 100644 index b9c41b057d..0000000000 --- a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts +++ /dev/null @@ -1,158 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { In } from 'typeorm'; -import * as Redis from 'ioredis'; -import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; -import { - obsoleteNotificationTypes, - groupedNotificationTypes, - FilterUnionByProperty, - notificationTypes, -} from '@/types.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; -import { NotificationService } from '@/core/NotificationService.js'; -import { DI } from '@/di-symbols.js'; -import { IdService } from '@/core/IdService.js'; -import { MiGroupedNotification, MiNotification } from '@/models/Notification.js'; - -export const meta = { - tags: ['account', 'notifications'], - - requireCredential: true, - - limit: { - duration: 30000, - max: 30, - }, - - kind: 'read:notifications', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Notification', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - markAsRead: { type: 'boolean', default: true }, - // 後方互換のため、廃止された通知タイプも受け付ける - includeTypes: { type: 'array', items: { - type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes], - } }, - excludeTypes: { type: 'array', items: { - type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes], - } }, - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, - - private idService: IdService, - private notificationEntityService: NotificationEntityService, - private notificationService: NotificationService, - ) { - super(meta, paramDef, async (ps, me) => { - const EXTRA_LIMIT = 100; - - // includeTypes が空の場合はクエリしない - if (ps.includeTypes && ps.includeTypes.length === 0) { - return []; - } - // excludeTypes に全指定されている場合はクエリしない - if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) { - return []; - } - - const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][]; - const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][]; - - const notifications = await this.notificationService.getNotifications(me.id, { - sinceId: ps.sinceId, - untilId: ps.untilId, - limit: ps.limit, - includeTypes, - excludeTypes, - }); - - if (notifications.length === 0) { - return []; - } - - // Mark all as read - if (ps.markAsRead) { - this.notificationService.readAllNotification(me.id); - } - - // grouping - let groupedNotifications = [notifications[0]] as MiGroupedNotification[]; - for (let i = 1; i < notifications.length; i++) { - const notification = notifications[i]; - const prev = notifications[i - 1]; - let prevGroupedNotification = groupedNotifications.at(-1)!; - - if (prev.type === 'reaction' && notification.type === 'reaction' && prev.noteId === notification.noteId) { - if (prevGroupedNotification.type !== 'reaction:grouped') { - groupedNotifications[groupedNotifications.length - 1] = { - type: 'reaction:grouped', - id: '', - createdAt: prev.createdAt, - noteId: prev.noteId!, - reactions: [{ - userId: prev.notifierId!, - reaction: prev.reaction!, - }], - }; - prevGroupedNotification = groupedNotifications.at(-1)!; - } - (prevGroupedNotification as FilterUnionByProperty).reactions.push({ - userId: notification.notifierId!, - reaction: notification.reaction!, - }); - prevGroupedNotification.id = notification.id; - continue; - } - if (prev.type === 'renote' && notification.type === 'renote' && prev.targetNoteId === notification.targetNoteId) { - if (prevGroupedNotification.type !== 'renote:grouped') { - groupedNotifications[groupedNotifications.length - 1] = { - type: 'renote:grouped', - id: '', - createdAt: notification.createdAt, - noteId: prev.noteId!, - userIds: [prev.notifierId!], - }; - prevGroupedNotification = groupedNotifications.at(-1)!; - } - (prevGroupedNotification as FilterUnionByProperty).userIds.push(notification.notifierId!); - prevGroupedNotification.id = notification.id; - continue; - } - - groupedNotifications.push(notification); - } - - groupedNotifications = groupedNotifications.slice(0, ps.limit); - - return await this.notificationEntityService.packGroupedMany(groupedNotifications, me.id); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index f5a48b2f69..f5662f4a0e 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -1,19 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { In } from 'typeorm'; +import { Brackets, In } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; -import { FilterUnionByProperty, notificationTypes, obsoleteNotificationTypes } from '@/types.js'; +import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotesRepository } from '@/models/index.js'; +import { obsoleteNotificationTypes, notificationTypes } from '@/types.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteReadService } from '@/core/NoteReadService.js'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; -import { MiNotification } from '@/models/Notification.js'; +import { Notification } from '@/models/entities/Notification.js'; export const meta = { tags: ['account', 'notifications'], @@ -56,18 +53,30 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.redis) private redisClient: Redis.Redis, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, private idService: IdService, private notificationEntityService: NotificationEntityService, private notificationService: NotificationService, + private queryService: QueryService, + private noteReadService: NoteReadService, ) { super(meta, paramDef, async (ps, me) => { // includeTypes が空の場合はクエリしない @@ -82,19 +91,43 @@ export default class extends Endpoint { // eslint- const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; - const notifications = await this.notificationService.getNotifications(me.id, { - sinceId: ps.sinceId, - untilId: ps.untilId, - limit: ps.limit, - includeTypes, - excludeTypes, - }); + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + const notificationsRes = await this.redisClient.xrevrange( + `notificationTimeline:${me.id}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-', + 'COUNT', limit); + + if (notificationsRes.length === 0) { + return []; + } + + let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as Notification[]; + + if (includeTypes && includeTypes.length > 0) { + notifications = notifications.filter(notification => includeTypes.includes(notification.type)); + } else if (excludeTypes && excludeTypes.length > 0) { + notifications = notifications.filter(notification => !excludeTypes.includes(notification.type)); + } + + if (notifications.length === 0) { + return []; + } // Mark all as read if (ps.markAsRead) { this.notificationService.readAllNotification(me.id); } + const noteIds = notifications + .filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)) + .map(notification => notification.noteId!); + + if (noteIds.length > 0) { + const notes = await this.notesRepository.findBy({ id: In(noteIds) }); + this.noteReadService.read(me.id, notes); + } + return await this.notificationEntityService.packMany(notifications, me.id); }); } diff --git a/packages/backend/src/server/api/endpoints/i/page-likes.ts b/packages/backend/src/server/api/endpoints/i/page-likes.ts index d4c09426a7..70e6e0a6a8 100644 --- a/packages/backend/src/server/api/endpoints/i/page-likes.ts +++ b/packages/backend/src/server/api/endpoints/i/page-likes.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { PageLikesRepository } from '@/models/_.js'; +import type { PageLikesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { PageLikeEntityService } from '@/core/entities/PageLikeEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -48,8 +43,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.pageLikesRepository) private pageLikesRepository: PageLikesRepository, @@ -63,7 +59,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('like.page', 'page'); const likes = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return this.pageLikeEntityService.packMany(likes, me); diff --git a/packages/backend/src/server/api/endpoints/i/pages.ts b/packages/backend/src/server/api/endpoints/i/pages.ts index 1b6359a633..285aa34e91 100644 --- a/packages/backend/src/server/api/endpoints/i/pages.ts +++ b/packages/backend/src/server/api/endpoints/i/pages.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { PagesRepository } from '@/models/_.js'; +import type { PagesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -38,8 +33,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, @@ -52,7 +48,7 @@ export default class extends Endpoint { // eslint- .andWhere('page.userId = :meId', { meId: me.id }); const pages = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.pageEntityService.packMany(pages); diff --git a/packages/backend/src/server/api/endpoints/i/pin.ts b/packages/backend/src/server/api/endpoints/i/pin.ts index b7cafd74df..2293500945 100644 --- a/packages/backend/src/server/api/endpoints/i/pin.ts +++ b/packages/backend/src/server/api/endpoints/i/pin.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -52,8 +47,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private userEntityService: UserEntityService, private notePiningService: NotePiningService, @@ -66,8 +62,8 @@ export default class extends Endpoint { // eslint- throw err; }); - return await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + return await this.userEntityService.pack(me.id, me, { + detail: true, }); }); } diff --git a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts new file mode 100644 index 0000000000..b92de4b739 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts @@ -0,0 +1,41 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { NoteUnreadsRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['account'], + + requireCredential: true, + + kind: 'write:account', +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.noteUnreadsRepository) + private noteUnreadsRepository: NoteUnreadsRepository, + + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + // Remove documents + await this.noteUnreadsRepository.delete({ + userId: me.id, + }); + + // 全て既読になったイベントを発行 + this.globalEventService.publishMainStream(me.id, 'readAllUnreadMentions'); + this.globalEventService.publishMainStream(me.id, 'readAllUnreadSpecifiedNotes'); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts index 4db1ca73c1..b8922b91e5 100644 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts @@ -1,11 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AnnouncementService } from '@/core/AnnouncementService.js'; +import { IdService } from '@/core/IdService.js'; +import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['account'], @@ -15,6 +15,11 @@ export const meta = { kind: 'write:account', errors: { + noSuchAnnouncement: { + message: 'No such announcement.', + code: 'NO_SUCH_ANNOUNCEMENT', + id: '184663db-df88-4bc2-8b52-fb85f0681939', + }, }, } as const; @@ -26,13 +31,49 @@ export const paramDef = { required: ['announcementId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private announcementService: AnnouncementService, + @Inject(DI.announcementsRepository) + private announcementsRepository: AnnouncementsRepository, + + @Inject(DI.announcementReadsRepository) + private announcementReadsRepository: AnnouncementReadsRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - await this.announcementService.read(me, ps.announcementId); + // Check if announcement exists + const announcement = await this.announcementsRepository.findOneBy({ id: ps.announcementId }); + + if (announcement == null) { + throw new ApiError(meta.errors.noSuchAnnouncement); + } + + // Check if already read + const read = await this.announcementReadsRepository.findOneBy({ + announcementId: ps.announcementId, + userId: me.id, + }); + + if (read != null) { + return; + } + + // Create read + await this.announcementReadsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + announcementId: ps.announcementId, + userId: me.id, + }); + + if (!await this.userEntityService.getHasUnreadAnnouncement(me.id)) { + this.globalEventService.publishMainStream(me.id, 'readAllAnnouncements'); + } }); } } diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index d25d5d5e0e..23ff63f5e9 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; -import { generateNativeUserToken } from '@/misc/token.js'; +import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import generateUserToken from '@/misc/generate-native-user-token.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -25,8 +20,9 @@ export const paramDef = { required: ['password'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -49,7 +45,7 @@ export default class extends Endpoint { // eslint- throw new Error('incorrect password'); } - const newToken = generateNativeUserToken(); + const newToken = generateUserToken(); await this.usersRepository.update(me.id, { token: newToken, diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index f1797cfde7..17154c1f76 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -1,19 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryApiService } from '@/core/RegistryApiService.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, - kind: 'read:account', - res: { - type: 'object', - }, + secure: true, } as const; export const paramDef = { @@ -22,18 +15,24 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, - domain: { type: 'string', nullable: true }, }, - required: ['scope'], + required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private registryApiService: RegistryApiService, + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, ) { - super(meta, paramDef, async (ps, me, accessToken) => { - const items = await this.registryApiService.getAllItemsOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope); + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.scope = :scope', { scope: ps.scope }); + + const items = await query.getMany(); const res = {} as Record; diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index d53c390460..233686dbe1 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -1,16 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryApiService } from '@/core/RegistryApiService.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, - kind: 'read:account', + + secure: true, errors: { noSuchKey: { @@ -19,19 +16,6 @@ export const meta = { id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a', }, }, - - res: { - type: 'object', - properties: { - updatedAt: { - type: 'string', - optional: false, - }, - value: { - optional: false, - }, - }, - }, } as const; export const paramDef = { @@ -41,25 +25,32 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, - domain: { type: 'string', nullable: true }, }, - required: ['key', 'scope'], + required: ['key'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private registryApiService: RegistryApiService, + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, ) { - super(meta, paramDef, async (ps, me, accessToken) => { - const item = await this.registryApiService.getItem(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key); + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.key = :key', { key: ps.key }) + .andWhere('item.scope = :scope', { scope: ps.scope }); + + const item = await query.getOne(); if (item == null) { throw new ApiError(meta.errors.noSuchKey); } return { - updatedAt: item.updatedAt.toISOString(), + updatedAt: item.updatedAt, value: item.value, }; }); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index d9a8fdd449..99cdf95bad 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -1,16 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryApiService } from '@/core/RegistryApiService.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, - kind: 'read:account', + + secure: true, errors: { noSuchKey: { @@ -19,10 +16,6 @@ export const meta = { id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a', }, }, - - res: { - type: 'object', - } } as const; export const paramDef = { @@ -32,18 +25,25 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, - domain: { type: 'string', nullable: true }, }, - required: ['key', 'scope'], + required: ['key'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private registryApiService: RegistryApiService, + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, ) { - super(meta, paramDef, async (ps, me, accessToken) => { - const item = await this.registryApiService.getItem(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key); + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.key = :key', { key: ps.key }) + .andWhere('item.scope = :scope', { scope: ps.scope }); + + const item = await query.getOne(); if (item == null) { throw new ApiError(meta.errors.noSuchKey); diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index 3fe339606d..362a5e89f4 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -1,22 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryApiService } from '@/core/RegistryApiService.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, - kind: 'read:account', - res: { - type: 'object', - additionalProperties: { - type: 'string', - }, - }, + secure: true, } as const; export const paramDef = { @@ -25,31 +15,37 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, - domain: { type: 'string', nullable: true }, }, - required: ['scope'], + required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private registryApiService: RegistryApiService, + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, ) { - super(meta, paramDef, async (ps, me, accessToken) => { - const items = await this.registryApiService.getAllItemsOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope); + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.scope = :scope', { scope: ps.scope }); + + const items = await query.getMany(); const res = {} as Record; for (const item of items) { const type = typeof item.value; res[item.key] = - item.value === null ? 'null' : - Array.isArray(item.value) ? 'array' : - type === 'number' ? 'number' : - type === 'string' ? 'string' : - type === 'boolean' ? 'boolean' : - type === 'object' ? 'object' : - null as never; + item.value === null ? 'null' : + Array.isArray(item.value) ? 'array' : + type === 'number' ? 'number' : + type === 'string' ? 'string' : + type === 'boolean' ? 'boolean' : + type === 'object' ? 'object' : + null as never; } return res; diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts index 28f158c62d..99f69d8bed 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -1,22 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryApiService } from '@/core/RegistryApiService.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, - kind: 'read:account', - res: { - type: 'array', - items: { - type: 'string', - }, - }, + secure: true, } as const; export const paramDef = { @@ -25,18 +15,27 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, - domain: { type: 'string', nullable: true }, }, - required: ['scope'], + required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private registryApiService: RegistryApiService, + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, ) { - super(meta, paramDef, async (ps, me, accessToken) => { - return await this.registryApiService.getAllKeysOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope); + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .select('item.key') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.scope = :scope', { scope: ps.scope }); + + const items = await query.getMany(); + + return items.map(x => x.key); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts index cf965ba0cf..78a641f5e2 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -1,18 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -import { RegistryApiService } from '@/core/RegistryApiService.js'; import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, - kind: 'write:account', + + secure: true, errors: { noSuchKey: { @@ -30,18 +25,31 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, - domain: { type: 'string', nullable: true }, }, - required: ['key', 'scope'], + required: ['key'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private registryApiService: RegistryApiService, + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, ) { - super(meta, paramDef, async (ps, me, accessToken) => { - await this.registryApiService.remove(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key); + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.key = :key', { key: ps.key }) + .andWhere('item.scope = :scope', { scope: ps.scope }); + + const item = await query.getOne(); + + if (item == null) { + throw new ApiError(meta.errors.noSuchKey); + } + + await this.registryItemsRepository.remove(item); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts deleted file mode 100644 index 67a99b028a..0000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryApiService } from '@/core/RegistryApiService.js'; - -export const meta = { - requireCredential: true, - secure: true, - - res: { - type: 'array', - items: { - type: 'object', - properties: { - scopes: { - type: 'array', - items: { - type: 'array', - items: { - type: 'string', - } - } - }, - domain: { - type: 'string', - nullable: true, - }, - }, - }, - } -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private registryApiService: RegistryApiService, - ) { - super(meta, paramDef, async (ps, me) => { - return await this.registryApiService.getAllScopeAndDomains(me.id); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts new file mode 100644 index 0000000000..0a4ecb9c51 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts @@ -0,0 +1,43 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + requireCredential: true, + + secure: true, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .select('item.scope') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }); + + const items = await query.getMany(); + + const res = [] as string[][]; + + for (const item of items) { + if (res.some(scope => scope.join('.') === item.scope.join('.'))) continue; + res.push(item.scope); + } + + return res; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts index 8723035d84..c8e72203c4 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -1,15 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { RegistryApiService } from '@/core/RegistryApiService.js'; +import type { RegistryItemsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, - kind: 'write:account', + + secure: true, } as const; export const paramDef = { @@ -20,18 +19,53 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, - domain: { type: 'string', nullable: true }, }, - required: ['key', 'value', 'scope'], + required: ['key', 'value'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private registryApiService: RegistryApiService, + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + + private idService: IdService, + private globalEventService: GlobalEventService, ) { - super(meta, paramDef, async (ps, me, accessToken) => { - await this.registryApiService.set(me.id, accessToken ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key, ps.value); + super(meta, paramDef, async (ps, me) => { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where('item.domain IS NULL') + .andWhere('item.userId = :userId', { userId: me.id }) + .andWhere('item.key = :key', { key: ps.key }) + .andWhere('item.scope = :scope', { scope: ps.scope }); + + const existingItem = await query.getOne(); + + if (existingItem) { + await this.registryItemsRepository.update(existingItem.id, { + updatedAt: new Date(), + value: ps.value, + }); + } else { + await this.registryItemsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + updatedAt: new Date(), + userId: me.id, + domain: null, + scope: ps.scope, + key: ps.key, + value: ps.value, + }); + } + + // TODO: サードパーティアプリが傍受出来てしまうのでどうにかする + this.globalEventService.publishMainStream(me.id, 'registryUpdated', { + scope: ps.scope, + key: ps.key, + value: ps.value, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index 08f5e3a7a1..93daeb0cd7 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -1,11 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AccessTokensRepository } from '@/models/_.js'; +import type { AccessTokensRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -15,49 +11,30 @@ export const meta = { } as const; export const paramDef = { - anyOf: [ - { - type: 'object', - properties: { - tokenId: { type: 'string', format: 'misskey:id' }, - }, - required: ['tokenId'], - }, - { - type: 'object', - properties: { - token: { type: 'string', nullable: true }, - }, - required: ['token'], - }, - ], + type: 'object', + properties: { + tokenId: { type: 'string', format: 'misskey:id' }, + }, + required: ['tokenId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.accessTokensRepository) private accessTokensRepository: AccessTokensRepository, + + private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - if ('tokenId' in ps) { - const tokenExist = await this.accessTokensRepository.exists({ where: { id: ps.tokenId } }); + const token = await this.accessTokensRepository.findOneBy({ id: ps.tokenId }); - if (tokenExist) { - await this.accessTokensRepository.delete({ - id: ps.tokenId, - userId: me.id, - }); - } - } else if (ps.token) { - const tokenExist = await this.accessTokensRepository.exists({ where: { token: ps.token } }); - - if (tokenExist) { - await this.accessTokensRepository.delete({ - token: ps.token, - userId: me.id, - }); - } + if (token) { + await this.accessTokensRepository.delete({ + id: ps.tokenId, + userId: me.id, + }); } }); } diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts index 76ad0bbe21..9b30a24336 100644 --- a/packages/backend/src/server/api/endpoints/i/signin-history.ts +++ b/packages/backend/src/server/api/endpoints/i/signin-history.ts @@ -1,28 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { SigninsRepository } from '@/models/_.js'; +import type { SigninsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { SigninEntityService } from '@/core/entities/SigninEntityService.js'; import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, - secure: true, - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Signin', - }, - }, + secure: true, } as const; export const paramDef = { @@ -35,8 +21,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, @@ -48,7 +35,7 @@ export default class extends Endpoint { // eslint- const query = this.queryService.makePaginationQuery(this.signinsRepository.createQueryBuilder('signin'), ps.sinceId, ps.untilId) .andWhere('signin.userId = :meId', { meId: me.id }); - const history = await query.limit(ps.limit).getMany(); + const history = await query.take(ps.limit).getMany(); return await Promise.all(history.map(record => this.signinEntityService.pack(record))); }); diff --git a/packages/backend/src/server/api/endpoints/i/unpin.ts b/packages/backend/src/server/api/endpoints/i/unpin.ts index 74825cf9f3..db239dc284 100644 --- a/packages/backend/src/server/api/endpoints/i/unpin.ts +++ b/packages/backend/src/server/api/endpoints/i/unpin.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -39,8 +34,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private userEntityService: UserEntityService, private notePiningService: NotePiningService, @@ -51,8 +47,8 @@ export default class extends Endpoint { // eslint- throw err; }); - return await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + return await this.userEntityService.pack(me.id, me, { + detail: true, }); }); } diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index da1faee30d..58e056bd37 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -1,20 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import bcrypt from 'bcryptjs'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MiMeta, UserProfilesRepository } from '@/models/_.js'; +import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { EmailService } from '@/core/EmailService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; -import { UserAuthService } from '@/core/UserAuthService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -39,17 +33,6 @@ export const meta = { code: 'UNAVAILABLE', id: 'a2defefb-f220-8849-0af6-17f816099323', }, - - emailRequired: { - message: 'Email address is required.', - code: 'EMAIL_REQUIRED', - id: '324c7a88-59f2-492f-903f-89134f93e47e', - }, - }, - - res: { - type: 'object', - ref: 'MeDetailed', }, } as const; @@ -58,46 +41,34 @@ export const paramDef = { properties: { password: { type: 'string' }, email: { type: 'string', nullable: true }, - token: { type: 'string', nullable: true }, }, required: ['password'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private serverSettings: MiMeta, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, private userEntityService: UserEntityService, private emailService: EmailService, - private userAuthService: UserAuthService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - const token = ps.token; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - if (profile.twoFactorEnabled) { - if (token == null) { - throw new Error('authentication failed'); - } + // Compare password + const same = await bcrypt.compare(ps.password, profile.password!); - try { - await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (e) { - throw new Error('authentication failed'); - } - } - - const passwordMatched = await bcrypt.compare(ps.password, profile.password!); - if (!passwordMatched) { + if (!same) { throw new ApiError(meta.errors.incorrectPassword); } @@ -106,8 +77,6 @@ export default class extends Endpoint { // eslint- if (!res.available) { throw new ApiError(meta.errors.unavailable); } - } else if (this.serverSettings.emailRequiredForSignup) { - throw new ApiError(meta.errors.emailRequired); } await this.userProfilesRepository.update(me.id, { @@ -117,7 +86,7 @@ export default class extends Endpoint { // eslint- }); const iObj = await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, }); diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 082d97f5d4..8f5e6177c2 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -1,20 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import RE2 from 're2'; import * as mfm from 'mfm-js'; import { Inject, Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { JSDOM } from 'jsdom'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; import * as Acct from '@/misc/acct.js'; -import type { UsersRepository, DriveFilesRepository, MiMeta, UserProfilesRepository, PagesRepository } from '@/models/_.js'; -import type { MiLocalUser, MiUser } from '@/models/User.js'; -import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js'; -import type { MiUserProfile } from '@/models/UserProfile.js'; +import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/entities/User.js'; +import type { UserProfile } from '@/models/entities/UserProfile.js'; +import { notificationTypes } from '@/types.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { langmap } from '@/misc/langmap.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -22,18 +16,13 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; -import { UtilityService } from '@/core/UtilityService.js'; import { HashtagService } from '@/core/HashtagService.js'; import { DI } from '@/di-symbols.js'; -import { RolePolicies, RoleService } from '@/core/RoleService.js'; +import { RoleService } from '@/core/RoleService.js'; import { CacheService } from '@/core/CacheService.js'; +import { AccountMoveService } from '@/core/AccountMoveService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import { HttpRequestService } from '@/core/HttpRequestService.js'; -import type { Config } from '@/config.js'; -import { safeForSql } from '@/misc/safe-for-sql.js'; -import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; -import { notificationRecieveConfig } from '@/models/json-schema/user.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import { ApiError } from '../../error.js'; @@ -44,11 +33,6 @@ export const meta = { kind: 'write:account', - limit: { - duration: ms('1hour'), - max: 20, - }, - errors: { noSuchAvatar: { message: 'No such avatar file.', @@ -115,13 +99,6 @@ export const meta = { code: 'RESTRICTED_BY_ROLE', id: '8feff0ba-5ab5-585b-31f4-4df816663fad', }, - - nameContainsProhibitedWords: { - message: 'Your new name contains prohibited words.', - code: 'YOUR_NAME_CONTAINS_PROHIBITED_WORDS', - id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191', - httpStatusCode: 422, - }, }, res: { @@ -131,32 +108,15 @@ export const meta = { }, } as const; -const muteWords = { type: 'array', items: { oneOf: [ - { type: 'array', items: { type: 'string' } }, - { type: 'string' }, -] } } as const; - export const paramDef = { type: 'object', properties: { name: { ...nameSchema, nullable: true }, description: { ...descriptionSchema, nullable: true }, - followedMessage: { ...followedMessageSchema, nullable: true }, location: { ...locationSchema, nullable: true }, birthday: { ...birthdaySchema, nullable: true }, lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true }, avatarId: { type: 'string', format: 'misskey:id', nullable: true }, - avatarDecorations: { type: 'array', maxItems: 16, items: { - type: 'object', - properties: { - id: { type: 'string', format: 'misskey:id' }, - angle: { type: 'number', nullable: true, maximum: 0.5, minimum: -0.5 }, - flipH: { type: 'boolean', nullable: true }, - offsetX: { type: 'number', nullable: true, maximum: 0.25, minimum: -0.25 }, - offsetY: { type: 'number', nullable: true, maximum: 0.25, minimum: -0.25 }, - }, - required: ['id'], - } }, bannerId: { type: 'string', format: 'misskey:id', nullable: true }, fields: { type: 'array', @@ -179,45 +139,21 @@ export const paramDef = { autoAcceptFollowed: { type: 'boolean' }, noCrawle: { type: 'boolean' }, preventAiLearning: { type: 'boolean' }, - requireSigninToViewContents: { type: 'boolean' }, - makeNotesFollowersOnlyBefore: { type: 'integer', nullable: true }, - makeNotesHiddenBefore: { type: 'integer', nullable: true }, isBot: { type: 'boolean' }, isCat: { type: 'boolean' }, injectFeaturedNote: { type: 'boolean' }, receiveAnnouncementEmail: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' }, autoSensitive: { type: 'boolean' }, - followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, - followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, - chatScope: { type: 'string', enum: ['everyone', 'followers', 'following', 'mutual', 'none'] }, + ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true }, - mutedWords: muteWords, - hardMutedWords: muteWords, + mutedWords: { type: 'array' }, mutedInstances: { type: 'array', items: { type: 'string', } }, - notificationRecieveConfig: { - type: 'object', - nullable: false, - properties: { - note: notificationRecieveConfig, - follow: notificationRecieveConfig, - mention: notificationRecieveConfig, - reply: notificationRecieveConfig, - renote: notificationRecieveConfig, - quote: notificationRecieveConfig, - reaction: notificationRecieveConfig, - pollEnded: notificationRecieveConfig, - receiveFollowRequest: notificationRecieveConfig, - followRequestAccepted: notificationRecieveConfig, - roleAssigned: notificationRecieveConfig, - chatRoomInvitationReceived: notificationRecieveConfig, - achievementEarned: notificationRecieveConfig, - app: notificationRecieveConfig, - test: notificationRecieveConfig, - }, - }, + mutingNotificationTypes: { type: 'array', items: { + type: 'string', enum: notificationTypes, + } }, emailNotificationTypes: { type: 'array', items: { type: 'string', } }, @@ -230,15 +166,10 @@ export const paramDef = { }, } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.meta) - private instanceMeta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -256,55 +187,38 @@ export default class extends Endpoint { // eslint- private globalEventService: GlobalEventService, private userFollowingService: UserFollowingService, private accountUpdateService: AccountUpdateService, + private accountMoveService: AccountMoveService, private remoteUserResolveService: RemoteUserResolveService, private apiLoggerService: ApiLoggerService, private hashtagService: HashtagService, private roleService: RoleService, private cacheService: CacheService, - private httpRequestService: HttpRequestService, - private avatarDecorationService: AvatarDecorationService, - private utilityService: UtilityService, ) { super(meta, paramDef, async (ps, _user, token) => { - const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser; + const user = await this.usersRepository.findOneByOrFail({ id: _user.id }); const isSecure = token == null; - const updates = {} as Partial; - const profileUpdates = {} as Partial; + const updates = {} as Partial; + const profileUpdates = {} as Partial; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - let policies: RolePolicies | null = null; - if (ps.name !== undefined) { - if (ps.name === null) { - updates.name = null; - } else { - const trimmedName = ps.name.trim(); - updates.name = trimmedName === '' ? null : trimmedName; - } - } + if (ps.name !== undefined) updates.name = ps.name; if (ps.description !== undefined) profileUpdates.description = ps.description; - if (ps.followedMessage !== undefined) profileUpdates.followedMessage = ps.followedMessage; if (ps.lang !== undefined) profileUpdates.lang = ps.lang; if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; - if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility; - if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility; - if (ps.chatScope !== undefined) updates.chatScope = ps.chatScope; - - function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { + if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; + if (ps.mutedWords !== undefined) { // TODO: ちゃんと数える - const length = JSON.stringify(mutedWords).length; - if (length > limit) { + const length = JSON.stringify(ps.mutedWords).length; + if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) { throw new ApiError(meta.errors.tooManyMutedWords); } - } - function validateMuteWordRegex(mutedWords: (string[] | string)[]) { - for (const mutedWord of mutedWords) { - if (typeof mutedWord !== 'string') continue; - - const regexp = mutedWord.match(/^\/(.+)\/(.*)$/); + // validate regular expression syntax + ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => { + const regexp = x.match(/^\/(.+)\/(.*)$/); if (!regexp) throw new ApiError(meta.errors.invalidRegexp); try { @@ -312,25 +226,13 @@ export default class extends Endpoint { // eslint- } catch (err) { throw new ApiError(meta.errors.invalidRegexp); } - } - } - - if (ps.mutedWords !== undefined) { - policies ??= await this.roleService.getUserPolicies(user.id); - checkMuteWordCount(ps.mutedWords, policies.wordMuteLimit); - validateMuteWordRegex(ps.mutedWords); + }); profileUpdates.mutedWords = ps.mutedWords; profileUpdates.enableWordMute = ps.mutedWords.length > 0; } - if (ps.hardMutedWords !== undefined) { - policies ??= await this.roleService.getUserPolicies(user.id); - checkMuteWordCount(ps.hardMutedWords, policies.wordMuteLimit); - validateMuteWordRegex(ps.hardMutedWords); - profileUpdates.hardMutedWords = ps.hardMutedWords; - } if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; - if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig; + if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][]; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable; if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus; @@ -340,24 +242,17 @@ export default class extends Endpoint { // eslint- if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning; - if (typeof ps.requireSigninToViewContents === 'boolean') updates.requireSigninToViewContents = ps.requireSigninToViewContents; - if ((typeof ps.makeNotesFollowersOnlyBefore === 'number') || (ps.makeNotesFollowersOnlyBefore === null)) updates.makeNotesFollowersOnlyBefore = ps.makeNotesFollowersOnlyBefore; - if ((typeof ps.makeNotesHiddenBefore === 'number') || (ps.makeNotesHiddenBefore === null)) updates.makeNotesHiddenBefore = ps.makeNotesHiddenBefore; if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.alwaysMarkNsfw === 'boolean') { - policies ??= await this.roleService.getUserPolicies(user.id); - if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole); + if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole); profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; } if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive; if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; if (ps.avatarId) { - policies ??= await this.roleService.getUserPolicies(user.id); - if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole); - const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId }); if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); @@ -373,9 +268,6 @@ export default class extends Endpoint { // eslint- } if (ps.bannerId) { - policies ??= await this.roleService.getUserPolicies(user.id); - if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole); - const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId }); if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); @@ -390,26 +282,6 @@ export default class extends Endpoint { // eslint- updates.bannerBlurhash = null; } - if (ps.avatarDecorations) { - policies ??= await this.roleService.getUserPolicies(user.id); - const decorations = await this.avatarDecorationService.getAll(true); - const myRoles = await this.roleService.getUserRoles(user.id); - const allRoles = await this.roleService.getRoles(); - const decorationIds = decorations - .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) - .map(d => d.id); - - if (ps.avatarDecorations.length > policies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); - - updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ - id: d.id, - angle: d.angle ?? 0, - flipH: d.flipH ?? false, - offsetX: d.offsetX ?? 0, - offsetY: d.offsetY ?? 0, - })); - } - if (ps.pinnedPageId) { const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId }); @@ -422,9 +294,9 @@ export default class extends Endpoint { // eslint- if (ps.fields) { profileUpdates.fields = ps.fields - .filter(x => typeof x.name === 'string' && x.name.trim() !== '' && typeof x.value === 'string' && x.value.trim() !== '') + .filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '') .map(x => { - return { name: x.name.trim(), value: x.value.trim() }; + return { name: x.name, value: x.value }; }); } @@ -467,40 +339,16 @@ export default class extends Endpoint { // eslint- const newName = updates.name === undefined ? user.name : updates.name; const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; - const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields; - const newFollowedMessage = profileUpdates.followedMessage === undefined ? profile.followedMessage : profileUpdates.followedMessage; if (newName != null) { - let hasProhibitedWords = false; - if (!await this.roleService.isModerator(user)) { - hasProhibitedWords = this.utilityService.isKeyWordIncluded(newName, this.instanceMeta.prohibitedWordsForNameOfUser); - } - if (hasProhibitedWords) { - throw new ApiError(meta.errors.nameContainsProhibitedWords); - } - const tokens = mfm.parseSimple(newName); - emojis = emojis.concat(extractCustomEmojisFromMfm(tokens)); + emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); } if (newDescription != null) { const tokens = mfm.parse(newDescription); - emojis = emojis.concat(extractCustomEmojisFromMfm(tokens)); - tags = extractHashtags(tokens).map(tag => normalizeForSearch(tag)).splice(0, 32); - } - - for (const field of newFields) { - const nameTokens = mfm.parseSimple(field.name); - const valueTokens = mfm.parseSimple(field.value); - emojis = emojis.concat([ - ...extractCustomEmojisFromMfm(nameTokens), - ...extractCustomEmojisFromMfm(valueTokens), - ]); - } - - if (newFollowedMessage != null) { - const tokens = mfm.parse(newFollowedMessage); - emojis = emojis.concat(extractCustomEmojisFromMfm(tokens)); + emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); + tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32); } updates.emojis = emojis; @@ -510,18 +358,14 @@ export default class extends Endpoint { // eslint- this.hashtagService.updateUsertags(user, tags); //#endregion - if (Object.keys(updates).length > 0) { - await this.usersRepository.update(user.id, updates); - this.globalEventService.publishInternalEvent('localUserUpdated', { id: user.id }); + if (Object.keys(updates).length > 0) await this.usersRepository.update(user.id, updates); + if (Object.keys(updates).includes('alsoKnownAs')) { + this.cacheService.uriPersonCache.set(this.userEntityService.genLocalUserUri(user.id), { ...user, ...updates }); } + if (Object.keys(profileUpdates).length > 0) await this.userProfilesRepository.update(user.id, profileUpdates); - await this.userProfilesRepository.update(user.id, { - ...profileUpdates, - verifiedLinks: [], - }); - - const iObj = await this.userEntityService.pack(user.id, user, { - schema: 'MeDetailed', + const iObj = await this.userEntityService.pack(user.id, user, { + detail: true, includeSecrets: isSecure, }); @@ -540,44 +384,7 @@ export default class extends Endpoint { // eslint- // フォロワーにUpdateを配信 this.accountUpdateService.publishToFollowers(user.id); - const urls = updatedProfile.fields.filter(x => x.value.startsWith('https://')); - for (const url of urls) { - this.verifyLink(url.value, user); - } - return iObj; }); } - - private async verifyLink(url: string, user: MiLocalUser) { - if (!safeForSql(url)) return; - - try { - const html = await this.httpRequestService.getHtml(url); - - const { window } = new JSDOM(html); - const doc: Document = window.document; - - const myLink = `${this.config.url}/@${user.username}`; - - const aEls = Array.from(doc.getElementsByTagName('a')); - const linkEls = Array.from(doc.getElementsByTagName('link')); - - const includesMyLink = aEls.some(a => a.href === myLink); - const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink); - - if (includesMyLink || includesRelMeLinks) { - await this.userProfilesRepository.createQueryBuilder('profile').update() - .where('userId = :userId', { userId: user.id }) - .set({ - verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている - }) - .execute(); - } - - window.close(); - } catch (err) { - // なにもしない - } - } } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index 6e84603f7a..51fcce6cf0 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -1,19 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; -import type { WebhooksRepository } from '@/models/_.js'; -import { webhookEventTypes } from '@/models/Webhook.js'; +import type { WebhooksRepository } from '@/models/index.js'; +import { webhookEventTypes } from '@/models/entities/Webhook.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '@/server/api/error.js'; -// TODO: UserWebhook schemaの適用 export const meta = { tags: ['webhooks'], @@ -28,33 +22,6 @@ export const meta = { id: '87a9bb19-111e-4e37-81d3-a3e7426453b0', }, }, - - res: { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - userId: { - type: 'string', - format: 'misskey:id', - }, - name: { type: 'string' }, - on: { - type: 'array', - items: { - type: 'string', - enum: webhookEventTypes, - }, - }, - url: { type: 'string' }, - secret: { type: 'string' }, - active: { type: 'boolean' }, - latestSentAt: { type: 'string', format: 'date-time', nullable: true }, - latestStatus: { type: 'integer', nullable: true }, - }, - }, } as const; export const paramDef = { @@ -62,18 +29,19 @@ export const paramDef = { properties: { name: { type: 'string', minLength: 1, maxLength: 100 }, url: { type: 'string', minLength: 1, maxLength: 1024 }, - secret: { type: 'string', maxLength: 1024, default: '' }, + secret: { type: 'string', minLength: 1, maxLength: 1024 }, on: { type: 'array', items: { type: 'string', enum: webhookEventTypes, } }, }, - required: ['name', 'url', 'on'], + required: ['name', 'url', 'secret', 'on'], } as const; // TODO: ロジックをサービスに切り出す +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.webhooksRepository) private webhooksRepository: WebhooksRepository, @@ -86,32 +54,23 @@ export default class extends Endpoint { // eslint- const currentWebhooksCount = await this.webhooksRepository.countBy({ userId: me.id, }); - if (currentWebhooksCount >= (await this.roleService.getUserPolicies(me.id)).webhookLimit) { + if (currentWebhooksCount > (await this.roleService.getUserPolicies(me.id)).webhookLimit) { throw new ApiError(meta.errors.tooManyWebhooks); } - const webhook = await this.webhooksRepository.insertOne({ - id: this.idService.gen(), + const webhook = await this.webhooksRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), userId: me.id, name: ps.name, url: ps.url, secret: ps.secret, on: ps.on, - }); + }).then(x => this.webhooksRepository.findOneByOrFail(x.identifiers[0])); this.globalEventService.publishInternalEvent('webhookCreated', webhook); - return { - id: webhook.id, - userId: webhook.userId, - name: webhook.name, - on: webhook.on, - url: webhook.url, - secret: webhook.secret, - active: webhook.active, - latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null, - latestStatus: webhook.latestStatus, - }; + return webhook; }); } } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts index 1b1ac00670..7bdad136aa 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { WebhooksRepository } from '@/models/_.js'; +import type { WebhooksRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -36,8 +31,9 @@ export const paramDef = { // TODO: ロジックをサービスに切り出す +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.webhooksRepository) private webhooksRepository: WebhooksRepository, diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index 394c178f2a..58c84938cc 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -1,51 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { webhookEventTypes } from '@/models/Webhook.js'; -import type { WebhooksRepository } from '@/models/_.js'; +import type { WebhooksRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -// TODO: UserWebhook schemaの適用 export const meta = { tags: ['webhooks', 'account'], requireCredential: true, kind: 'read:account', - - res: { - type: 'array', - items: { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - userId: { - type: 'string', - format: 'misskey:id', - }, - name: { type: 'string' }, - on: { - type: 'array', - items: { - type: 'string', - enum: webhookEventTypes, - }, - }, - url: { type: 'string' }, - secret: { type: 'string' }, - active: { type: 'boolean' }, - latestSentAt: { type: 'string', format: 'date-time', nullable: true }, - latestStatus: { type: 'integer', nullable: true }, - }, - }, - }, } as const; export const paramDef = { @@ -54,8 +17,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.webhooksRepository) private webhooksRepository: WebhooksRepository, @@ -65,19 +29,7 @@ export default class extends Endpoint { // eslint- userId: me.id, }); - return webhooks.map(webhook => ( - { - id: webhook.id, - userId: webhook.userId, - name: webhook.name, - on: webhook.on, - url: webhook.url, - secret: webhook.secret, - active: webhook.active, - latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null, - latestStatus: webhook.latestStatus, - } - )); + return webhooks; }); } } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index 4a0c09ff0c..d15ca0050d 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -1,16 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { webhookEventTypes } from '@/models/Webhook.js'; -import type { WebhooksRepository } from '@/models/_.js'; +import type { WebhooksRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -// TODO: UserWebhook schemaの適用 export const meta = { tags: ['webhooks'], @@ -25,33 +18,6 @@ export const meta = { id: '50f614d9-3047-4f7e-90d8-ad6b2d5fb098', }, }, - - res: { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - userId: { - type: 'string', - format: 'misskey:id', - }, - name: { type: 'string' }, - on: { - type: 'array', - items: { - type: 'string', - enum: webhookEventTypes, - }, - }, - url: { type: 'string' }, - secret: { type: 'string' }, - active: { type: 'boolean' }, - latestSentAt: { type: 'string', format: 'date-time', nullable: true }, - latestStatus: { type: 'integer', nullable: true }, - }, - }, } as const; export const paramDef = { @@ -62,8 +28,9 @@ export const paramDef = { required: ['webhookId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.webhooksRepository) private webhooksRepository: WebhooksRepository, @@ -78,17 +45,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchWebhook); } - return { - id: webhook.id, - userId: webhook.userId, - name: webhook.name, - on: webhook.on, - url: webhook.url, - secret: webhook.secret, - active: webhook.active, - latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null, - latestStatus: webhook.latestStatus, - }; + return webhook; }); } } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/test.ts b/packages/backend/src/server/api/endpoints/i/webhooks/test.ts deleted file mode 100644 index 2bf6df9ce2..0000000000 --- a/packages/backend/src/server/api/endpoints/i/webhooks/test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { webhookEventTypes } from '@/models/Webhook.js'; -import { WebhookTestService } from '@/core/WebhookTestService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['webhooks'], - - requireCredential: true, - secure: true, - kind: 'read:account', - - limit: { - duration: ms('15min'), - max: 60, - }, - - errors: { - noSuchWebhook: { - message: 'No such webhook.', - code: 'NO_SUCH_WEBHOOK', - id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - webhookId: { - type: 'string', - format: 'misskey:id', - }, - type: { - type: 'string', - enum: webhookEventTypes, - }, - override: { - type: 'object', - properties: { - url: { type: 'string' }, - secret: { type: 'string' }, - }, - }, - }, - required: ['webhookId', 'type'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private webhookTestService: WebhookTestService, - ) { - super(meta, paramDef, async (ps, me) => { - try { - await this.webhookTestService.testUserWebhook({ - webhookId: ps.webhookId, - type: ps.type, - override: ps.override, - }, me); - } catch (e) { - if (e instanceof WebhookTestService.NoSuchWebhookError) { - throw new ApiError(meta.errors.noSuchWebhook); - } - throw e; - } - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts index 07a25bd82a..8ec308eda7 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { WebhooksRepository } from '@/models/_.js'; -import { webhookEventTypes } from '@/models/Webhook.js'; +import type { WebhooksRepository } from '@/models/index.js'; +import { webhookEventTypes } from '@/models/entities/Webhook.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -34,19 +29,20 @@ export const paramDef = { webhookId: { type: 'string', format: 'misskey:id' }, name: { type: 'string', minLength: 1, maxLength: 100 }, url: { type: 'string', minLength: 1, maxLength: 1024 }, - secret: { type: 'string', nullable: true, maxLength: 1024 }, + secret: { type: 'string', minLength: 1, maxLength: 1024 }, on: { type: 'array', items: { type: 'string', enum: webhookEventTypes, } }, active: { type: 'boolean' }, }, - required: ['webhookId'], + required: ['webhookId', 'name', 'url', 'secret', 'on', 'active'], } as const; // TODO: ロジックをサービスに切り出す +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.webhooksRepository) private webhooksRepository: WebhooksRepository, @@ -66,7 +62,7 @@ export default class extends Endpoint { // eslint- await this.webhooksRepository.update(webhook.id, { name: ps.name, url: ps.url, - secret: ps.secret === null ? '' : ps.secret, + secret: ps.secret, on: ps.on, active: ps.active, }); diff --git a/packages/backend/src/server/api/endpoints/invite.ts b/packages/backend/src/server/api/endpoints/invite.ts new file mode 100644 index 0000000000..276adcb07f --- /dev/null +++ b/packages/backend/src/server/api/endpoints/invite.ts @@ -0,0 +1,60 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RegistrationTicketsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { DI } from '@/di-symbols.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; + +export const meta = { + tags: ['meta'], + + requireCredential: true, + requireRolePolicy: 'canInvite', + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + code: { + type: 'string', + optional: false, nullable: false, + example: '2ERUA5VR', + maxLength: 8, + minLength: 8, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.registrationTicketsRepository) + private registrationTicketsRepository: RegistrationTicketsRepository, + + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const code = secureRndstr(8, { + chars: '23456789ABCDEFGHJKLMNPQRSTUVWXYZ', // [0-9A-Z] w/o [01IO] (32 patterns) + }); + + await this.registrationTicketsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + code, + }); + + return { + code, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/invite/create.ts b/packages/backend/src/server/api/endpoints/invite/create.ts deleted file mode 100644 index f2e683ddf2..0000000000 --- a/packages/backend/src/server/api/endpoints/invite/create.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { MoreThan } from 'typeorm'; -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistrationTicketsRepository } from '@/models/_.js'; -import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js'; -import { IdService } from '@/core/IdService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { DI } from '@/di-symbols.js'; -import { generateInviteCode } from '@/misc/generate-invite-code.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['meta'], - - requireCredential: true, - requiredRolePolicy: 'canInvite', - kind: 'write:invite-codes', - - errors: { - exceededCreateLimit: { - message: 'You have exceeded the limit for creating an invitation code.', - code: 'EXCEEDED_LIMIT_OF_CREATE_INVITE_CODE', - id: '8b165dd3-6f37-4557-8db1-73175d63c641', - }, - }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'InviteCode', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.registrationTicketsRepository) - private registrationTicketsRepository: RegistrationTicketsRepository, - - private inviteCodeEntityService: InviteCodeEntityService, - private idService: IdService, - private roleService: RoleService, - ) { - super(meta, paramDef, async (ps, me) => { - const policies = await this.roleService.getUserPolicies(me.id); - - if (policies.inviteLimit) { - const count = await this.registrationTicketsRepository.countBy({ - id: MoreThan(this.idService.gen(Date.now() - (policies.inviteLimitCycle * 1000 * 60))), - createdById: me.id, - }); - - if (count >= policies.inviteLimit) { - throw new ApiError(meta.errors.exceededCreateLimit); - } - } - - const ticket = await this.registrationTicketsRepository.insertOne({ - id: this.idService.gen(), - createdBy: me, - createdById: me.id, - expiresAt: policies.inviteExpirationTime ? new Date(Date.now() + (policies.inviteExpirationTime * 1000 * 60)) : null, - code: generateInviteCode(), - }); - - return await this.inviteCodeEntityService.pack(ticket, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/invite/delete.ts b/packages/backend/src/server/api/endpoints/invite/delete.ts deleted file mode 100644 index 06f47e90bc..0000000000 --- a/packages/backend/src/server/api/endpoints/invite/delete.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistrationTicketsRepository } from '@/models/_.js'; -import { RoleService } from '@/core/RoleService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['meta'], - - requireCredential: true, - requiredRolePolicy: 'canInvite', - kind: 'write:invite-codes', - - errors: { - noSuchCode: { - message: 'No such invite code.', - code: 'NO_SUCH_INVITE_CODE', - id: 'cd4f9ae4-7854-4e3e-8df9-c296f051e634', - }, - - cantDelete: { - message: 'You can\'t delete this invite code.', - code: 'CAN_NOT_DELETE_INVITE_CODE', - id: 'ff17af39-000c-4d4e-abdf-848fa30fc1ce', - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '5eb8d909-2540-4970-90b8-dd6f86088121', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - inviteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['inviteId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.registrationTicketsRepository) - private registrationTicketsRepository: RegistrationTicketsRepository, - - private roleService: RoleService, - ) { - super(meta, paramDef, async (ps, me) => { - const ticket = await this.registrationTicketsRepository.findOneBy({ id: ps.inviteId }); - const isModerator = await this.roleService.isModerator(me); - - if (ticket == null) { - throw new ApiError(meta.errors.noSuchCode); - } - - if (ticket.createdById !== me.id && !isModerator) { - throw new ApiError(meta.errors.accessDenied); - } - - if (ticket.usedAt && !isModerator) { - throw new ApiError(meta.errors.cantDelete); - } - - await this.registrationTicketsRepository.delete(ticket.id); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/invite/limit.ts b/packages/backend/src/server/api/endpoints/invite/limit.ts deleted file mode 100644 index 0067dce231..0000000000 --- a/packages/backend/src/server/api/endpoints/invite/limit.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { MoreThan } from 'typeorm'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistrationTicketsRepository } from '@/models/_.js'; -import { RoleService } from '@/core/RoleService.js'; -import { DI } from '@/di-symbols.js'; -import { IdService } from '@/core/IdService.js'; - -export const meta = { - tags: ['meta'], - - requireCredential: true, - requiredRolePolicy: 'canInvite', - kind: 'read:invite-codes', - - res: { - type: 'object', - optional: false, nullable: false, - properties: { - remaining: { - type: 'integer', - optional: false, nullable: true, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.registrationTicketsRepository) - private registrationTicketsRepository: RegistrationTicketsRepository, - - private roleService: RoleService, - private idService: IdService, - ) { - super(meta, paramDef, async (ps, me) => { - const policies = await this.roleService.getUserPolicies(me.id); - - const count = policies.inviteLimit ? await this.registrationTicketsRepository.countBy({ - id: MoreThan(this.idService.gen(Date.now() - (policies.inviteLimitCycle * 60 * 1000))), - createdById: me.id, - }) : null; - - return { - remaining: count !== null ? Math.max(0, policies.inviteLimit - count) : null, - }; - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/invite/list.ts b/packages/backend/src/server/api/endpoints/invite/list.ts deleted file mode 100644 index a99974a91e..0000000000 --- a/packages/backend/src/server/api/endpoints/invite/list.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistrationTicketsRepository } from '@/models/_.js'; -import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js'; -import { QueryService } from '@/core/QueryService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['meta'], - - requireCredential: true, - requiredRolePolicy: 'canInvite', - kind: 'read:invite-codes', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'InviteCode', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.registrationTicketsRepository) - private registrationTicketsRepository: RegistrationTicketsRepository, - - private inviteCodeEntityService: InviteCodeEntityService, - private queryService: QueryService, - ) { - super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.registrationTicketsRepository.createQueryBuilder('ticket'), ps.sinceId, ps.untilId) - .andWhere('ticket.createdById = :meId', { meId: me.id }) - .leftJoinAndSelect('ticket.createdBy', 'createdBy') - .leftJoinAndSelect('ticket.usedBy', 'usedBy'); - - const tickets = await query - .limit(ps.limit) - .getMany(); - - return await this.inviteCodeEntityService.packMany(tickets, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 5460635e1d..3b3c5caa00 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -1,11 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import JSON5 from 'json5'; +import type { AdsRepository, UsersRepository } from '@/models/index.js'; +import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MetaEntityService } from '@/core/entities/MetaEntityService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { MetaService } from '@/core/MetaService.js'; +import type { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; +import { DEFAULT_POLICIES } from '@/core/RoleService.js'; export const meta = { tags: ['meta'], @@ -14,10 +17,222 @@ export const meta = { res: { type: 'object', - oneOf: [ - { type: 'object', ref: 'MetaLite' }, - { type: 'object', ref: 'MetaDetailed' }, - ], + optional: false, nullable: false, + properties: { + maintainerName: { + type: 'string', + optional: false, nullable: true, + }, + maintainerEmail: { + type: 'string', + optional: false, nullable: true, + }, + version: { + type: 'string', + optional: false, nullable: false, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + uri: { + type: 'string', + optional: false, nullable: false, + format: 'url', + example: 'https://misskey.example.com', + }, + description: { + type: 'string', + optional: false, nullable: true, + }, + langs: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, + tosUrl: { + type: 'string', + optional: false, nullable: true, + }, + repositoryUrl: { + type: 'string', + optional: false, nullable: false, + default: 'https://github.com/misskey-dev/misskey', + }, + feedbackUrl: { + type: 'string', + optional: false, nullable: false, + default: 'https://github.com/misskey-dev/misskey/issues/new', + }, + defaultDarkTheme: { + type: 'string', + optional: false, nullable: true, + }, + defaultLightTheme: { + type: 'string', + optional: false, nullable: true, + }, + disableRegistration: { + type: 'boolean', + optional: false, nullable: false, + }, + cacheRemoteFiles: { + type: 'boolean', + optional: false, nullable: false, + }, + emailRequiredForSignup: { + type: 'boolean', + optional: false, nullable: false, + }, + enableHcaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, + hcaptchaSiteKey: { + type: 'string', + optional: false, nullable: true, + }, + enableRecaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, + recaptchaSiteKey: { + type: 'string', + optional: false, nullable: true, + }, + enableTurnstile: { + type: 'boolean', + optional: false, nullable: false, + }, + turnstileSiteKey: { + type: 'string', + optional: false, nullable: true, + }, + swPublickey: { + type: 'string', + optional: false, nullable: true, + }, + mascotImageUrl: { + type: 'string', + optional: false, nullable: false, + default: '/assets/ai.png', + }, + bannerUrl: { + type: 'string', + optional: false, nullable: false, + }, + serverErrorImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + infoImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + notFoundImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + iconUrl: { + type: 'string', + optional: false, nullable: true, + }, + maxNoteTextLength: { + type: 'number', + optional: false, nullable: false, + }, + ads: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + place: { + type: 'string', + optional: false, nullable: false, + }, + url: { + type: 'string', + optional: false, nullable: false, + format: 'url', + }, + imageUrl: { + type: 'string', + optional: false, nullable: false, + format: 'url', + }, + }, + }, + }, + requireSetup: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, + enableEmail: { + type: 'boolean', + optional: false, nullable: false, + }, + enableServiceWorker: { + type: 'boolean', + optional: false, nullable: false, + }, + translatorAvailable: { + type: 'boolean', + optional: false, nullable: false, + }, + proxyAccountName: { + type: 'string', + optional: false, nullable: true, + }, + mediaProxy: { + type: 'string', + optional: false, nullable: false, + }, + features: { + type: 'object', + optional: true, nullable: false, + properties: { + registration: { + type: 'boolean', + optional: false, nullable: false, + }, + localTimeLine: { + type: 'boolean', + optional: false, nullable: false, + }, + globalTimeLine: { + type: 'boolean', + optional: false, nullable: false, + }, + hcaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, + recaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, + objectStorage: { + type: 'boolean', + optional: false, nullable: false, + }, + serviceWorker: { + type: 'boolean', + optional: false, nullable: false, + }, + miauth: { + type: 'boolean', + optional: true, nullable: false, + default: true, + }, + }, + }, + }, }, } as const; @@ -29,13 +244,110 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private metaEntityService: MetaEntityService, + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.adsRepository) + private adsRepository: AdsRepository, + + private userEntityService: UserEntityService, + private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { - return ps.detail ? await this.metaEntityService.packDetailed() : await this.metaEntityService.pack(); + const instance = await this.metaService.fetch(true); + + const ads = await this.adsRepository.find({ + where: { + expiresAt: MoreThan(new Date()), + startsAt: LessThanOrEqual(new Date()), + }, + }); + + const response: any = { + maintainerName: instance.maintainerName, + maintainerEmail: instance.maintainerEmail, + + version: this.config.version, + + name: instance.name, + uri: this.config.url, + description: instance.description, + langs: instance.langs, + tosUrl: instance.termsOfServiceUrl, + repositoryUrl: instance.repositoryUrl, + feedbackUrl: instance.feedbackUrl, + disableRegistration: instance.disableRegistration, + emailRequiredForSignup: instance.emailRequiredForSignup, + enableHcaptcha: instance.enableHcaptcha, + hcaptchaSiteKey: instance.hcaptchaSiteKey, + enableRecaptcha: instance.enableRecaptcha, + recaptchaSiteKey: instance.recaptchaSiteKey, + enableTurnstile: instance.enableTurnstile, + turnstileSiteKey: instance.turnstileSiteKey, + swPublickey: instance.swPublicKey, + themeColor: instance.themeColor, + mascotImageUrl: instance.mascotImageUrl, + bannerUrl: instance.bannerUrl, + infoImageUrl: instance.infoImageUrl, + serverErrorImageUrl: instance.serverErrorImageUrl, + notFoundImageUrl: instance.notFoundImageUrl, + iconUrl: instance.iconUrl, + backgroundImageUrl: instance.backgroundImageUrl, + logoImageUrl: instance.logoImageUrl, + maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, + // クライアントの手間を減らすためあらかじめJSONに変換しておく + defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null, + defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null, + ads: ads.map(ad => ({ + id: ad.id, + url: ad.url, + place: ad.place, + ratio: ad.ratio, + imageUrl: ad.imageUrl, + })), + enableEmail: instance.enableEmail, + enableServiceWorker: instance.enableServiceWorker, + + translatorAvailable: instance.deeplAuthKey != null, + + serverRules: instance.serverRules, + + policies: { ...DEFAULT_POLICIES, ...instance.policies }, + + mediaProxy: this.config.mediaProxy, + + ...(ps.detail ? { + cacheRemoteFiles: instance.cacheRemoteFiles, + requireSetup: (await this.usersRepository.countBy({ + host: IsNull(), + })) === 0, + } : {}), + }; + + if (ps.detail) { + const proxyAccount = instance.proxyAccountId ? await this.userEntityService.pack(instance.proxyAccountId).catch(() => null) : null; + + response.proxyAccountName = proxyAccount ? proxyAccount.username : null; + response.features = { + registration: !instance.disableRegistration, + emailRequiredForSignup: instance.emailRequiredForSignup, + hcaptcha: instance.enableHcaptcha, + recaptcha: instance.enableRecaptcha, + turnstile: instance.enableTurnstile, + objectStorage: instance.useObjectStorage, + serviceWorker: instance.enableServiceWorker, + miauth: true, + }; + } + + return response; }); } } diff --git a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts index f1e3726641..0ea29f04dc 100644 --- a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts +++ b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts @@ -1,13 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AccessTokensRepository } from '@/models/_.js'; +import type { AccessTokensRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; -import { NotificationService } from '@/core/NotificationService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DI } from '@/di-symbols.js'; @@ -44,14 +38,14 @@ export const paramDef = { required: ['session', 'permission'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.accessTokensRepository) private accessTokensRepository: AccessTokensRepository, private idService: IdService, - private notificationService: NotificationService, ) { super(meta, paramDef, async (ps, me) => { // Generate access token @@ -61,7 +55,8 @@ export default class extends Endpoint { // eslint- // Insert access token doc await this.accessTokensRepository.insert({ - id: this.idService.gen(now.getTime()), + id: this.idService.genId(), + createdAt: now, lastUsedAt: now, session: ps.session, userId: me.id, @@ -73,9 +68,6 @@ export default class extends Endpoint { // eslint- permission: ps.permission, }); - // アクセストークンが生成されたことを通知 - this.notificationService.createNotification(me.id, 'createToken', {}); - return { token: accessToken, }; diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index e39c133b43..ee358d5c6c 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MutingsRepository } from '@/models/_.js'; +import type { MutingsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserMutingService } from '@/core/UserMutingService.js'; @@ -59,8 +54,9 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, @@ -83,14 +79,12 @@ export default class extends Endpoint { // eslint- }); // Check if already muting - const exist = await this.mutingsRepository.exists({ - where: { - muterId: muter.id, - muteeId: mutee.id, - }, + const exist = await this.mutingsRepository.findOneBy({ + muterId: muter.id, + muteeId: mutee.id, }); - if (exist) { + if (exist != null) { throw new ApiError(meta.errors.alreadyMuting); } diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts index d11832858e..90b74590be 100644 --- a/packages/backend/src/server/api/endpoints/mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/mute/delete.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MutingsRepository } from '@/models/_.js'; +import type { MutingsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserMutingService } from '@/core/UserMutingService.js'; @@ -47,8 +42,9 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, diff --git a/packages/backend/src/server/api/endpoints/mute/list.ts b/packages/backend/src/server/api/endpoints/mute/list.ts index 23204f2829..9ec6d17273 100644 --- a/packages/backend/src/server/api/endpoints/mute/list.ts +++ b/packages/backend/src/server/api/endpoints/mute/list.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MutingsRepository } from '@/models/_.js'; +import type { MutingsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { MutingEntityService } from '@/core/entities/MutingEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -38,8 +33,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, @@ -52,7 +48,7 @@ export default class extends Endpoint { // eslint- .andWhere('muting.muterId = :meId', { meId: me.id }); const mutings = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.mutingEntityService.packMany(mutings, me); diff --git a/packages/backend/src/server/api/endpoints/my/apps.ts b/packages/backend/src/server/api/endpoints/my/apps.ts index c04a92626f..4b7ed80123 100644 --- a/packages/backend/src/server/api/endpoints/my/apps.ts +++ b/packages/backend/src/server/api/endpoints/my/apps.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AppsRepository } from '@/models/_.js'; +import type { AppsRepository } from '@/models/index.js'; import { AppEntityService } from '@/core/entities/AppEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -13,7 +8,6 @@ export const meta = { tags: ['account', 'app'], requireCredential: true, - kind: 'read:account', res: { type: 'array', @@ -35,8 +29,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.appsRepository) private appsRepository: AppsRepository, diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts index 9938322a2a..5fbc7aba58 100644 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ b/packages/backend/src/server/api/endpoints/notes.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -39,8 +34,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -57,34 +53,34 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - + if (ps.local) { query.andWhere('note.userHost IS NULL'); } - + if (ps.reply !== undefined) { query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL'); } - + if (ps.renote !== undefined) { query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL'); } - + if (ps.withFiles !== undefined) { query.andWhere(ps.withFiles ? 'note.fileIds != \'{}\'' : 'note.fileIds = \'{}\''); } - + if (ps.poll !== undefined) { query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE'); } - + // TODO //if (bot != undefined) { // query.isBot = bot; //} - - const notes = await query.limit(ps.limit).getMany(); - + + const notes = await query.take(ps.limit).getMany(); + return await this.noteEntityService.packMany(notes); }); } diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index d457ad1220..26f2d6772d 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -38,8 +33,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -49,19 +45,16 @@ export default class extends Endpoint { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(new Brackets(qb => { - qb - .where('note.replyId = :noteId', { noteId: ps.noteId }) - .orWhere(new Brackets(qb => { - qb - .where('note.renoteId = :noteId', { noteId: ps.noteId }) - .andWhere(new Brackets(qb => { - qb - .where('note.text IS NOT NULL') - .orWhere('note.fileIds != \'{}\'') - .orWhere('note.hasPoll = TRUE'); - })); + .andWhere(new Brackets(qb => { qb + .where('note.replyId = :noteId', { noteId: ps.noteId }) + .orWhere(new Brackets(qb => { qb + .where('note.renoteId = :noteId', { noteId: ps.noteId }) + .andWhere(new Brackets(qb => { qb + .where('note.text IS NOT NULL') + .orWhere('note.fileIds != \'{}\'') + .orWhere('note.hasPoll = TRUE'); })); + })); })) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') @@ -70,9 +63,12 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBaseNoteFilteringQuery(query, me); + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + } - const notes = await query.limit(ps.limit).getMany(); + const notes = await query.take(ps.limit).getMany(); return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts index 29cab9f212..0a5542f497 100644 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ b/packages/backend/src/server/api/endpoints/notes/clips.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { ClipNotesRepository, ClipsRepository } from '@/models/_.js'; +import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -44,8 +39,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index d13fd5e82e..5ecf7cf458 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -1,16 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { MiNote } from '@/models/Note.js'; -import type { NotesRepository } from '@/models/_.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; -import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['notes'], @@ -46,8 +41,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -61,7 +57,7 @@ export default class extends Endpoint { // eslint- throw err; }); - const conversation: MiNote[] = []; + const conversation: Note[] = []; let i = 0; const get = async (id: any) => { diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts index 6097f9c562..6bff7fc0c9 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.test.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import { readFile } from 'node:fs/promises'; @@ -48,11 +43,6 @@ describe('api:notes/create', () => { expect(v({ text: await tooLong })) .toBe(INVALID); }); - - test('whitespace-only post', () => { - expect(v({ text: ' ' })) - .toBe(INVALID); - }); }); describe('cw', () => { @@ -68,7 +58,7 @@ describe('api:notes/create', () => { test('0 characters cw', () => { expect(v({ text: 'Body', cw: '' })) - .toBe(INVALID); + .toBe(VALID); }); test('reject only cw', () => { diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 253a360815..96be5ed844 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -1,23 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { MiUser } from '@/models/User.js'; -import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/_.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiNote } from '@/models/Note.js'; -import type { MiChannel } from '@/models/Channel.js'; +import type { User } from '@/models/entities/User.js'; +import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/index.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { Channel } from '@/models/entities/Channel.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; import { DI } from '@/di-symbols.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -59,36 +52,18 @@ export const meta = { id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a', }, - cannotRenoteDueToVisibility: { - message: 'You can not Renote due to target visibility.', - code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY', - id: 'be9529e9-fe72-4de0-ae43-0b363c4938af', - }, - noSuchReplyTarget: { message: 'No such reply target.', code: 'NO_SUCH_REPLY_TARGET', id: '749ee0f6-d3da-459a-bf02-282e2da4292c', }, - cannotReplyToInvisibleNote: { - message: 'You cannot reply to an invisible Note.', - code: 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE', - id: 'b98980fa-3780-406c-a935-b6d0eeee10d1', - }, - cannotReplyToPureRenote: { message: 'You can not reply to a pure Renote.', code: 'CANNOT_REPLY_TO_A_PURE_RENOTE', id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15', }, - cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: { - message: 'You cannot reply to a specified visibility note with extended visibility.', - code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY', - id: 'ed940410-535c-4d5e-bfa3-af798671e93c', - }, - cannotCreateAlreadyExpiredPoll: { message: 'Poll is already expired.', code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', @@ -112,24 +87,6 @@ export const meta = { code: 'NO_SUCH_FILE', id: 'b6992544-63e7-67f0-fa7f-32444b1b5306', }, - - cannotRenoteOutsideOfChannel: { - message: 'Cannot renote outside of channel.', - code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL', - id: '33510210-8452-094c-6227-4a6c05d99f00', - }, - - containsProhibitedWords: { - message: 'Cannot post because it contains prohibited words.', - code: 'CONTAINS_PROHIBITED_WORDS', - id: 'aa6e01d3-a85c-669d-758a-76aab43af334', - }, - - containsTooManyMentions: { - message: 'Cannot post because it exceeds the allowed number of mentions.', - code: 'CONTAINS_TOO_MANY_MENTIONS', - id: '4de0363a-3046-481b-9b0f-feff3e211025', - }, }, } as const; @@ -140,7 +97,7 @@ export const paramDef = { visibleUserIds: { type: 'array', uniqueItems: true, items: { type: 'string', format: 'misskey:id', } }, - cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, + cw: { type: 'string', nullable: true, maxLength: 100 }, localOnly: { type: 'boolean', default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, noExtractMentions: { type: 'boolean', default: false }, @@ -156,7 +113,7 @@ export const paramDef = { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, - nullable: true, + nullable: false, }, fileIds: { type: 'array', @@ -191,37 +148,18 @@ export const paramDef = { }, }, // (re)note with text, files and poll are optional - if: { - properties: { - renoteId: { - type: 'null', - }, - fileIds: { - type: 'null', - }, - mediaIds: { - type: 'null', - }, - poll: { - type: 'null', - }, - }, - }, - then: { - properties: { - text: { - type: 'string', - minLength: 1, - maxLength: MAX_NOTE_TEXT_LENGTH, - pattern: '[^\\s]+', - }, - }, - required: ['text'], - }, + anyOf: [ + { required: ['text'] }, + { required: ['renoteId'] }, + { required: ['fileIds'] }, + { required: ['mediaIds'] }, + { required: ['poll'] }, + ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -242,15 +180,15 @@ export default class extends Endpoint { // eslint- private noteCreateService: NoteCreateService, ) { super(meta, paramDef, async (ps, me) => { - let visibleUsers: MiUser[] = []; + let visibleUsers: User[] = []; if (ps.visibleUserIds) { visibleUsers = await this.usersRepository.findBy({ id: In(ps.visibleUserIds), }); } - let files: MiDriveFile[] = []; - const fileIds = ps.fileIds ?? ps.mediaIds ?? null; + let files: DriveFile[] = []; + const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; if (fileIds != null) { files = await this.driveFilesRepository.createQueryBuilder('file') .where('file.userId = :userId AND file.id IN (:...fileIds)', { @@ -266,76 +204,47 @@ export default class extends Endpoint { // eslint- } } - let renote: MiNote | null = null; + let renote: Note | null = null; if (ps.renoteId != null) { // Fetch renote to note renote = await this.notesRepository.findOneBy({ id: ps.renoteId }); if (renote == null) { throw new ApiError(meta.errors.noSuchRenoteTarget); - } else if (isRenote(renote) && !isQuote(renote)) { + } else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) { throw new ApiError(meta.errors.cannotReRenote); } // Check blocking if (renote.userId !== me.id) { - const blockExist = await this.blockingsRepository.exists({ - where: { - blockerId: renote.userId, - blockeeId: me.id, - }, + const block = await this.blockingsRepository.findOneBy({ + blockerId: renote.userId, + blockeeId: me.id, }); - if (blockExist) { + if (block) { throw new ApiError(meta.errors.youHaveBeenBlocked); } } - - if (renote.visibility === 'followers' && renote.userId !== me.id) { - // 他人のfollowers noteはreject - throw new ApiError(meta.errors.cannotRenoteDueToVisibility); - } else if (renote.visibility === 'specified') { - // specified / direct noteはreject - throw new ApiError(meta.errors.cannotRenoteDueToVisibility); - } - - if (renote.channelId && renote.channelId !== ps.channelId) { - // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック - // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する - const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId }); - if (renoteChannel == null) { - // リノートしたいノートが書き込まれているチャンネルが無い - throw new ApiError(meta.errors.noSuchChannel); - } else if (!renoteChannel.allowRenoteToExternal) { - // リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合 - throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel); - } - } } - let reply: MiNote | null = null; + let reply: Note | null = null; if (ps.replyId != null) { // Fetch reply reply = await this.notesRepository.findOneBy({ id: ps.replyId }); if (reply == null) { throw new ApiError(meta.errors.noSuchReplyTarget); - } else if (isRenote(reply) && !isQuote(reply)) { + } else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) { throw new ApiError(meta.errors.cannotReplyToPureRenote); - } else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) { - throw new ApiError(meta.errors.cannotReplyToInvisibleNote); - } else if (reply.visibility === 'specified' && ps.visibility !== 'specified') { - throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); } // Check blocking if (reply.userId !== me.id) { - const blockExist = await this.blockingsRepository.exists({ - where: { - blockerId: reply.userId, - blockeeId: me.id, - }, + const block = await this.blockingsRepository.findOneBy({ + blockerId: reply.userId, + blockeeId: me.id, }); - if (blockExist) { + if (block) { throw new ApiError(meta.errors.youHaveBeenBlocked); } } @@ -351,7 +260,7 @@ export default class extends Endpoint { // eslint- } } - let channel: MiChannel | null = null; + let channel: Channel | null = null; if (ps.channelId != null) { channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false }); @@ -361,43 +270,31 @@ export default class extends Endpoint { // eslint- } // 投稿を作成 - try { - const note = await this.noteCreateService.create(me, { - createdAt: new Date(), - files: files, - poll: ps.poll ? { - choices: ps.poll.choices, - multiple: ps.poll.multiple ?? false, - expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, - } : undefined, - text: ps.text ?? undefined, - reply, - renote, - cw: ps.cw, - localOnly: ps.localOnly, - reactionAcceptance: ps.reactionAcceptance, - visibility: ps.visibility, - visibleUsers, - channel, - apMentions: ps.noExtractMentions ? [] : undefined, - apHashtags: ps.noExtractHashtags ? [] : undefined, - apEmojis: ps.noExtractEmojis ? [] : undefined, - }); + const note = await this.noteCreateService.create(me, { + createdAt: new Date(), + files: files, + poll: ps.poll ? { + choices: ps.poll.choices, + multiple: ps.poll.multiple ?? false, + expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, + } : undefined, + text: ps.text ?? undefined, + reply, + renote, + cw: ps.cw, + localOnly: ps.localOnly, + reactionAcceptance: ps.reactionAcceptance, + visibility: ps.visibility, + visibleUsers, + channel, + apMentions: ps.noExtractMentions ? [] : undefined, + apHashtags: ps.noExtractHashtags ? [] : undefined, + apEmojis: ps.noExtractEmojis ? [] : undefined, + }); - return { - createdNote: await this.noteEntityService.pack(note, me), - }; - } catch (e) { - // TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい - if (e instanceof IdentifiableError) { - if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') { - throw new ApiError(meta.errors.containsProhibitedWords); - } else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') { - throw new ApiError(meta.errors.containsTooManyMentions); - } - } - throw e; - } + return { + createdNote: await this.noteEntityService.pack(note, me), + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index 9d7c9a9081..16c4c01387 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/_.js'; +import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteDeleteService } from '@/core/NoteDeleteService.js'; import { DI } from '@/di-symbols.js'; @@ -49,8 +44,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -70,7 +66,7 @@ export default class extends Endpoint { // eslint- } // この操作を行うのが投稿者とは限らない(例えばモデレーター)ため - await this.noteDeleteService.delete(await this.usersRepository.findOneByOrFail({ id: note.userId }), note, false, me); + await this.noteDeleteService.delete(await this.usersRepository.findOneByOrFail({ id: note.userId }), note); }); } } diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index 804071b3d4..611ea19560 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; -import type { NoteFavoritesRepository } from '@/models/_.js'; +import type { NoteFavoritesRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -49,8 +44,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.noteFavoritesRepository) private noteFavoritesRepository: NoteFavoritesRepository, @@ -67,20 +63,19 @@ export default class extends Endpoint { // eslint- }); // if already favorited - const exist = await this.noteFavoritesRepository.exists({ - where: { - noteId: note.id, - userId: me.id, - }, + const exist = await this.noteFavoritesRepository.findOneBy({ + noteId: note.id, + userId: me.id, }); - if (exist) { + if (exist != null) { throw new ApiError(meta.errors.alreadyFavorited); } // Create favorite await this.noteFavoritesRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), noteId: note.id, userId: me.id, }); diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts index 2036facdba..bb3a7c501a 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; -import type { NoteFavoritesRepository } from '@/models/_.js'; +import type { NoteFavoritesRepository } from '@/models/index.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -40,8 +35,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.noteFavoritesRepository) private noteFavoritesRepository: NoteFavoritesRepository, diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index a57c84d432..bdb06498bc 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -1,17 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; -import { FeaturedService } from '@/core/FeaturedService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { CacheService } from '@/core/CacheService.js'; -import { QueryService } from '@/core/QueryService.js'; export const meta = { tags: ['notes'], @@ -35,78 +27,50 @@ export const paramDef = { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - untilId: { type: 'string', format: 'misskey:id' }, + offset: { type: 'integer', default: 0 }, channelId: { type: 'string', nullable: true, format: 'misskey:id' }, }, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - private globalNotesRankingCache: string[] = []; - private globalNotesRankingCacheLastFetchedAt = 0; - +export default class extends Endpoint { constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, - private cacheService: CacheService, private noteEntityService: NoteEntityService, - private featuredService: FeaturedService, private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - let noteIds: string[]; - if (ps.channelId) { - noteIds = await this.featuredService.getInChannelNotesRanking(ps.channelId, 50); - } else { - if (this.globalNotesRankingCacheLastFetchedAt !== 0 && (Date.now() - this.globalNotesRankingCacheLastFetchedAt < 1000 * 60 * 30)) { - noteIds = this.globalNotesRankingCache; - } else { - noteIds = await this.featuredService.getGlobalNotesRanking(100); - this.globalNotesRankingCache = noteIds; - this.globalNotesRankingCacheLastFetchedAt = Date.now(); - } - } - - noteIds.sort((a, b) => a > b ? -1 : 1); - if (ps.untilId) { - noteIds = noteIds.filter(id => id < ps.untilId!); - } - noteIds = noteIds.slice(0, ps.limit); - - if (noteIds.length === 0) { - return []; - } - - const [ - userIdsWhoMeMuting, - userIdsWhoBlockingMe, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]) : [new Set(), new Set()]; + const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .addSelect('note.score') + .where('note.userHost IS NULL') + .andWhere('note.score > 0') + .andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) }) + .andWhere('note.visibility = \'public\'') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); + .leftJoinAndSelect('renote.user', 'renoteUser'); - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); + if (ps.channelId) query.andWhere('note.channelId = :channelId', { channelId: ps.channelId }); - const notes = (await query.getMany()).filter(note => { - if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); - return true; - }); + let notes = await query + .orderBy('note.score', 'DESC') + .take(100) + .getMany(); - notes.sort((a, b) => a.id > b.id ? -1 : 1); + notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + + notes = notes.slice(ps.offset, ps.offset + ps.limit); return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 1c73edf08e..88c1ca7f58 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -1,14 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import { Brackets } from 'typeorm'; -import type { NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { MetaService } from '@/core/MetaService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; @@ -40,7 +35,7 @@ export const paramDef = { type: 'object', properties: { withFiles: { type: 'boolean', default: false }, - withRenotes: { type: 'boolean', default: true }, + withReplies: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -50,14 +45,16 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, private queryService: QueryService, + private metaService: MetaService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, ) { @@ -78,25 +75,20 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - this.queryService.generateBaseNoteFilteringQuery(query, me); - if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateMutedNoteQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + } if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); } - - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.where('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.where('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } //#endregion - const timeline = await query.limit(ps.limit).getMany(); + const timeline = await query.take(ps.limit).getMany(); process.nextTick(() => { if (me) { diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 2c8459525a..7a3581e6e4 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -1,30 +1,20 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js'; +import type { NotesRepository, FollowingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; -import { CacheService } from '@/core/CacheService.js'; -import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; -import { QueryService } from '@/core/QueryService.js'; -import { UserFollowingService } from '@/core/UserFollowingService.js'; -import { MiLocalUser } from '@/models/User.js'; -import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], requireCredential: true, - kind: 'read:account', res: { type: 'array', @@ -42,12 +32,6 @@ export const meta = { code: 'STL_DISABLED', id: '620763f4-f621-4533-ab33-0577a1a3c342', }, - - bothWithRepliesAndWithFiles: { - message: 'Specifying both withReplies and withFiles is not supported', - code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', - id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f' - }, }, } as const; @@ -59,228 +43,107 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, - allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false }, - withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, }, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, - @Inject(DI.channelFollowingsRepository) - private channelFollowingsRepository: ChannelFollowingsRepository, + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, private noteEntityService: NoteEntityService, + private queryService: QueryService, + private metaService: MetaService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, private idService: IdService, - private cacheService: CacheService, - private queryService: QueryService, - private userFollowingService: UserFollowingService, - private fanoutTimelineEndpointService: FanoutTimelineEndpointService, ) { super(meta, paramDef, async (ps, me) => { - const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); - const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const policies = await this.roleService.getUserPolicies(me.id); if (!policies.ltlAvailable) { throw new ApiError(meta.errors.stlDisabled); } - if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + //#region Construct query + const followingQuery = this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); - if (!this.serverSettings.enableFanoutTimeline) { - const timeline = await this.getFromDb({ - untilId, - sinceId, - limit: ps.limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withReplies: ps.withReplies, - }, me); + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで + .andWhere(new Brackets(qb => { + qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id }) + .orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); + })) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .setParameters(followingQuery.getParameters()); - process.nextTick(() => { - this.activeUsersChart.read(me); - }); + this.queryService.generateChannelQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); + this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateMutedNoteQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - return await this.noteEntityService.packMany(timeline, me); + if (ps.includeMyRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.userId != :meId', { meId: me.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); } - let timelineConfig: FanoutTimelineName[]; + if (ps.includeRenotedMyNotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + if (ps.includeLocalRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserHost IS NOT NULL'); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } if (ps.withFiles) { - timelineConfig = [ - `homeTimelineWithFiles:${me.id}`, - 'localTimelineWithFiles', - ]; - } else if (ps.withReplies) { - timelineConfig = [ - `homeTimeline:${me.id}`, - 'localTimeline', - 'localTimelineWithReplies', - ]; - } else { - timelineConfig = [ - `homeTimeline:${me.id}`, - 'localTimeline', - `localTimelineWithReplyTo:${me.id}`, - ]; + query.andWhere('note.fileIds != \'{}\''); } + //#endregion - const [ - followings, - ] = await Promise.all([ - this.cacheService.userFollowingsCache.fetch(me.id), - ]); - - const redisTimeline = await this.fanoutTimelineEndpointService.timeline({ - untilId, - sinceId, - limit: ps.limit, - allowPartial: ps.allowPartial, - me, - redisTimelines: timelineConfig, - useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, - alwaysIncludeMyNotes: true, - excludePureRenotes: !ps.withRenotes, - noteFilter: note => { - if (note.reply && note.reply.visibility === 'followers') { - if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false; - } - - return true; - }, - dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ - untilId, - sinceId, - limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withReplies: ps.withReplies, - }, me), - }); + const timeline = await query.take(ps.limit).getMany(); process.nextTick(() => { this.activeUsersChart.read(me); }); - return redisTimeline; + return await this.noteEntityService.packMany(timeline, me); }); } - - private async getFromDb(ps: { - untilId: string | null, - sinceId: string | null, - limit: number, - includeMyRenotes: boolean, - includeRenotedMyNotes: boolean, - includeLocalRenotes: boolean, - withFiles: boolean, - withReplies: boolean, - }, me: MiLocalUser) { - const followees = await this.userFollowingService.getFollowees(me.id); - const followingChannels = await this.channelFollowingsRepository.find({ - where: { - followerId: me.id, - }, - }); - - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(new Brackets(qb => { - if (followees.length > 0) { - const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; - qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); - qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); - } else { - qb.where('note.userId = :meId', { meId: me.id }); - qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); - } - })) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - if (followingChannels.length > 0) { - const followingChannelIds = followingChannels.map(x => x.followeeId); - - query.andWhere(new Brackets(qb => { - qb.where('note.channelId IN (:...followingChannelIds)', { followingChannelIds }); - qb.orWhere('note.channelId IS NULL'); - })); - } else { - query.andWhere('note.channelId IS NULL'); - } - - if (!ps.withReplies) { - query.andWhere(new Brackets(qb => { - qb - .where('note.replyId IS NULL') // 返信ではない - .orWhere(new Brackets(qb => { - qb // 返信だけど投稿者自身への返信 - .where('note.replyId IS NOT NULL') - .andWhere('note.replyUserId = note.userId'); - })); - })); - } - - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBaseNoteFilteringQuery(query, me); - this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion - - return await query.limit(ps.limit).getMany(); - } } diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index ee61ab43da..2ee549232c 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -1,20 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { MiMeta, NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { MetaService } from '@/core/MetaService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; -import { QueryService } from '@/core/QueryService.js'; -import { MiLocalUser } from '@/models/User.js'; -import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -36,12 +30,6 @@ export const meta = { code: 'LTL_DISABLED', id: '45a6eb02-7695-4393-b023-dd3be9aaaefd', }, - - bothWithRepliesAndWithFiles: { - message: 'Specifying both withReplies and withFiles is not supported', - code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', - id: 'dd9c8400-1cb5-4eef-8a31-200c5f933793', - }, }, } as const; @@ -49,85 +37,80 @@ export const paramDef = { type: 'object', properties: { withFiles: { type: 'boolean', default: false }, - withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, + fileType: { type: 'array', items: { + type: 'string', + } }, + excludeNsfw: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, - allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, }, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, + private queryService: QueryService, + private metaService: MetaService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, private idService: IdService, - private fanoutTimelineEndpointService: FanoutTimelineEndpointService, - private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); - const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const policies = await this.roleService.getUserPolicies(me ? me.id : null); if (!policies.ltlAvailable) { throw new ApiError(meta.errors.ltlDisabled); } - if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで + .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); - if (!this.serverSettings.enableFanoutTimeline) { - const timeline = await this.getFromDb({ - untilId, - sinceId, - limit: ps.limit, - withFiles: ps.withFiles, - withReplies: ps.withReplies, - }, me); + this.queryService.generateChannelQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); + this.queryService.generateVisibilityQuery(query, me); + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateMutedNoteQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); + if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - process.nextTick(() => { - if (me) { - this.activeUsersChart.read(me); - } - }); - - return await this.noteEntityService.packMany(timeline, me); + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } - const timeline = await this.fanoutTimelineEndpointService.timeline({ - untilId, - sinceId, - limit: ps.limit, - allowPartial: ps.allowPartial, - me, - useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, - redisTimelines: - ps.withFiles ? ['localTimelineWithFiles'] - : ps.withReplies ? ['localTimeline', 'localTimelineWithReplies'] - : me ? ['localTimeline', `localTimelineWithReplyTo:${me.id}`] - : ['localTimeline'], - alwaysIncludeMyNotes: true, - excludePureRenotes: !ps.withRenotes, - dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ - untilId, - sinceId, - limit, - withFiles: ps.withFiles, - withReplies: ps.withReplies, - }, me), - }); + if (ps.fileType != null) { + query.andWhere('note.fileIds != \'{}\''); + query.andWhere(new Brackets(qb => { + for (const type of ps.fileType!) { + const i = ps.fileType!.indexOf(type); + qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + } + })); + + if (ps.excludeNsfw) { + query.andWhere('note.cw IS NULL'); + query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); + } + } + //#endregion + + const timeline = await query.take(ps.limit).getMany(); process.nextTick(() => { if (me) { @@ -135,46 +118,7 @@ export default class extends Endpoint { // eslint- } }); - return timeline; + return await this.noteEntityService.packMany(timeline, me); }); } - - private async getFromDb(ps: { - sinceId: string | null, - untilId: string | null, - limit: number, - withFiles: boolean, - withReplies: boolean, - }, me: MiLocalUser | null) { - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), - ps.sinceId, ps.untilId) - .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL) AND (note.channelId IS NULL)') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBaseNoteFilteringQuery(query, me); - if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - - if (!ps.withReplies) { - query.andWhere(new Brackets(qb => { - qb - .where('note.replyId IS NULL') // 返信ではない - .orWhere(new Brackets(qb => { - qb // 返信だけど投稿者自身への返信 - .where('note.replyId IS NOT NULL') - .andWhere('note.replyUserId = note.userId'); - })); - })); - } - - return await query.limit(ps.limit).getMany(); - } } diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 7ffc349db5..4e9f604d8d 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -1,21 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, FollowingsRepository } from '@/models/_.js'; +import type { NotesRepository, FollowingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes'], requireCredential: true, - kind: 'read:account', res: { type: 'array', @@ -40,8 +35,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -51,6 +47,7 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, + private noteReadService: NoteReadService, ) { super(meta, paramDef, async (ps, me) => { const followingQuery = this.followingsRepository.createQueryBuilder('following') @@ -58,13 +55,10 @@ export default class extends Endpoint { // eslint- .where('following.followerId = :followerId', { followerId: me.id }); const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(new Brackets(qb => { - qb // このmeIdAsListパラメータはqueryServiceのgenerateVisibilityQueryでセットされる - .where(':meIdAsList <@ note.mentions') - .orWhere(':meIdAsList <@ note.visibleUserIds'); + .andWhere(new Brackets(qb => { qb + .where(`'{"${me.id}"}' <@ note.mentions`) + .orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`); })) - // Avoid scanning primary key index - .orderBy('CONCAT(note.id)', 'DESC') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') @@ -72,8 +66,9 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBaseNoteFilteringQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); this.queryService.generateMutedNoteThreadQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); if (ps.visibility) { query.andWhere('note.visibility = :visibility', { visibility: ps.visibility }); @@ -84,7 +79,9 @@ export default class extends Endpoint { // eslint- query.setParameters(followingQuery.getParameters()); } - const mentions = await query.limit(ps.limit).getMany(); + const mentions = await query.take(ps.limit).getMany(); + + this.noteReadService.read(me.id, mentions); return await this.noteEntityService.packMany(mentions, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts index 4fd6f8682d..6cdc9b902c 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Brackets, In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, MutingsRepository, PollsRepository, PollVotesRepository } from '@/models/_.js'; +import type { NotesRepository, MutingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -14,7 +9,6 @@ export const meta = { tags: ['notes'], requireCredential: true, - kind: 'read:account', res: { type: 'array', @@ -32,13 +26,13 @@ export const paramDef = { properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, - excludeChannels: { type: 'boolean', default: false }, }, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -59,10 +53,9 @@ export default class extends Endpoint { // eslint- .where('poll.userHost IS NULL') .andWhere('poll.userId != :meId', { meId: me.id }) .andWhere('poll.noteVisibility = \'public\'') - .andWhere(new Brackets(qb => { - qb - .where('poll.expiresAt IS NULL') - .orWhere('poll.expiresAt > :now', { now: new Date() }); + .andWhere(new Brackets(qb => { qb + .where('poll.expiresAt IS NULL') + .orWhere('poll.expiresAt > :now', { now: new Date() }); })); //#region exclude arleady voted polls @@ -87,16 +80,10 @@ export default class extends Endpoint { // eslint- query.setParameters(mutingQuery.getParameters()); //#endregion - //#region exclude channels - if (ps.excludeChannels) { - query.andWhere('poll.channelId IS NULL'); - } - //#endregion - const polls = await query .orderBy('poll.noteId', 'DESC') - .limit(ps.limit) - .offset(ps.offset) + .take(ps.limit) + .skip(ps.offset) .getMany(); if (polls.length === 0) return []; @@ -106,7 +93,7 @@ export default class extends Endpoint { // eslint- id: In(polls.map(poll => poll.noteId)), }, order: { - id: 'DESC', + createdAt: 'DESC', }, }); diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index f33f49075b..3a33b037f8 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, PollsRepository, PollVotesRepository } from '@/models/_.js'; -import type { MiRemoteUser } from '@/models/User.js'; +import type { UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; +import type { RemoteUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -76,8 +71,9 @@ export const paramDef = { // TODO: ロジックをサービスに切り出す +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -144,12 +140,13 @@ export default class extends Endpoint { // eslint- } // Create vote - const vote = await this.pollVotesRepository.insertOne({ - id: this.idService.gen(createdAt.getTime()), + const vote = await this.pollVotesRepository.insert({ + id: this.idService.genId(), + createdAt, noteId: note.id, userId: me.id, choice: ps.choice, - }); + }).then(x => this.pollVotesRepository.findOneByOrFail(x.identifiers[0])); // Increment votes count const index = ps.choice + 1; // In SQL, array index is 1 based @@ -162,7 +159,7 @@ export default class extends Endpoint { // eslint- // リモート投票の場合リプライ送信 if (note.userHost != null) { - const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as MiRemoteUser; + const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as RemoteUser; this.queueService.deliver(me, this.apRendererService.addContext(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox, false); } diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 97b12ab7f7..4772c4f809 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -1,16 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import { Brackets, type FindOptionsWhere } from 'typeorm'; -import type { NoteReactionsRepository } from '@/models/_.js'; -import type { MiNoteReaction } from '@/models/NoteReaction.js'; +import type { NoteReactionsRepository } from '@/models/index.js'; +import type { NoteReaction } from '@/models/entities/NoteReaction.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js'; import { DI } from '@/di-symbols.js'; -import { QueryService } from '@/core/QueryService.js'; +import type { FindOptionsWhere } from 'typeorm'; export const meta = { tags: ['notes', 'reactions'], @@ -45,38 +39,46 @@ export const paramDef = { noteId: { type: 'string', format: 'misskey:id' }, type: { type: 'string', nullable: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + offset: { type: 'integer', default: 0 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, }, required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.noteReactionsRepository) private noteReactionsRepository: NoteReactionsRepository, private noteReactionEntityService: NoteReactionEntityService, - private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'), ps.sinceId, ps.untilId) - .andWhere('reaction.noteId = :noteId', { noteId: ps.noteId }) - .leftJoinAndSelect('reaction.user', 'user') - .leftJoinAndSelect('reaction.note', 'note'); + const query = { + noteId: ps.noteId, + } as FindOptionsWhere; if (ps.type) { // ローカルリアクションはホスト名が . とされているが // DB 上ではそうではないので、必要に応じて変換 const suffix = '@.:'; const type = ps.type.endsWith(suffix) ? ps.type.slice(0, ps.type.length - suffix.length) + ':' : ps.type; - query.andWhere('reaction.reaction = :type', { type }); + query.reaction = type; } - const reactions = await query.limit(ps.limit).getMany(); + const reactions = await this.noteReactionsRepository.find({ + where: query, + take: ps.limit, + skip: ps.offset, + order: { + id: -1, + }, + relations: ['user', 'note'], + }); - return await this.noteReactionEntityService.packMany(reactions, me); + return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me))); }); } } diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts index 0f0dcca605..97cb026779 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -36,12 +31,6 @@ export const meta = { code: 'YOU_HAVE_BEEN_BLOCKED', id: '20ef5475-9f38-4e4c-bd33-de6d979498ec', }, - - cannotReactToRenote: { - message: 'You cannot react to Renote.', - code: 'CANNOT_REACT_TO_RENOTE', - id: 'eaccdc08-ddef-43fe-908f-d108faad57f5', - }, }, } as const; @@ -54,8 +43,9 @@ export const paramDef = { required: ['noteId', 'reaction'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private getterService: GetterService, private reactionService: ReactionService, @@ -68,7 +58,6 @@ export default class extends Endpoint { // eslint- await this.reactionService.create(me, note, ps.reaction).catch(err => { if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted); if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked); - if (err.id === '12c35529-3c79-4327-b1cc-e2cf63a71925') throw new ApiError(meta.errors.cannotReactToRenote); throw err; }); return; diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts index e6c3bbbcf5..207f0b4cf2 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -46,8 +41,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private getterService: GetterService, private reactionService: ReactionService, diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index fa2306c1bf..d406855660 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -47,8 +42,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -72,9 +68,10 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBaseNoteFilteringQuery(query, me); + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); - const renotes = await query.limit(ps.limit).getMany(); + const renotes = await query.take(ps.limit).getMany(); return await this.noteEntityService.packMany(renotes, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index 9626947480..f2af71d55f 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -37,8 +32,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -56,9 +52,10 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBaseNoteFilteringQuery(query, me); + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); - const timeline = await query.limit(ps.limit).getMany(); + const timeline = await query.take(ps.limit).getMany(); return await this.noteEntityService.packMany(timeline, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index 51255f0bbf..742df0ca95 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/index.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -28,58 +23,44 @@ export const meta = { } as const; export const paramDef = { - allOf: [ - { - anyOf: [ - { - type: 'object', - properties: { - tag: { type: 'string', minLength: 1 }, - }, - required: ['tag'], - }, - { - type: 'object', - properties: { - query: { - type: 'array', - description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.', - items: { - type: 'array', - items: { - type: 'string', - minLength: 1, - }, - minItems: 1, - }, - minItems: 1, - }, - }, - required: ['query'], - }, - ], + type: 'object', + properties: { + reply: { type: 'boolean', nullable: true, default: null }, + renote: { type: 'boolean', nullable: true, default: null }, + withFiles: { + type: 'boolean', + default: false, + description: 'Only show notes that have attached files.', }, - { - type: 'object', - properties: { - reply: { type: 'boolean', nullable: true, default: null }, - renote: { type: 'boolean', nullable: true, default: null }, - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', + poll: { type: 'boolean', nullable: true, default: null }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + + tag: { type: 'string', minLength: 1 }, + query: { + type: 'array', + description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.', + items: { + type: 'array', + items: { + type: 'string', + minLength: 1, }, - poll: { type: 'boolean', nullable: true, default: null }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + minItems: 1, }, + minItems: 1, }, + }, + anyOf: [ + { required: ['tag'] }, + { required: ['query'] }, ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -96,19 +77,20 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBaseNoteFilteringQuery(query, me); + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); try { - if ('tag' in ps) { + if (ps.tag) { if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection'); - query.andWhere(':tag <@ note.tags', { tag: [normalizeForSearch(ps.tag)] }); + query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`); } else { query.andWhere(new Brackets(qb => { - for (const tags of ps.query) { + for (const tags of ps.query!) { qb.orWhere(new Brackets(qb => { for (const tag of tags) { if (!safeForSql(normalizeForSearch(tag))) throw new Error('Injection'); - qb.andWhere(':tag <@ note.tags', { tag: [normalizeForSearch(tag)] }); + qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`); } })); } @@ -148,7 +130,7 @@ export default class extends Endpoint { // eslint- } // Search notes - const notes = await query.limit(ps.limit).getMany(); + const notes = await query.take(ps.limit).getMany(); return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index 3fe19806e3..f6385400c3 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,12 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { SearchService } from '@/core/SearchService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import type { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; @@ -54,9 +52,13 @@ export const paramDef = { // TODO: ロジックをサービスに切り出す +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.config) + private config: Config, + private noteEntityService: NoteEntityService, private searchService: SearchService, private roleService: RoleService, @@ -66,7 +68,7 @@ export default class extends Endpoint { // eslint- if (!policies.canSearchNotes) { throw new ApiError(meta.errors.unavailable); } - + const notes = await this.searchService.searchNote(ps.query, me, { userId: ps.userId, channelId: ps.channelId, diff --git a/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts b/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts deleted file mode 100644 index e102bc1d4a..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['notes'], - - requireCredential: false, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - reactions: { - type: 'object', - optional: false, nullable: false, - additionalProperties: { - type: 'number', - }, - }, - reactionEmojis: { - type: 'object', - optional: false, nullable: false, - additionalProperties: { - type: 'string', - }, - }, - }, - }, - }, - - errors: { - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteIds: { type: 'array', items: { type: 'string', format: 'misskey:id' }, maxItems: 100, minItems: 1 }, - }, - required: ['noteIds'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private noteEntityService: NoteEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - return await this.noteEntityService.fetchDiffs(ps.noteIds); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index b93c73b0c5..6b1b84a18e 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -1,15 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; -import { MiMeta } from '@/models/Meta.js'; import { ApiError } from '../../error.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['notes'], @@ -28,12 +23,6 @@ export const meta = { code: 'NO_SUCH_NOTE', id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d', }, - - signinRequired: { - message: 'Signin required.', - code: 'SIGNIN_REQUIRED', - id: '8e75455b-738c-471d-9f80-62693f33372e', - }, }, } as const; @@ -45,33 +34,22 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { - const note = await this.getterService.getNoteWithUser(ps.noteId).catch(err => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; }); - if (note.user!.requireSigninToViewContents && me == null) { - throw new ApiError(meta.errors.signinRequired); - } - - if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) { - throw new ApiError(meta.errors.signinRequired); - } - - if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.userHost != null && me == null) { - throw new ApiError(meta.errors.signinRequired); - } - return await this.noteEntityService.pack(note, me, { detail: true, }); diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index 4c1eb86542..93517ab10c 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, NoteThreadMutingsRepository, NoteFavoritesRepository } from '@/models/_.js'; +import type { NotesRepository, NoteThreadMutingsRepository, NoteFavoritesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -12,7 +7,6 @@ export const meta = { tags: ['notes'], requireCredential: true, - kind: 'read:account', res: { type: 'object', @@ -38,8 +32,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts index 29c6aa7434..abea069da8 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts @@ -1,14 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; -import type { NotesRepository, NoteThreadMutingsRepository } from '@/models/_.js'; +import type { NotesRepository, NoteThreadMutingsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -41,8 +37,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -51,6 +48,7 @@ export default class extends Endpoint { // eslint- private noteThreadMutingsRepository: NoteThreadMutingsRepository, private getterService: GetterService, + private noteReadService: NoteReadService, private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { @@ -67,8 +65,11 @@ export default class extends Endpoint { // eslint- }], }); + await this.noteReadService.read(me.id, mutedNotes); + await this.noteThreadMutingsRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), threadId: note.threadId ?? note.id, userId: me.id, }); diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts index d94d6cd652..30016d48bc 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { NoteThreadMutingsRepository } from '@/models/_.js'; +import type { NoteThreadMutingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; @@ -34,8 +29,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.noteThreadMutingsRepository) private noteThreadMutingsRepository: NoteThreadMutingsRepository, diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index c76cca1518..e1f286439b 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -1,27 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js'; +import type { NotesRepository, FollowingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; -import { CacheService } from '@/core/CacheService.js'; -import { UserFollowingService } from '@/core/UserFollowingService.js'; -import { MiLocalUser } from '@/models/User.js'; -import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; export const meta = { tags: ['notes'], requireCredential: true, - kind: 'read:account', res: { type: 'array', @@ -42,205 +32,104 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, - allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false }, - withRenotes: { type: 'boolean', default: true }, + withReplies: { type: 'boolean', default: false }, }, required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, - @Inject(DI.channelFollowingsRepository) - private channelFollowingsRepository: ChannelFollowingsRepository, + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, private noteEntityService: NoteEntityService, + private queryService: QueryService, private activeUsersChart: ActiveUsersChart, private idService: IdService, - private cacheService: CacheService, - private fanoutTimelineEndpointService: FanoutTimelineEndpointService, - private userFollowingService: UserFollowingService, - private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); - const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); + const followees = await this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }) + .getMany(); - if (!this.serverSettings.enableFanoutTimeline) { - const timeline = await this.getFromDb({ - untilId, - sinceId, - limit: ps.limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withRenotes: ps.withRenotes, - }, me); + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); - process.nextTick(() => { - this.activeUsersChart.read(me); - }); + if (followees.length > 0) { + const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; - return await this.noteEntityService.packMany(timeline, me); + query.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); + } else { + query.andWhere('note.userId = :meId', { meId: me.id }); } - const [ - followings, - ] = await Promise.all([ - this.cacheService.userFollowingsCache.fetch(me.id), - ]); + this.queryService.generateChannelQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); + this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateMutedNoteQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - const timeline = this.fanoutTimelineEndpointService.timeline({ - untilId, - sinceId, - limit: ps.limit, - allowPartial: ps.allowPartial, - me, - useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, - redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`], - alwaysIncludeMyNotes: true, - excludePureRenotes: !ps.withRenotes, - noteFilter: note => { - if (note.reply && note.reply.visibility === 'followers') { - if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false; - } + if (ps.includeMyRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.userId != :meId', { meId: me.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } - return true; - }, - dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ - untilId, - sinceId, - limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withRenotes: ps.withRenotes, - }, me), - }); + if (ps.includeRenotedMyNotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + if (ps.includeLocalRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserHost IS NOT NULL'); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + //#endregion + + const timeline = await query.take(ps.limit).getMany(); process.nextTick(() => { this.activeUsersChart.read(me); }); - return timeline; + return await this.noteEntityService.packMany(timeline, me); }); } - - private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; includeMyRenotes: boolean; includeRenotedMyNotes: boolean; includeLocalRenotes: boolean; withFiles: boolean; withRenotes: boolean; }, me: MiLocalUser) { - const followees = await this.userFollowingService.getFollowees(me.id); - const followingChannels = await this.channelFollowingsRepository.find({ - where: { - followerId: me.id, - }, - }); - - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - if (followees.length > 0 && followingChannels.length > 0) { - // ユーザー・チャンネルともにフォローあり - const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; - const followingChannelIds = followingChannels.map(x => x.followeeId); - query.andWhere(new Brackets(qb => { - qb - .where(new Brackets(qb2 => { - qb2 - .where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }) - .andWhere('note.channelId IS NULL'); - })) - .orWhere('note.channelId IN (:...followingChannelIds)', { followingChannelIds }); - })); - } else if (followees.length > 0) { - // ユーザーフォローのみ(チャンネルフォローなし) - const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; - query - .andWhere('note.channelId IS NULL') - .andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); - } else if (followingChannels.length > 0) { - // チャンネルフォローのみ(ユーザーフォローなし) - const followingChannelIds = followingChannels.map(x => x.followeeId); - query.andWhere(new Brackets(qb => { - qb - .where('note.channelId IN (:...followingChannelIds)', { followingChannelIds }) - .orWhere('note.userId = :meId', { meId: me.id }); - })); - } else { - // フォローなし - query - .andWhere('note.channelId IS NULL') - .andWhere('note.userId = :meId', { meId: me.id }); - } - - query.andWhere(new Brackets(qb => { - qb - .where('note.replyId IS NULL') // 返信ではない - .orWhere(new Brackets(qb => { - qb // 返信だけど投稿者自身への返信 - .where('note.replyId IS NOT NULL') - .andWhere('note.replyUserId = note.userId'); - })); - })); - - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBaseNoteFilteringQuery(query, me); - this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - - if (ps.withRenotes === false) { - query.andWhere('note.renoteId IS NULL'); - } - //#endregion - - return await query.limit(ps.limit).getMany(); - } } diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index e9a6a36b02..66655234a1 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -1,50 +1,31 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { URLSearchParams } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { GetterService } from '@/server/api/GetterService.js'; -import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; -import { MiMeta } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes'], - requireCredential: true, - kind: 'read:account', + requireCredential: false, res: { type: 'object', - optional: true, nullable: false, - properties: { - sourceLang: { type: 'string' }, - text: { type: 'string' }, - }, + optional: false, nullable: false, }, errors: { - unavailable: { - message: 'Translate of notes unavailable.', - code: 'UNAVAILABLE', - id: '50a70314-2d8a-431b-b433-efa5cc56444c', - }, noSuchNote: { message: 'No such note.', code: 'NO_SUCH_NOTE', id: 'bea9b03f-36e0-49c5-a4db-627a029f8971', }, - cannotTranslateInvisibleNote: { - message: 'Cannot translate invisible note.', - code: 'CANNOT_TRANSLATE_INVISIBLE_NOTE', - id: 'ea29f2ca-c368-43b3-aaf1-5ac3e74bbe5d', - }, }, } as const; @@ -57,49 +38,50 @@ export const paramDef = { required: ['noteId', 'targetLang'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, + @Inject(DI.config) + private config: Config, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, private getterService: GetterService, + private metaService: MetaService, private httpRequestService: HttpRequestService, - private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const policies = await this.roleService.getUserPolicies(me.id); - if (!policies.canUseTranslator) { - throw new ApiError(meta.errors.unavailable); - } - const note = await this.getterService.getNote(ps.noteId).catch(err => { if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; }); - if (!(await this.noteEntityService.isVisibleForMe(note, me.id))) { - throw new ApiError(meta.errors.cannotTranslateInvisibleNote); + if (!(await this.noteEntityService.isVisibleForMe(note, me ? me.id : null))) { + return 204; // TODO: 良い感じのエラー返す } if (note.text == null) { - return; + return 204; } - if (this.serverSettings.deeplAuthKey == null) { - throw new ApiError(meta.errors.unavailable); + const instance = await this.metaService.fetch(); + + if (instance.deeplAuthKey == null) { + return 204; // TODO: 良い感じのエラー返す } let targetLang = ps.targetLang; if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; const params = new URLSearchParams(); - params.append('auth_key', this.serverSettings.deeplAuthKey); + params.append('auth_key', instance.deeplAuthKey); params.append('text', note.text); params.append('target_lang', targetLang); - const endpoint = this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; + const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; const res = await this.httpRequestService.send(endpoint, { method: 'POST', diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts index 73e70cfde4..e9581beedc 100644 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, NotesRepository } from '@/models/_.js'; +import type { UsersRepository, NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteDeleteService } from '@/core/NoteDeleteService.js'; import { DI } from '@/di-symbols.js'; @@ -42,8 +37,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 614cd9204d..afc9bc4213 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -1,26 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; -import type { MiMeta, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; -import { IdService } from '@/core/IdService.js'; -import { QueryService } from '@/core/QueryService.js'; -import { MiLocalUser } from '@/models/User.js'; -import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; export const meta = { tags: ['notes', 'lists'], requireCredential: true, - kind: 'read:account', res: { type: 'array', @@ -50,11 +41,9 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, - allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, - withRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false, @@ -64,31 +53,24 @@ export const paramDef = { required: ['listId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, private noteEntityService: NoteEntityService, - private activeUsersChart: ActiveUsersChart, - private idService: IdService, - private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, + private activeUsersChart: ActiveUsersChart, ) { super(meta, paramDef, async (ps, me) => { - const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); - const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const list = await this.userListsRepository.findOneBy({ id: ps.listId, userId: me.id, @@ -98,140 +80,58 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchList); } - if (!this.serverSettings.enableFanoutTimeline) { - const timeline = await this.getFromDb(list, { - untilId, - sinceId, - limit: ps.limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withRenotes: ps.withRenotes, - }, me); + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .innerJoin(this.userListJoiningsRepository.metadata.targetName, 'userListJoining', 'userListJoining.userId = note.userId') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .andWhere('userListJoining.userListId = :userListId', { userListId: list.id }); - this.activeUsersChart.read(me); + this.queryService.generateVisibilityQuery(query, me); - return await this.noteEntityService.packMany(timeline, me); + if (ps.includeMyRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.userId != :meId', { meId: me.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); } - const timeline = await this.fanoutTimelineEndpointService.timeline({ - untilId, - sinceId, - limit: ps.limit, - allowPartial: ps.allowPartial, - me, - useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, - redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`], - alwaysIncludeMyNotes: true, - excludePureRenotes: !ps.withRenotes, - dbFallback: async (untilId, sinceId, limit) => await this.getFromDb(list, { - untilId, - sinceId, - limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withRenotes: ps.withRenotes, - }, me), - }); + if (ps.includeRenotedMyNotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + if (ps.includeLocalRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserHost IS NOT NULL'); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + //#endregion + + const timeline = await query.take(ps.limit).getMany(); this.activeUsersChart.read(me); - return timeline; + return await this.noteEntityService.packMany(timeline, me); }); } - - private async getFromDb(list: MiUserList, ps: { - untilId: string | null, - sinceId: string | null, - limit: number, - includeMyRenotes: boolean, - includeRenotedMyNotes: boolean, - includeLocalRenotes: boolean, - withFiles: boolean, - withRenotes: boolean, - }, me: MiLocalUser) { - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .innerJoin(this.userListMembershipsRepository.metadata.targetName, 'userListMemberships', 'userListMemberships.userId = note.userId') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .andWhere('userListMemberships.userListId = :userListId', { userListId: list.id }) - .andWhere('note.channelId IS NULL') // チャンネルノートではない - .andWhere(new Brackets(qb => { - qb - .where('note.replyId IS NULL') // 返信ではない - .orWhere(new Brackets(qb => { - qb // 返信だけど投稿者自身への返信 - .where('note.replyId IS NOT NULL') - .andWhere('note.replyUserId = note.userId'); - })) - .orWhere(new Brackets(qb => { - qb // 返信だけど自分宛ての返信 - .where('note.replyId IS NOT NULL') - .andWhere('note.replyUserId = :meId', { meId: me.id }); - })) - .orWhere(new Brackets(qb => { - qb // 返信だけどwithRepliesがtrueの場合 - .where('note.replyId IS NOT NULL') - .andWhere('userListMemberships.withReplies = true'); - })); - })); - - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBaseNoteFilteringQuery(query, me); - this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion - - return await query.limit(ps.limit).getMany(); - } } diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts index 7671b58e6b..4102a924ad 100644 --- a/packages/backend/src/server/api/endpoints/notifications/create.ts +++ b/packages/backend/src/server/api/endpoints/notifications/create.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NotificationService } from '@/core/NotificationService.js'; @@ -14,11 +9,6 @@ export const meta = { kind: 'write:notifications', - limit: { - duration: 1000 * 60, - max: 10, - }, - errors: { }, } as const; @@ -33,8 +23,9 @@ export const paramDef = { required: ['body'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private notificationService: NotificationService, ) { @@ -42,8 +33,8 @@ export default class extends Endpoint { // eslint- this.notificationService.createNotification(user.id, 'app', { appAccessTokenId: token ? token.id : null, customBody: ps.body, - customHeader: ps.header ?? token?.name ?? null, - customIcon: ps.icon ?? token?.iconUrl ?? null, + customHeader: ps.header, + customIcon: ps.icon, }); }); } diff --git a/packages/backend/src/server/api/endpoints/notifications/flush.ts b/packages/backend/src/server/api/endpoints/notifications/flush.ts deleted file mode 100644 index 47c0642fd1..0000000000 --- a/packages/backend/src/server/api/endpoints/notifications/flush.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NotificationService } from '@/core/NotificationService.js'; - -export const meta = { - tags: ['notifications', 'account'], - - requireCredential: true, - - kind: 'write:notifications', -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private notificationService: NotificationService, - ) { - super(meta, paramDef, async (ps, me) => { - this.notificationService.flushAllNotifications(me.id); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts index 6565125c00..e601bf9d5b 100644 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -1,10 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { NotificationService } from '@/core/NotificationService.js'; export const meta = { @@ -21,8 +17,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( private notificationService: NotificationService, ) { diff --git a/packages/backend/src/server/api/endpoints/notifications/test-notification.ts b/packages/backend/src/server/api/endpoints/notifications/test-notification.ts deleted file mode 100644 index 50b850a519..0000000000 --- a/packages/backend/src/server/api/endpoints/notifications/test-notification.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NotificationService } from '@/core/NotificationService.js'; - -export const meta = { - tags: ['notifications'], - - requireCredential: true, - - kind: 'write:notifications', - - limit: { - duration: 1000 * 60, - max: 10, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private notificationService: NotificationService, - ) { - super(meta, paramDef, async (ps, user) => { - this.notificationService.createNotification(user.id, 'test', {}); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts index ce454ab24a..1d6fb567f0 100644 --- a/packages/backend/src/server/api/endpoints/page-push.ts +++ b/packages/backend/src/server/api/endpoints/page-push.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository } from '@/models/_.js'; +import type { PagesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -34,8 +29,9 @@ export const paramDef = { required: ['pageId', 'event'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, @@ -55,7 +51,7 @@ export default class extends Endpoint { // eslint- var: ps.var, userId: me.id, user: await this.userEntityService.pack(me.id, { id: page.userId }, { - schema: 'UserDetailed', + detail: true, }), }); }); diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index 6de5fe3d44..e08ab399f8 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository, PagesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, PagesRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; -import { MiPage, pageNameSchema } from '@/models/Page.js'; +import { Page } from '@/models/entities/Page.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -51,7 +46,7 @@ export const paramDef = { type: 'object', properties: { title: { type: 'string' }, - name: { ...pageNameSchema, minLength: 1 }, + name: { type: 'string', minLength: 1 }, summary: { type: 'string', nullable: true }, content: { type: 'array', items: { type: 'object', additionalProperties: true, @@ -68,8 +63,9 @@ export const paramDef = { required: ['title', 'name', 'content', 'variables', 'script'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, @@ -102,8 +98,9 @@ export default class extends Endpoint { // eslint- } }); - const page = await this.pagesRepository.insertOne(new MiPage({ - id: this.idService.gen(), + const page = await this.pagesRepository.insert(new Page({ + id: this.idService.genId(), + createdAt: new Date(), updatedAt: new Date(), title: ps.title, name: ps.name, @@ -117,7 +114,7 @@ export default class extends Endpoint { // eslint- alignCenter: ps.alignCenter, hideTitleWhenPinned: ps.hideTitleWhenPinned, font: ps.font, - })); + })).then(x => this.pagesRepository.findOneByOrFail(x.identifiers[0])); return await this.pageEntityService.pack(page); }); diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts index f2bc946788..e64733131c 100644 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ b/packages/backend/src/server/api/endpoints/pages/delete.ts @@ -1,14 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository, UsersRepository } from '@/models/_.js'; +import type { PagesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -41,40 +34,23 @@ export const paramDef = { required: ['pageId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private moderationLogService: ModerationLogService, - private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const page = await this.pagesRepository.findOneBy({ id: ps.pageId }); - if (page == null) { throw new ApiError(meta.errors.noSuchPage); } - - if (!await this.roleService.isModerator(me) && page.userId !== me.id) { + if (page.userId !== me.id) { throw new ApiError(meta.errors.accessDenied); } await this.pagesRepository.delete(page.id); - - if (page.userId !== me.id) { - const user = await this.usersRepository.findOneByOrFail({ id: page.userId }); - this.moderationLogService.log(me, 'deletePage', { - pageId: page.id, - pageUserId: page.userId, - pageUserUsername: user.username, - page, - }); - } }); } } diff --git a/packages/backend/src/server/api/endpoints/pages/featured.ts b/packages/backend/src/server/api/endpoints/pages/featured.ts index a47b69e56e..31844165e2 100644 --- a/packages/backend/src/server/api/endpoints/pages/featured.ts +++ b/packages/backend/src/server/api/endpoints/pages/featured.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository } from '@/models/_.js'; +import type { PagesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -31,8 +26,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, @@ -45,7 +41,7 @@ export default class extends Endpoint { // eslint- .andWhere('page.likedCount > 0') .orderBy('page.likedCount', 'DESC'); - const pages = await query.limit(10).getMany(); + const pages = await query.take(10).getMany(); return await this.pageEntityService.packMany(pages, me); }); diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index 11eed693ad..543c126d9c 100644 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ b/packages/backend/src/server/api/endpoints/pages/like.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository, PageLikesRepository } from '@/models/_.js'; +import type { PagesRepository, PageLikesRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -48,8 +43,9 @@ export const paramDef = { required: ['pageId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, @@ -70,20 +66,19 @@ export default class extends Endpoint { // eslint- } // if already liked - const exist = await this.pageLikesRepository.exists({ - where: { - pageId: page.id, - userId: me.id, - }, + const exist = await this.pageLikesRepository.findOneBy({ + pageId: page.id, + userId: me.id, }); - if (exist) { + if (exist != null) { throw new ApiError(meta.errors.alreadyLiked); } // Create like await this.pageLikesRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), pageId: page.id, userId: me.id, }); diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts index 8427bab2d5..bf2b2a431e 100644 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ b/packages/backend/src/server/api/endpoints/pages/show.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, PagesRepository } from '@/models/_.js'; -import type { MiPage } from '@/models/Page.js'; +import type { UsersRepository, PagesRepository } from '@/models/index.js'; +import type { Page } from '@/models/entities/Page.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -33,27 +28,21 @@ export const meta = { } as const; export const paramDef = { + type: 'object', + properties: { + pageId: { type: 'string', format: 'misskey:id' }, + name: { type: 'string' }, + username: { type: 'string' }, + }, anyOf: [ - { - type: 'object', - properties: { - pageId: { type: 'string', format: 'misskey:id' }, - }, - required: ['pageId'], - }, - { - type: 'object', - properties: { - name: { type: 'string' }, - username: { type: 'string' }, - }, - required: ['name', 'username'], - }, + { required: ['pageId'] }, + { required: ['name', 'username'] }, ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -64,11 +53,11 @@ export default class extends Endpoint { // eslint- private pageEntityService: PageEntityService, ) { super(meta, paramDef, async (ps, me) => { - let page: MiPage | null = null; + let page: Page | null = null; - if ('pageId' in ps) { + if (ps.pageId) { page = await this.pagesRepository.findOneBy({ id: ps.pageId }); - } else { + } else if (ps.name && ps.username) { const author = await this.usersRepository.findOneBy({ host: IsNull(), usernameLower: ps.username.toLowerCase(), diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts index 70c965e0ad..f0c0198460 100644 --- a/packages/backend/src/server/api/endpoints/pages/unlike.ts +++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository, PageLikesRepository } from '@/models/_.js'; +import type { PagesRepository, PageLikesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -41,8 +36,9 @@ export const paramDef = { required: ['pageId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index a6aeb6002e..751274067e 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -1,16 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository, DriveFilesRepository } from '@/models/_.js'; +import type { PagesRepository, DriveFilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -import { pageNameSchema } from '@/models/Page.js'; export const meta = { tags: ['pages'], @@ -32,11 +26,13 @@ export const meta = { code: 'NO_SUCH_PAGE', id: '21149b9e-3616-4778-9592-c4ce89f5a864', }, + accessDenied: { message: 'Access denied.', code: 'ACCESS_DENIED', id: '3c15cd52-3b4b-4274-967d-6456fc4f792b', }, + noSuchFile: { message: 'No such file.', code: 'NO_SUCH_FILE', @@ -55,7 +51,7 @@ export const paramDef = { properties: { pageId: { type: 'string', format: 'misskey:id' }, title: { type: 'string' }, - name: { ...pageNameSchema, minLength: 1 }, + name: { type: 'string', minLength: 1 }, summary: { type: 'string', nullable: true }, content: { type: 'array', items: { type: 'object', additionalProperties: true, @@ -69,11 +65,12 @@ export const paramDef = { alignCenter: { type: 'boolean' }, hideTitleWhenPinned: { type: 'boolean' }, }, - required: ['pageId'], + required: ['pageId', 'title', 'name', 'content', 'variables', 'script'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, @@ -90,8 +87,9 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.accessDenied); } + let eyeCatchingImage = null; if (ps.eyeCatchingImageId != null) { - const eyeCatchingImage = await this.driveFilesRepository.findOneBy({ + eyeCatchingImage = await this.driveFilesRepository.findOneBy({ id: ps.eyeCatchingImageId, userId: me.id, }); @@ -101,30 +99,32 @@ export default class extends Endpoint { // eslint- } } - if (ps.name != null) { - await this.pagesRepository.findBy({ - id: Not(ps.pageId), - userId: me.id, - name: ps.name, - }).then(result => { - if (result.length > 0) { - throw new ApiError(meta.errors.nameAlreadyExists); - } - }); - } + await this.pagesRepository.findBy({ + id: Not(ps.pageId), + userId: me.id, + name: ps.name, + }).then(result => { + if (result.length > 0) { + throw new ApiError(meta.errors.nameAlreadyExists); + } + }); await this.pagesRepository.update(page.id, { updatedAt: new Date(), title: ps.title, - name: ps.name, + name: ps.name === undefined ? page.name : ps.name, summary: ps.summary === undefined ? page.summary : ps.summary, content: ps.content, variables: ps.variables, script: ps.script, - alignCenter: ps.alignCenter, - hideTitleWhenPinned: ps.hideTitleWhenPinned, - font: ps.font, - eyeCatchingImageId: ps.eyeCatchingImageId, + alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, + hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned, + font: ps.font === undefined ? page.font : ps.font, + eyeCatchingImageId: ps.eyeCatchingImageId === null + ? null + : ps.eyeCatchingImageId === undefined + ? page.eyeCatchingImageId + : eyeCatchingImage!.id, }); }); } diff --git a/packages/backend/src/server/api/endpoints/ping.ts b/packages/backend/src/server/api/endpoints/ping.ts index e218a8f755..5807bf101e 100644 --- a/packages/backend/src/server/api/endpoints/ping.ts +++ b/packages/backend/src/server/api/endpoints/ping.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -29,8 +24,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( ) { super(meta, paramDef, async () => { diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 5b0b656c63..f2c6e798ef 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -1,14 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { MiMeta, UsersRepository } from '@/models/_.js'; +import type { UsersRepository } from '@/models/index.js'; import * as Acct from '@/misc/acct.js'; -import type { MiUser } from '@/models/User.js'; +import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MetaService } from '@/core/MetaService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -34,24 +30,25 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, + private metaService: MetaService, private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { - const users = await Promise.all(this.serverSettings.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({ + const meta = await this.metaService.fetch(); + + const users = await Promise.all(meta.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({ usernameLower: acct.username.toLowerCase(), host: acct.host ?? IsNull(), }))); - return await this.userEntityService.packMany(users.filter(x => x != null), me, { schema: 'UserDetailed' }); + return await this.userEntityService.packMany(users.filter(x => x !== null) as User[], me, { detail: true }); }); } } diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts index 9f7d078014..90febdbce7 100644 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ b/packages/backend/src/server/api/endpoints/promo/read.ts @@ -1,21 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { PromoReadsRepository } from '@/models/_.js'; +import type { PromoReadsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['notes'], requireCredential: true, - kind: 'write:account', errors: { noSuchNote: { @@ -34,8 +28,9 @@ export const paramDef = { required: ['noteId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.promoReadsRepository) private promoReadsRepository: PromoReadsRepository, @@ -49,19 +44,18 @@ export default class extends Endpoint { // eslint- throw err; }); - const exist = await this.promoReadsRepository.exists({ - where: { - noteId: note.id, - userId: me.id, - }, + const exist = await this.promoReadsRepository.findOneBy({ + noteId: note.id, + userId: me.id, }); - if (exist) { + if (exist != null) { return; } await this.promoReadsRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), noteId: note.id, userId: me.id, }); diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts index 84a1f010d4..beb5850d78 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts @@ -1,16 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import type { RenoteMutingsRepository } from '@/models/index.js'; +import type { RenoteMuting } from '@/models/entities/RenoteMuting.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; -import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js"; -import type { RenoteMutingsRepository } from '@/models/_.js'; export const meta = { tags: ['account'], @@ -54,14 +51,16 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, + private globalEventService: GlobalEventService, private getterService: GetterService, - private userRenoteMutingService: UserRenoteMutingService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const muter = me; @@ -72,25 +71,28 @@ export default class extends Endpoint { // eslint- } // Get mutee - const mutee = await this.getterService.getUser(ps.userId).catch(err => { + const mutee = await getterService.getUser(ps.userId).catch(err => { if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); throw err; }); // Check if already muting - const exist = await this.renoteMutingsRepository.exists({ - where: { - muterId: muter.id, - muteeId: mutee.id, - }, + const exist = await this.renoteMutingsRepository.findOneBy({ + muterId: muter.id, + muteeId: mutee.id, }); - if (exist === true) { + if (exist != null) { throw new ApiError(meta.errors.alreadyMuting); } // Create mute - await this.userRenoteMutingService.mute(muter, mutee); + await this.renoteMutingsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + muterId: muter.id, + muteeId: mutee.id, + } as RenoteMuting); }); } } diff --git a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts index 1a584b8404..70901a1406 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts @@ -1,15 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RenoteMutingsRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; -import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js"; -import type { RenoteMutingsRepository } from '@/models/_.js'; export const meta = { tags: ['account'], @@ -47,14 +42,15 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, + private globalEventService: GlobalEventService, private getterService: GetterService, - private userRenoteMutingService: UserRenoteMutingService, ) { super(meta, paramDef, async (ps, me) => { const muter = me; @@ -81,7 +77,9 @@ export default class extends Endpoint { // eslint- } // Delete mute - await this.userRenoteMutingService.unmute([exist]); + await this.renoteMutingsRepository.delete({ + id: exist.id, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/renote-mute/list.ts b/packages/backend/src/server/api/endpoints/renote-mute/list.ts index 3be01f989a..b2d7addb64 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/list.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/list.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RenoteMutingsRepository } from '@/models/_.js'; +import type { RenoteMutingsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { RenoteMutingEntityService } from '@/core/entities/RenoteMutingEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -38,8 +33,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, @@ -52,7 +48,7 @@ export default class extends Endpoint { // eslint- .andWhere('muting.muterId = :meId', { meId: me.id }); const mutings = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.renoteMutingEntityService.packMany(mutings, me); diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts index 86fe6a2e6e..284ed8410d 100644 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { PasswordResetRequestsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { PasswordResetRequestsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; import type { Config } from '@/config.js'; @@ -40,8 +35,9 @@ export const paramDef = { required: ['username', 'email'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.config) private config: Config, @@ -84,7 +80,8 @@ export default class extends Endpoint { // eslint- const token = secureRndstr(64, { chars: L_CHARS }); await this.passwordResetRequestsRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), userId: profile.userId, token, }); diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index 552362b64a..1d4825f812 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -1,17 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import * as Redis from 'ioredis'; -import { LoggerService } from '@/core/LoggerService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { resetDb } from '@/misc/reset-db.js'; -import { MetaService } from '@/core/MetaService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; export const meta = { tags: ['non-productive'], @@ -31,35 +23,22 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.db) private db: DataSource, @Inject(DI.redis) private redisClient: Redis.Redis, - - private loggerService: LoggerService, - private metaService: MetaService, - private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { if (process.env.NODE_ENV !== 'test') throw new Error('NODE_ENV is not a test'); - const logger = this.loggerService.getLogger('reset-db'); - logger.info('---- Resetting database...'); - - await this.redisClient.flushdb(); + await redisClient.flushdb(); await resetDb(this.db); - // DIコンテナで管理しているmetaのインスタンスには上記のリセット処理が届かないため、 - // 初期値を流して明示的にリフレッシュする - const meta = await this.metaService.fetch(true); - this.globalEventService.publishInternalEvent('metaUpdated', { after: meta }); - - logger.info('---- Database reset complete.'); - await new Promise(resolve => setTimeout(resolve, 1000)); }); } diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index 9693892637..e6f1af7b22 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -1,14 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; -import type { UserProfilesRepository, PasswordResetRequestsRepository } from '@/models/_.js'; +import type { UserProfilesRepository, PasswordResetRequestsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['reset password'], @@ -31,16 +25,15 @@ export const paramDef = { required: ['token', 'password'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.passwordResetRequestsRepository) private passwordResetRequestsRepository: PasswordResetRequestsRepository, @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - - private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const req = await this.passwordResetRequestsRepository.findOneByOrFail({ @@ -48,7 +41,7 @@ export default class extends Endpoint { // eslint- }); // 発行してから30分以上経過していたら無効 - if (Date.now() - this.idService.parse(req.id).date.getTime() > 1000 * 60 * 30) { + if (Date.now() - req.createdAt.getTime() > 1000 * 60 * 30) { throw new Error(); // TODO } diff --git a/packages/backend/src/server/api/endpoints/retention.ts b/packages/backend/src/server/api/endpoints/retention.ts index 4695f32042..e9c0fd4dcd 100644 --- a/packages/backend/src/server/api/endpoints/retention.ts +++ b/packages/backend/src/server/api/endpoints/retention.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { RetentionAggregationsRepository } from '@/models/_.js'; +import type { RetentionAggregationsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -14,32 +9,6 @@ export const meta = { requireCredential: false, res: { - type: 'array', - items: { - type: 'object', - properties: { - createdAt: { - type: 'string', - format: 'date-time', - }, - users: { - type: 'number', - }, - data: { - type: 'object', - additionalProperties: { - anyOf: [{ - type: 'number', - }], - }, - }, - }, - required: [ - 'createdAt', - 'users', - 'data', - ], - }, }, allowGet: true, @@ -52,8 +21,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.retentionAggregationsRepository) private retentionAggregationsRepository: RetentionAggregationsRepository, diff --git a/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts b/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts deleted file mode 100644 index dd6f273e01..0000000000 --- a/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ReversiService } from '@/core/ReversiService.js'; - -export const meta = { - requireCredential: true, - - kind: 'write:account', - - errors: { - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id', nullable: true }, - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private reversiService: ReversiService, - ) { - super(meta, paramDef, async (ps, me) => { - if (ps.userId) { - await this.reversiService.matchSpecificUserCancel(me, ps.userId); - return; - } else { - await this.reversiService.matchAnyUserCancel(me); - } - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/reversi/games.ts b/packages/backend/src/server/api/endpoints/reversi/games.ts deleted file mode 100644 index 6b06068727..0000000000 --- a/packages/backend/src/server/api/endpoints/reversi/games.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Brackets } from 'typeorm'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; -import { DI } from '@/di-symbols.js'; -import type { ReversiGamesRepository } from '@/models/_.js'; -import { QueryService } from '@/core/QueryService.js'; - -export const meta = { - requireCredential: false, - - res: { - type: 'array', - optional: false, nullable: false, - items: { ref: 'ReversiGameLite' }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - my: { type: 'boolean', default: false }, - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.reversiGamesRepository) - private reversiGamesRepository: ReversiGamesRepository, - - private reversiGameEntityService: ReversiGameEntityService, - private queryService: QueryService, - ) { - super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId) - .innerJoinAndSelect('game.user1', 'user1') - .innerJoinAndSelect('game.user2', 'user2'); - - if (ps.my && me) { - query.andWhere(new Brackets(qb => { - qb - .where('game.user1Id = :userId', { userId: me.id }) - .orWhere('game.user2Id = :userId', { userId: me.id }); - })); - } else { - query.andWhere('game.isStarted = TRUE'); - } - - const games = await query.take(ps.limit).getMany(); - - return await this.reversiGameEntityService.packLiteMany(games); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/reversi/invitations.ts b/packages/backend/src/server/api/endpoints/reversi/invitations.ts deleted file mode 100644 index 5b3b9da75b..0000000000 --- a/packages/backend/src/server/api/endpoints/reversi/invitations.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { ReversiService } from '@/core/ReversiService.js'; - -export const meta = { - requireCredential: true, - - kind: 'read:account', - - res: { - type: 'array', - optional: false, nullable: false, - items: { ref: 'UserLite' }, - }, -} as const; - -export const paramDef = { -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private userEntityService: UserEntityService, - private reversiService: ReversiService, - ) { - super(meta, paramDef, async (ps, me) => { - const invitations = await this.reversiService.getInvitations(me); - - return await this.userEntityService.packMany(invitations, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts deleted file mode 100644 index aa8b8a7d72..0000000000 --- a/packages/backend/src/server/api/endpoints/reversi/match.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ReversiService } from '@/core/ReversiService.js'; -import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; -import { ApiError } from '../../error.js'; -import { GetterService } from '../../GetterService.js'; - -export const meta = { - requireCredential: true, - - kind: 'write:account', - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '0b4f0559-b484-4e31-9581-3f73cee89b28', - }, - - isYourself: { - message: 'Target user is yourself.', - code: 'TARGET_IS_YOURSELF', - id: '96fd7bd6-d2bc-426c-a865-d055dcd2828e', - }, - }, - - res: { - type: 'object', - optional: true, - ref: 'ReversiGameDetailed', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id', nullable: true }, - noIrregularRules: { type: 'boolean', default: false }, - multiple: { type: 'boolean', default: false }, - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private getterService: GetterService, - private reversiService: ReversiService, - private reversiGameEntityService: ReversiGameEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - if (ps.userId === me.id) throw new ApiError(meta.errors.isYourself); - - const target = ps.userId ? await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw err; - }) : null; - - const game = target - ? await this.reversiService.matchSpecificUser(me, target, ps.multiple) - : await this.reversiService.matchAnyUser(me, { noIrregularRules: ps.noIrregularRules }, ps.multiple); - - if (game == null) return; - - return await this.reversiGameEntityService.packDetail(game); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/reversi/show-game.ts b/packages/backend/src/server/api/endpoints/reversi/show-game.ts deleted file mode 100644 index fc3b96eb51..0000000000 --- a/packages/backend/src/server/api/endpoints/reversi/show-game.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ReversiService } from '@/core/ReversiService.js'; -import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - requireCredential: false, - - errors: { - noSuchGame: { - message: 'No such game.', - code: 'NO_SUCH_GAME', - id: 'f13a03db-fae1-46c9-87f3-43c8165419e1', - }, - }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'ReversiGameDetailed', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - gameId: { type: 'string', format: 'misskey:id' }, - }, - required: ['gameId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private reversiService: ReversiService, - private reversiGameEntityService: ReversiGameEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const game = await this.reversiService.get(ps.gameId); - - if (game == null) { - throw new ApiError(meta.errors.noSuchGame); - } - - return await this.reversiGameEntityService.packDetail(game); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/reversi/surrender.ts b/packages/backend/src/server/api/endpoints/reversi/surrender.ts deleted file mode 100644 index 75e5372862..0000000000 --- a/packages/backend/src/server/api/endpoints/reversi/surrender.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ReversiService } from '@/core/ReversiService.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - requireCredential: true, - - kind: 'write:account', - - errors: { - noSuchGame: { - message: 'No such game.', - code: 'NO_SUCH_GAME', - id: 'ace0b11f-e0a6-4076-a30d-e8284c81b2df', - }, - - alreadyEnded: { - message: 'That game has already ended.', - code: 'ALREADY_ENDED', - id: '6c2ad4a6-cbf1-4a5b-b187-b772826cfc6d', - }, - - accessDenied: { - message: 'Access denied.', - code: 'ACCESS_DENIED', - id: '6e04164b-a992-4c93-8489-2123069973e1', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - gameId: { type: 'string', format: 'misskey:id' }, - }, - required: ['gameId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private reversiService: ReversiService, - ) { - super(meta, paramDef, async (ps, me) => { - const game = await this.reversiService.get(ps.gameId); - - if (game == null) { - throw new ApiError(meta.errors.noSuchGame); - } - - if (game.isEnded) { - throw new ApiError(meta.errors.alreadyEnded); - } - - if ((game.user1Id !== me.id) && (game.user2Id !== me.id)) { - throw new ApiError(meta.errors.accessDenied); - } - - await this.reversiService.surrender(game.id, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/reversi/verify.ts b/packages/backend/src/server/api/endpoints/reversi/verify.ts deleted file mode 100644 index 981735a3d7..0000000000 --- a/packages/backend/src/server/api/endpoints/reversi/verify.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ReversiService } from '@/core/ReversiService.js'; -import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - errors: { - noSuchGame: { - message: 'No such game.', - code: 'NO_SUCH_GAME', - id: '8fb05624-b525-43dd-90f7-511852bdfeee', - }, - }, - - res: { - type: 'object', - optional: false, nullable: false, - properties: { - desynced: { type: 'boolean' }, - game: { - type: 'object', - optional: true, nullable: true, - ref: 'ReversiGameDetailed', - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - gameId: { type: 'string', format: 'misskey:id' }, - crc32: { type: 'string' }, - }, - required: ['gameId', 'crc32'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private reversiService: ReversiService, - private reversiGameEntityService: ReversiGameEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const game = await this.reversiService.checkCrc(ps.gameId, ps.crc32); - if (game) { - return { - desynced: true, - game: await this.reversiGameEntityService.packDetail(game), - }; - } else { - return { - desynced: false, - }; - } - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/roles/list.ts b/packages/backend/src/server/api/endpoints/roles/list.ts index b087aa242b..5ad29839c2 100644 --- a/packages/backend/src/server/api/endpoints/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/roles/list.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository } from '@/models/_.js'; +import type { RolesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; @@ -13,17 +8,6 @@ export const meta = { tags: ['role'], requireCredential: true, - kind: 'read:account', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Role', - }, - }, } as const; export const paramDef = { @@ -34,8 +18,9 @@ export const paramDef = { ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index e8a760e9f8..42e36cb04a 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -1,24 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NotesRepository, RolesRepository } from '@/models/_.js'; +import type { NotesRepository, RolesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { tags: ['role', 'notes'], requireCredential: true, - kind: 'read:account', errors: { noSuchRole: { @@ -52,11 +45,12 @@ export const paramDef = { required: ['roleId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, + @Inject(DI.redis) + private redisClient: Redis.Redis, @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -67,12 +61,8 @@ export default class extends Endpoint { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, - private fanoutTimelineService: FanoutTimelineService, ) { super(meta, paramDef, async (ps, me) => { - const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); - const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const role = await this.rolesRepository.findOneBy({ id: ps.roleId, isPublic: true, @@ -81,12 +71,21 @@ export default class extends Endpoint { // eslint- if (role == null) { throw new ApiError(meta.errors.noSuchRole); } - if (!role.isExplorable) { + if (!role.isExplorable) { + return []; + } + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + const noteIdsRes = await this.redisClient.xrevrange( + `roleTimeline:${role.id}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', + 'COUNT', limit); + + if (noteIdsRes.length === 0) { return []; } - let noteIds = await this.fanoutTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId); - noteIds = noteIds.slice(0, ps.limit); + const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId); if (noteIds.length === 0) { return []; @@ -102,7 +101,8 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBaseNoteFilteringQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); const notes = await query.getMany(); notes.sort((a, b) => a.id > b.id ? -1 : 1); diff --git a/packages/backend/src/server/api/endpoints/roles/show.ts b/packages/backend/src/server/api/endpoints/roles/show.ts index 38477c5e8e..cc755dcc76 100644 --- a/packages/backend/src/server/api/endpoints/roles/show.ts +++ b/packages/backend/src/server/api/endpoints/roles/show.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { RolesRepository } from '@/models/_.js'; +import type { RolesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; @@ -22,12 +17,6 @@ export const meta = { id: 'de5502bf-009a-4639-86c1-fec349e46dcb', }, }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'Role', - }, } as const; export const paramDef = { @@ -38,8 +27,9 @@ export const paramDef = { required: ['roleId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index 48d350af59..b2cb8b42a8 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; -import type { RoleAssignmentsRepository, RolesRepository } from '@/models/_.js'; +import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; @@ -24,25 +19,6 @@ export const meta = { id: '30aaaee3-4792-48dc-ab0d-cf501a575ac5', }, }, - - res: { - type: 'array', - items: { - type: 'object', - nullable: false, - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - user: { - type: 'object', - ref: 'UserDetailed', - }, - }, - required: ['id', 'user'], - }, - }, } as const; export const paramDef = { @@ -56,8 +32,9 @@ export const paramDef = { required: ['roleId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, @@ -81,23 +58,19 @@ export default class extends Endpoint { // eslint- const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) .andWhere('assign.roleId = :roleId', { roleId: role.id }) - .andWhere(new Brackets(qb => { - qb - .where('assign.expiresAt IS NULL') - .orWhere('assign.expiresAt > :now', { now: new Date() }); + .andWhere(new Brackets(qb => { qb + .where('assign.expiresAt IS NULL') + .orWhere('assign.expiresAt > :now', { now: new Date() }); })) .innerJoinAndSelect('assign.user', 'user'); const assigns = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); - const _users = assigns.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' }) - .then(users => new Map(users.map(u => [u.id, u]))); return await Promise.all(assigns.map(async assign => ({ id: assign.id, - user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }), + user: await this.userEntityService.pack(assign.user!, me, { detail: true }), }))); }); } diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index 8301c85f2e..1620e8ae52 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -1,68 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as os from 'node:os'; import si from 'systeminformation'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MiMeta } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: false, - allowGet: true, - cacheSec: 60 * 1, tags: ['meta'], - res: { - type: 'object', - optional: false, nullable: false, - properties: { - machine: { - type: 'string', - nullable: false, - }, - cpu: { - type: 'object', - nullable: false, - properties: { - model: { - type: 'string', - nullable: false, - }, - cores: { - type: 'number', - nullable: false, - }, - }, - }, - mem: { - type: 'object', - properties: { - total: { - type: 'number', - nullable: false, - }, - }, - }, - fs: { - type: 'object', - nullable: false, - properties: { - total: { - type: 'number', - nullable: false, - }, - used: { - type: 'number', - nullable: false, - }, - }, - }, - }, - }, } as const; export const paramDef = { @@ -71,28 +15,12 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, ) { super(meta, paramDef, async () => { - if (!this.serverSettings.enableServerMachineStats) return { - machine: '?', - cpu: { - model: '?', - cores: 0, - }, - mem: { - total: 0, - }, - fs: { - total: 0, - used: 0, - }, - }; - const memStats = await si.mem(); const fsStats = await si.fsSize(); diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts index 1e6983177f..48a85758a0 100644 --- a/packages/backend/src/server/api/endpoints/stats.ts +++ b/packages/backend/src/server/api/endpoints/stats.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { InstancesRepository, NoteReactionsRepository } from '@/models/_.js'; +import type { InstancesRepository, NoteReactionsRepository, NotesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import NotesChart from '@/core/chart/charts/notes.js'; @@ -57,9 +52,16 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index fd76df2d3c..bfd5de7b00 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -1,20 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { IdService } from '@/core/IdService.js'; -import type { MiMeta, SwSubscriptionsRepository } from '@/models/_.js'; +import type { SwSubscriptionsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; -import { PushNotificationService } from '@/core/PushNotificationService.js'; export const meta = { tags: ['account'], requireCredential: true, - secure: true, description: 'Register to receive push notifications.', @@ -58,17 +52,15 @@ export const paramDef = { required: ['endpoint', 'auth', 'publickey'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, - @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, private idService: IdService, - private pushNotificationService: PushNotificationService, + private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { // if already subscribed @@ -79,10 +71,12 @@ export default class extends Endpoint { // eslint- publickey: ps.publickey, }); + const instance = await this.metaService.fetch(true); + if (exist != null) { return { state: 'already-subscribed' as const, - key: this.serverSettings.swPublicKey, + key: instance.swPublicKey, userId: me.id, endpoint: exist.endpoint, sendReadMessage: exist.sendReadMessage, @@ -90,7 +84,8 @@ export default class extends Endpoint { // eslint- } await this.swSubscriptionsRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), userId: me.id, endpoint: ps.endpoint, auth: ps.auth, @@ -98,11 +93,9 @@ export default class extends Endpoint { // eslint- sendReadMessage: ps.sendReadMessage, }); - this.pushNotificationService.refreshCache(me.id); - return { state: 'subscribed' as const, - key: this.serverSettings.swPublicKey, + key: instance.swPublicKey, userId: me.id, endpoint: ps.endpoint, sendReadMessage: ps.sendReadMessage, diff --git a/packages/backend/src/server/api/endpoints/sw/show-registration.ts b/packages/backend/src/server/api/endpoints/sw/show-registration.ts index 797e4fd34d..bede10be5c 100644 --- a/packages/backend/src/server/api/endpoints/sw/show-registration.ts +++ b/packages/backend/src/server/api/endpoints/sw/show-registration.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { SwSubscriptionsRepository } from '@/models/_.js'; +import type { SwSubscriptionsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -12,7 +7,6 @@ export const meta = { tags: ['account'], requireCredential: true, - secure: true, description: 'Check push notification registration exists.', @@ -44,8 +38,9 @@ export const paramDef = { required: ['endpoint'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts index 2edf7fab1b..f12b98617d 100644 --- a/packages/backend/src/server/api/endpoints/sw/unregister.ts +++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts @@ -1,13 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { SwSubscriptionsRepository } from '@/models/_.js'; +import type { SwSubscriptionsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { PushNotificationService } from '@/core/PushNotificationService.js'; export const meta = { tags: ['account'], @@ -25,23 +19,18 @@ export const paramDef = { required: ['endpoint'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, - - private pushNotificationService: PushNotificationService, ) { super(meta, paramDef, async (ps, me) => { await this.swSubscriptionsRepository.delete({ ...(me ? { userId: me.id } : {}), endpoint: ps.endpoint, }); - - if (me) { - this.pushNotificationService.refreshCache(me.id); - } }); } } diff --git a/packages/backend/src/server/api/endpoints/sw/update-registration.ts b/packages/backend/src/server/api/endpoints/sw/update-registration.ts index 839a07c770..9f08c8148d 100644 --- a/packages/backend/src/server/api/endpoints/sw/update-registration.ts +++ b/packages/backend/src/server/api/endpoints/sw/update-registration.ts @@ -1,20 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { SwSubscriptionsRepository } from '@/models/_.js'; +import type { SwSubscriptionsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { PushNotificationService } from '@/core/PushNotificationService.js'; import { ApiError } from '../../error.js'; export const meta = { tags: ['account'], requireCredential: true, - secure: true, description: 'Update push notification registration.', @@ -42,7 +35,7 @@ export const meta = { code: 'NO_SUCH_REGISTRATION', id: ' b09d8066-8064-5613-efb6-0e963b21d012', }, - }, + } } as const; export const paramDef = { @@ -54,13 +47,12 @@ export const paramDef = { required: ['endpoint'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, - - private pushNotificationService: PushNotificationService, ) { super(meta, paramDef, async (ps, me) => { const swSubscription = await this.swSubscriptionsRepository.findOneBy({ @@ -80,8 +72,6 @@ export default class extends Endpoint { // eslint- sendReadMessage: swSubscription.sendReadMessage, }); - this.pushNotificationService.refreshCache(me.id); - return { userId: swSubscription.userId, endpoint: swSubscription.endpoint, diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts index 9231f0ab94..c88f7f2daf 100644 --- a/packages/backend/src/server/api/endpoints/test.ts +++ b/packages/backend/src/server/api/endpoints/test.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -12,34 +7,6 @@ export const meta = { description: 'Endpoint for testing input validation.', requireCredential: false, - - res: { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - optional: true, nullable: false, - }, - required: { - type: 'boolean', - optional: false, nullable: false, - }, - string: { - type: 'string', - optional: true, nullable: false, - }, - default: { - type: 'string', - optional: true, nullable: false, - }, - nullableDefault: { - type: 'string', - default: 'hello', - optional: true, nullable: true, - }, - }, - }, } as const; export const paramDef = { @@ -54,8 +21,9 @@ export const paramDef = { required: ['required'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( ) { super(meta, paramDef, async (ps, me) => { diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts index 4944be9b05..6293c5cb50 100644 --- a/packages/backend/src/server/api/endpoints/username/available.ts +++ b/packages/backend/src/server/api/endpoints/username/available.ts @@ -1,14 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; +import type { UsedUsernamesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { localUsernameSchema } from '@/models/User.js'; +import { localUsernameSchema } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; +import { MetaService } from '@/core/MetaService.js'; export const meta = { tags: ['users'], @@ -35,17 +31,17 @@ export const paramDef = { required: ['username'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.usedUsernamesRepository) private usedUsernamesRepository: UsedUsernamesRepository, + + private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const exist = await this.usersRepository.countBy({ @@ -55,7 +51,8 @@ export default class extends Endpoint { // eslint- const exist2 = await this.usedUsernamesRepository.countBy({ username: ps.username.toLowerCase() }); - const isPreserved = this.serverSettings.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase()); + const meta = await this.metaService.fetch(); + const isPreserved = meta.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase()); return { available: exist === 0 && exist2 === 0 && !isPreserved, diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index e845853017..28cd9f6ce5 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/_.js'; +import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -44,8 +39,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -74,8 +70,8 @@ export default class extends Endpoint { // eslint- switch (ps.sort) { case '+follower': query.orderBy('user.followersCount', 'DESC'); break; case '-follower': query.orderBy('user.followersCount', 'ASC'); break; - case '+createdAt': query.orderBy('user.id', 'DESC'); break; - case '-createdAt': query.orderBy('user.id', 'ASC'); break; + case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; case '+updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'DESC'); break; case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); break; default: query.orderBy('user.id', 'ASC'); break; @@ -84,12 +80,12 @@ export default class extends Endpoint { // eslint- if (me) this.queryService.generateMutedUserQueryForUsers(query, me); if (me) this.queryService.generateBlockQueryForUsers(query, me); - query.limit(ps.limit); - query.offset(ps.offset); + query.take(ps.limit); + query.skip(ps.offset); const users = await query.getMany(); - return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' }); + return await this.userEntityService.packMany(users, me, { detail: true }); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts index bae216e347..2a095d83ea 100644 --- a/packages/backend/src/server/api/endpoints/users/achievements.ts +++ b/packages/backend/src/server/api/endpoints/users/achievements.ts @@ -1,22 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserProfilesRepository } from '@/models/_.js'; +import type { UserProfilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { - requireCredential: false, - - res: { - type: 'array', - items: { - ref: 'Achievement', - }, - }, + requireCredential: true, } as const; export const paramDef = { @@ -27,8 +15,9 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts index 7f7d2ea8cc..c5aa93baaf 100644 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ b/packages/backend/src/server/api/endpoints/users/clips.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { ClipsRepository } from '@/models/_.js'; +import type { ClipsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; @@ -37,8 +32,9 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, @@ -52,7 +48,7 @@ export default class extends Endpoint { // eslint- .andWhere('clip.isPublic = true'); const clips = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.clipEntityService.packMany(clips, me); diff --git a/packages/backend/src/server/api/endpoints/users/featured-notes.ts b/packages/backend/src/server/api/endpoints/users/featured-notes.ts deleted file mode 100644 index 90bd11bc25..0000000000 --- a/packages/backend/src/server/api/endpoints/users/featured-notes.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { DI } from '@/di-symbols.js'; -import { FeaturedService } from '@/core/FeaturedService.js'; -import { CacheService } from '@/core/CacheService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { QueryService } from '@/core/QueryService.js'; - -export const meta = { - tags: ['notes'], - - requireCredential: false, - allowGet: true, - cacheSec: 3600, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - untilId: { type: 'string', format: 'misskey:id' }, - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - private noteEntityService: NoteEntityService, - private featuredService: FeaturedService, - private cacheService: CacheService, - private queryService: QueryService, - ) { - super(meta, paramDef, async (ps, me) => { - const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set(); - - // early return if me is blocked by requesting user - if (userIdsWhoBlockingMe.has(ps.userId)) { - return []; - } - - let noteIds = await this.featuredService.getPerUserNotesRanking(ps.userId, 50); - - noteIds.sort((a, b) => a > b ? -1 : 1); - if (ps.untilId) { - noteIds = noteIds.filter(id => id < ps.untilId!); - } - noteIds = noteIds.slice(0, ps.limit); - - if (noteIds.length === 0) { - return []; - } - - const [ - userIdsWhoMeMuting, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - ]) : [new Set()]; - - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - - const notes = (await query.getMany()).filter(note => { - if (me && isUserRelated(note, userIdsWhoBlockingMe, false)) return false; - if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false; - - return true; - }); - - notes.sort((a, b) => a.id > b.id ? -1 : 1); - - return await this.noteEntityService.packMany(notes, me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/flashs.ts b/packages/backend/src/server/api/endpoints/users/flashs.ts deleted file mode 100644 index e5ea450215..0000000000 --- a/packages/backend/src/server/api/endpoints/users/flashs.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueryService } from '@/core/QueryService.js'; -import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; -import type { FlashsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['users', 'flashs'], - - description: 'Show all flashs this user created.', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Flash', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.flashsRepository) - private flashsRepository: FlashsRepository, - - private flashEntityService: FlashEntityService, - private queryService: QueryService, - ) { - super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.flashsRepository.createQueryBuilder('flash'), ps.sinceId, ps.untilId) - .andWhere('flash.userId = :userId', { userId: ps.userId }) - .andWhere('flash.visibility = \'public\''); - - const flashs = await query - .limit(ps.limit) - .getMany(); - - return await this.flashEntityService.packMany(flashs); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index bb8d4c49e9..97f1310c36 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -1,17 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js'; +import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; -import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -47,43 +41,29 @@ export const meta = { } as const; export const paramDef = { - allOf: [ - { - anyOf: [ - { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], - }, - { - type: 'object', - properties: { - username: { type: 'string' }, - host: { - type: 'string', - nullable: true, - description: 'The local host is represented with `null`.', - }, - }, - required: ['username', 'host'], - }, - ], - }, - { - type: 'object', - properties: { - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - }, + type: 'object', + properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + + userId: { type: 'string', format: 'misskey:id' }, + username: { type: 'string' }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', }, + }, + anyOf: [ + { required: ['userId'] }, + { required: ['username', 'host'] }, ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -97,12 +77,11 @@ export default class extends Endpoint { // eslint- private utilityService: UtilityService, private followingEntityService: FollowingEntityService, private queryService: QueryService, - private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy('userId' in ps + const user = await this.usersRepository.findOneBy(ps.userId != null ? { id: ps.userId } - : { usernameLower: ps.username.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() }); + : { usernameLower: ps.username!.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() }); if (user == null) { throw new ApiError(meta.errors.noSuchUser); @@ -110,25 +89,21 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.followersVisibility !== 'public' && !await this.roleService.isModerator(me)) { - if (profile.followersVisibility === 'private') { - if (me == null || (me.id !== user.id)) { + if (profile.ffVisibility === 'private') { + if (me == null || (me.id !== user.id)) { + throw new ApiError(meta.errors.forbidden); + } + } else if (profile.ffVisibility === 'followers') { + if (me == null) { + throw new ApiError(meta.errors.forbidden); + } else if (me.id !== user.id) { + const following = await this.followingsRepository.findOneBy({ + followeeId: user.id, + followerId: me.id, + }); + if (following == null) { throw new ApiError(meta.errors.forbidden); } - } else if (profile.followersVisibility === 'followers') { - if (me == null) { - throw new ApiError(meta.errors.forbidden); - } else if (me.id !== user.id) { - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: user.id, - followerId: me.id, - }, - }); - if (!isFollowing) { - throw new ApiError(meta.errors.forbidden); - } - } } } @@ -137,7 +112,7 @@ export default class extends Endpoint { // eslint- .innerJoinAndSelect('following.follower', 'follower'); const followings = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.followingEntityService.packMany(followings, me, { populateFollower: true }); diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 1fc87151b2..d406594a2e 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -1,18 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js'; -import { birthdaySchema } from '@/models/User.js'; +import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; -import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -44,54 +37,33 @@ export const meta = { code: 'FORBIDDEN', id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba', }, - - birthdayInvalid: { - message: 'Birthday date format is invalid.', - code: 'BIRTHDAY_DATE_FORMAT_INVALID', - id: 'a2b007b9-4782-4eba-abd3-93b05ed4130d', - }, }, } as const; export const paramDef = { - allOf: [ - { - anyOf: [ - { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], - }, - { - type: 'object', - properties: { - username: { type: 'string' }, - host: { - type: 'string', - nullable: true, - description: 'The local host is represented with `null`.', - }, - }, - required: ['username', 'host'], - }, - ], - }, - { - type: 'object', - properties: { - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - birthday: { ...birthdaySchema, nullable: true }, - }, + type: 'object', + properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + + userId: { type: 'string', format: 'misskey:id' }, + username: { type: 'string' }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', }, + }, + anyOf: [ + { required: ['userId'] }, + { required: ['username', 'host'] }, ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -105,12 +77,11 @@ export default class extends Endpoint { // eslint- private utilityService: UtilityService, private followingEntityService: FollowingEntityService, private queryService: QueryService, - private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy('userId' in ps + const user = await this.usersRepository.findOneBy(ps.userId != null ? { id: ps.userId } - : { usernameLower: ps.username.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() }); + : { usernameLower: ps.username!.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() }); if (user == null) { throw new ApiError(meta.errors.noSuchUser); @@ -118,25 +89,21 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.followingVisibility !== 'public' && !await this.roleService.isModerator(me)) { - if (profile.followingVisibility === 'private') { - if (me == null || (me.id !== user.id)) { + if (profile.ffVisibility === 'private') { + if (me == null || (me.id !== user.id)) { + throw new ApiError(meta.errors.forbidden); + } + } else if (profile.ffVisibility === 'followers') { + if (me == null) { + throw new ApiError(meta.errors.forbidden); + } else if (me.id !== user.id) { + const following = await this.followingsRepository.findOneBy({ + followeeId: user.id, + followerId: me.id, + }); + if (following == null) { throw new ApiError(meta.errors.forbidden); } - } else if (profile.followingVisibility === 'followers') { - if (me == null) { - throw new ApiError(meta.errors.forbidden); - } else if (me.id !== user.id) { - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: user.id, - followerId: me.id, - }, - }); - if (!isFollowing) { - throw new ApiError(meta.errors.forbidden); - } - } } } @@ -144,21 +111,8 @@ export default class extends Endpoint { // eslint- .andWhere('following.followerId = :userId', { userId: user.id }) .innerJoinAndSelect('following.followee', 'followee'); - if (ps.birthday) { - try { - const birthday = ps.birthday.substring(5, 10); - const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile'); - birthdayUserQuery.select('user_profile.userId') - .where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`); - - query.andWhere(`following.followeeId IN (${ birthdayUserQuery.getQuery() })`); - } catch (err) { - throw new ApiError(meta.errors.birthdayInvalid); - } - } - const followings = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.followingEntityService.packMany(followings, me, { populateFollowee: true }); diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts index 553886374c..6e57eee5fb 100644 --- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository } from '@/models/_.js'; +import type { GalleryPostsRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -37,8 +32,9 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -51,7 +47,7 @@ export default class extends Endpoint { // eslint- .andWhere('post.userId = :userId', { userId: ps.userId }); const posts = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.galleryPostEntityService.packMany(posts, me); diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts index 9248a2fa68..09f6acde9c 100644 --- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -1,17 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Not, In, IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { maximum } from '@/misc/prelude/array.js'; -import type { NotesRepository } from '@/models/_.js'; +import type { NotesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; -import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; +import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['users'], @@ -58,9 +53,13 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -118,14 +117,12 @@ export default class extends Endpoint { // eslint- const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); // Extract top replied users - const topRepliedUserIds = repliedUsersSorted.slice(0, ps.limit); + const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit); // Make replies object (includes weights) - const _userMap = await this.userEntityService.packMany(topRepliedUserIds, me, { schema: 'UserDetailed' }) - .then(users => new Map(users.map(u => [u.id, u]))); - const repliesObj = await Promise.all(topRepliedUserIds.map(async (userId) => ({ - user: _userMap.get(userId) ?? await this.userEntityService.pack(userId, me, { schema: 'UserDetailed' }), - weight: repliedUsers[userId] / peak, + const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ + user: await this.userEntityService.pack(user, me, { detail: true }), + weight: repliedUsers[user] / peak, }))); return repliesObj; diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts index 7e44d501ab..8591e4ab96 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository, UserListMembershipsRepository, BlockingsRepository } from '@/models/_.js'; +import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; -import type { MiUserList } from '@/models/UserList.js'; +import type { UserList } from '@/models/entities/UserList.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; @@ -18,7 +13,6 @@ import { UserListService } from '@/core/UserListService.js'; export const meta = { requireCredential: true, prohibitMoved: true, - kind: 'write:account', res: { type: 'object', optional: false, nullable: false, @@ -72,13 +66,13 @@ export const paramDef = { } as const; @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, @@ -90,27 +84,26 @@ export default class extends Endpoint { // eslint- private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const listExist = await this.userListsRepository.exists({ - where: { - id: ps.listId, - isPublic: true, - }, + const list = await this.userListsRepository.findOneBy({ + id: ps.listId, + isPublic: true, }); - if (!listExist) throw new ApiError(meta.errors.noSuchList); + if (list === null) throw new ApiError(meta.errors.noSuchList); const currentCount = await this.userListsRepository.countBy({ userId: me.id, }); - if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) { + if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { throw new ApiError(meta.errors.tooManyUserLists); } - - const userList = await this.userListsRepository.insertOne({ - id: this.idService.gen(), + + const userList = await this.userListsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), userId: me.id, name: ps.name, - } as MiUserList); + } as UserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); - const users = (await this.userListMembershipsRepository.findBy({ + const users = (await this.userListJoiningsRepository.findBy({ userListId: ps.listId, })).map(x => x.userId); @@ -121,30 +114,26 @@ export default class extends Endpoint { // eslint- }); if (currentUser.id !== me.id) { - const blockExist = await this.blockingsRepository.exists({ - where: { - blockerId: currentUser.id, - blockeeId: me.id, - }, + const block = await this.blockingsRepository.findOneBy({ + blockerId: currentUser.id, + blockeeId: me.id, }); - if (blockExist) { + if (block) { throw new ApiError(meta.errors.youHaveBeenBlocked); } } - const exist = await this.userListMembershipsRepository.exists({ - where: { - userListId: userList.id, - userId: currentUser.id, - }, + const exist = await this.userListJoiningsRepository.findOneBy({ + userListId: userList.id, + userId: currentUser.id, }); - + if (exist) { throw new ApiError(meta.errors.alreadyAdded); } try { - await this.userListService.addMember(currentUser, userList, me); + await this.userListService.push(currentUser, userList, me); } catch (err) { if (err instanceof UserListService.TooManyUsersError) { throw new ApiError(meta.errors.tooManyUsers); diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts index 7daf05ba4e..7510889526 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository } from '@/models/_.js'; +import type { UserListsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; -import type { MiUserList } from '@/models/UserList.js'; +import type { UserList } from '@/models/entities/UserList.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -47,8 +42,9 @@ export const paramDef = { required: ['name'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, @@ -61,15 +57,16 @@ export default class extends Endpoint { // eslint- const currentCount = await this.userListsRepository.countBy({ userId: me.id, }); - if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) { + if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { throw new ApiError(meta.errors.tooManyUserLists); } - const userList = await this.userListsRepository.insertOne({ - id: this.idService.gen(), + const userList = await this.userListsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), userId: me.id, name: ps.name, - } as MiUserList); + } as UserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); return await this.userListEntityService.pack(userList); }); diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts index dc0d28a0eb..237cb075ab 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/delete.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository } from '@/models/_.js'; +import type { UserListsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -35,8 +30,9 @@ export const paramDef = { required: ['listId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, diff --git a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts index fd142d5a01..263852fde1 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts @@ -1,18 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserListFavoritesRepository, UserListsRepository } from '@/models/_.js'; +import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { ApiError } from '@/server/api/error.js'; import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, - kind: 'write:account', errors: { noSuchList: { message: 'No such user list.', @@ -47,30 +41,27 @@ export default class extends Endpoint { private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - const userListExist = await this.userListsRepository.exists({ - where: { - id: ps.listId, - isPublic: true, - }, + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + isPublic: true, }); - if (!userListExist) { + if (userList === null) { throw new ApiError(meta.errors.noSuchList); } - const exist = await this.userListFavoritesRepository.exists({ - where: { - userId: me.id, - userListId: ps.listId, - }, + const exist = await this.userListFavoritesRepository.findOneBy({ + userId: me.id, + userListId: ps.listId, }); - if (exist) { + if (exist !== null) { throw new ApiError(meta.errors.alreadyFavorited); } await this.userListFavoritesRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), + createdAt: new Date(), userId: me.id, userListId: ps.listId, }); diff --git a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts deleted file mode 100644 index 6d6e8d34ea..0000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository, UserListFavoritesRepository, UserListMembershipsRepository } from '@/models/_.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; -import { DI } from '@/di-symbols.js'; -import { QueryService } from '@/core/QueryService.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['lists', 'account'], - - requireCredential: false, - - kind: 'read:account', - - errors: { - noSuchList: { - message: 'No such list.', - code: 'NO_SUCH_LIST', - id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686', - }, - }, - - res: { - type: 'array', - items: { - type: 'object', - nullable: false, - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - createdAt: { - type: 'string', - format: 'date-time', - }, - userId: { - type: 'string', - format: 'misskey:id', - }, - user: { - type: 'object', - ref: 'UserLite', - }, - withReplies: { - type: 'boolean', - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - listId: { type: 'string', format: 'misskey:id' }, - forPublic: { type: 'boolean', default: false }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, - required: ['listId'], -} as const; - -@Injectable() // eslint-disable-next-line import/no-default-export -export default class extends Endpoint { - constructor( - @Inject(DI.userListsRepository) - private userListsRepository: UserListsRepository, - - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, - - private userListEntityService: UserListEntityService, - private queryService: QueryService, - ) { - super(meta, paramDef, async (ps, me) => { - // Fetch the list - const userList = await this.userListsRepository.findOneBy(!ps.forPublic && me !== null ? { - id: ps.listId, - userId: me.id, - } : { - id: ps.listId, - isPublic: true, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } - - const query = this.queryService.makePaginationQuery(this.userListMembershipsRepository.createQueryBuilder('membership'), ps.sinceId, ps.untilId) - .andWhere('membership.userListId = :userListId', { userListId: userList.id }) - .innerJoinAndSelect('membership.user', 'user'); - - const memberships = await query - .limit(ps.limit) - .getMany(); - - return this.userListEntityService.packMembershipsMany(memberships); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts index 4241ef1cd0..eab29944b2 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository, UsersRepository } from '@/models/_.js'; +import type { UserListsRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { ApiError } from '@/server/api/error.js'; diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts index 94f06f3bea..d50b70efc2 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -1,14 +1,10 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository } from '@/models/_.js'; +import type { UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import { UserListService } from '@/core/UserListService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -46,14 +42,19 @@ export const paramDef = { required: ['listId', 'userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - private userListService: UserListService, + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + + private userEntityService: UserEntityService, private getterService: GetterService, + private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { // Fetch the list @@ -72,7 +73,10 @@ export default class extends Endpoint { // eslint- throw err; }); - await this.userListService.removeMember(user, userList); + // Pull the user + await this.userListJoiningsRepository.delete({ userListId: userList.id, userId: user.id }); + + this.globalEventService.publishUserListStream(userList.id, 'userRemoved', await this.userEntityService.pack(user)); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index c717b3959c..925037e484 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; -import type { UserListsRepository, UserListMembershipsRepository, BlockingsRepository } from '@/models/_.js'; +import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserListService } from '@/core/UserListService.js'; @@ -70,14 +65,15 @@ export const paramDef = { required: ['listId', 'userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, @@ -104,22 +100,18 @@ export default class extends Endpoint { // eslint- // Check blocking if (user.id !== me.id) { - const blockExist = await this.blockingsRepository.exists({ - where: { - blockerId: user.id, - blockeeId: me.id, - }, + const block = await this.blockingsRepository.findOneBy({ + blockerId: user.id, + blockeeId: me.id, }); - if (blockExist) { + if (block) { throw new ApiError(meta.errors.youHaveBeenBlocked); } } - const exist = await this.userListMembershipsRepository.exists({ - where: { - userListId: userList.id, - userId: user.id, - }, + const exist = await this.userListJoiningsRepository.findOneBy({ + userListId: userList.id, + userId: user.id, }); if (exist) { @@ -127,7 +119,7 @@ export default class extends Endpoint { // eslint- } try { - await this.userListService.addMember(user, userList, me); + await this.userListService.push(user, userList, me); } catch (err) { if (err instanceof UserListService.TooManyUsersError) { throw new ApiError(meta.errors.tooManyUsers); diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index 8756801fe4..8077841c8c 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository, UserListFavoritesRepository } from '@/models/_.js'; +import type { UserListsRepository, UserListFavoritesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -74,12 +69,10 @@ export default class extends Endpoint { userListId: ps.listId, }); if (me !== null) { - additionalProperties.isLiked = await this.userListFavoritesRepository.exists({ - where: { - userId: me.id, - userListId: ps.listId, - }, - }); + additionalProperties.isLiked = (await this.userListFavoritesRepository.findOneBy({ + userId: me.id, + userListId: ps.listId, + }) !== null); } else { additionalProperties.isLiked = false; } diff --git a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts index 3f4bd5af8c..be8e317816 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts @@ -1,17 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserListFavoritesRepository, UserListsRepository } from '@/models/_.js'; +import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js'; import { ApiError } from '@/server/api/error.js'; import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, - kind: 'write:account', errors: { noSuchList: { message: 'No such user list.', @@ -45,14 +39,12 @@ export default class extends Endpoint { private userListFavoritesRepository: UserListFavoritesRepository, ) { super(meta, paramDef, async (ps, me) => { - const userListExist = await this.userListsRepository.exists({ - where: { - id: ps.listId, - isPublic: true, - }, + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + isPublic: true, }); - if (!userListExist) { + if (userList === null) { throw new ApiError(meta.errors.noSuchList); } diff --git a/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts b/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts deleted file mode 100644 index 3948ae1685..0000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository } from '@/models/_.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { DI } from '@/di-symbols.js'; -import { UserListService } from '@/core/UserListService.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['lists', 'users'], - - requireCredential: true, - - prohibitMoved: true, - - kind: 'write:account', - - errors: { - noSuchList: { - message: 'No such list.', - code: 'NO_SUCH_LIST', - id: '7f44670e-ab16-43b8-b4c1-ccd2ee89cc02', - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '588e7f72-c744-4a61-b180-d354e912bda2', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - listId: { type: 'string', format: 'misskey:id' }, - userId: { type: 'string', format: 'misskey:id' }, - withReplies: { type: 'boolean' }, - }, - required: ['listId', 'userId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.userListsRepository) - private userListsRepository: UserListsRepository, - - private userListService: UserListService, - private getterService: GetterService, - ) { - super(meta, paramDef, async (ps, me) => { - // Fetch the list - const userList = await this.userListsRepository.findOneBy({ - id: ps.listId, - userId: me.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } - - // Fetch the user - const user = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw err; - }); - - await this.userListService.updateMembership(user, userList, { - withReplies: ps.withReplies, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index a38f84d7b0..b0a95a2f28 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository } from '@/models/_.js'; +import type { UserListsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -44,8 +39,9 @@ export const paramDef = { required: ['listId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 5832790a61..aaf94734a3 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -1,25 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { MiMeta, NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; -import { CacheService } from '@/core/CacheService.js'; -import { IdService } from '@/core/IdService.js'; -import { QueryService } from '@/core/QueryService.js'; -import { MiLocalUser } from '@/models/User.js'; -import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; -import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; -import { ApiError } from '@/server/api/error.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['users', 'notes'], + description: 'Show all notes that this user created.', + res: { type: 'array', optional: false, nullable: false, @@ -36,18 +29,6 @@ export const meta = { code: 'NO_SUCH_USER', id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b', }, - - bothWithRepliesAndWithFiles: { - message: 'Specifying both withReplies and withFiles is not supported', - code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', - id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222', - }, - - signinRequired: { - message: 'Signin required.', - code: 'SIGNIN_REQUIRED', - id: 'd1588a9e-4b4d-4c07-807f-16f1486577a2', - }, }, } as const; @@ -55,156 +36,93 @@ export const paramDef = { type: 'object', properties: { userId: { type: 'string', format: 'misskey:id' }, - withReplies: { type: 'boolean', default: false }, - withRenotes: { type: 'boolean', default: true }, - withChannelNotes: { type: 'boolean', default: false }, + includeReplies: { type: 'boolean', default: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, - allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default + includeMyRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false }, + fileType: { type: 'array', items: { + type: 'string', + } }, + excludeNsfw: { type: 'boolean', default: false }, }, required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, private queryService: QueryService, - private cacheService: CacheService, - private idService: IdService, - private fanoutTimelineEndpointService: FanoutTimelineEndpointService, + private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { - const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); - const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const isSelf = me && (me.id === ps.userId); + // Lookup user + const user = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); - if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.userId = :userId', { userId: user.id }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); - // early return if me is blocked by requesting user - if (me != null) { - const userIdsWhoBlockingMe = await this.cacheService.userBlockedCache.fetch(me.id); - if (userIdsWhoBlockingMe.has(ps.userId)) { - return []; + this.queryService.generateVisibilityQuery(query, me); + if (me) { + this.queryService.generateMutedUserQuery(query, me, user); + this.queryService.generateBlockedUserQuery(query, me); + } + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + + if (ps.fileType != null) { + query.andWhere('note.fileIds != \'{}\''); + query.andWhere(new Brackets(qb => { + for (const type of ps.fileType!) { + const i = ps.fileType!.indexOf(type); + qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + } + })); + + if (ps.excludeNsfw) { + query.andWhere('note.cw IS NULL'); + query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); } } - if (!this.serverSettings.enableFanoutTimeline) { - const timeline = await this.getFromDb({ - untilId, - sinceId, - limit: ps.limit, - userId: ps.userId, - withChannelNotes: ps.withChannelNotes, - withFiles: ps.withFiles, - withRenotes: ps.withRenotes, - }, me); - - return await this.noteEntityService.packMany(timeline, me); + if (!ps.includeReplies) { + query.andWhere('note.replyId IS NULL'); } - const redisTimelines: FanoutTimelineName[] = [ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`]; + if (ps.includeMyRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.userId != :userId', { userId: user.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } - if (ps.withReplies) redisTimelines.push(`userTimelineWithReplies:${ps.userId}`); - if (ps.withChannelNotes) redisTimelines.push(`userTimelineWithChannel:${ps.userId}`); + //#endregion - const isFollowing = me && Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId); + const timeline = await query.take(ps.limit).getMany(); - const timeline = await this.fanoutTimelineEndpointService.timeline({ - untilId, - sinceId, - limit: ps.limit, - allowPartial: ps.allowPartial, - me, - redisTimelines, - useDbFallback: true, - ignoreAuthorFromMute: true, - ignoreAuthorFromInstanceBlock: true, - ignoreAuthorFromUserSuspension: true, - excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies - excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files - excludePureRenotes: !ps.withRenotes, - noteFilter: note => { - if (note.channel?.isSensitive && !isSelf) return false; - if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false; - if (note.visibility === 'followers' && !isFollowing && !isSelf) return false; - - return true; - }, - dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ - untilId, - sinceId, - limit, - userId: ps.userId, - withChannelNotes: ps.withChannelNotes, - withFiles: ps.withFiles, - withRenotes: ps.withRenotes, - }, me), - }); - - return timeline; + return await this.noteEntityService.packMany(timeline, me); }); } - - private async getFromDb(ps: { - untilId: string | null, - sinceId: string | null, - limit: number, - userId: string, - withChannelNotes: boolean, - withFiles: boolean, - withRenotes: boolean, - }, me: MiLocalUser | null) { - const isSelf = me && (me.id === ps.userId); - - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere('note.userId = :userId', { userId: ps.userId }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('note.channel', 'channel') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - if (ps.withChannelNotes) { - if (!isSelf) query.andWhere(new Brackets(qb => { - qb.orWhere('note.channelId IS NULL'); - qb.orWhere('channel.isSensitive = false'); - })); - } else { - query.andWhere('note.channelId IS NULL'); - } - - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBaseNoteFilteringQuery(query, me, { - excludeAuthor: true, - excludeUserFromMute: ps.userId, - }); - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :userId', { userId: ps.userId }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - return await query.limit(ps.limit).getMany(); - } } diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts index bb7de0e0b5..a105103f16 100644 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ b/packages/backend/src/server/api/endpoints/users/pages.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; -import type { PagesRepository } from '@/models/_.js'; +import type { PagesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -37,8 +32,9 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, @@ -52,7 +48,7 @@ export default class extends Endpoint { // eslint- .andWhere('page.visibility = \'public\''); const pages = await query - .limit(ps.limit) + .take(ps.limit) .getMany(); return await this.pageEntityService.packMany(pages); diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index d6f1ecd8ed..ac401a60ee 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -1,18 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { UserProfilesRepository, NoteReactionsRepository } from '@/models/_.js'; +import type { UserProfilesRepository, NoteReactionsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js'; import { DI } from '@/di-symbols.js'; -import { CacheService } from '@/core/CacheService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -38,11 +29,6 @@ export const meta = { code: 'REACTIONS_NOT_PUBLIC', id: '673a7dd2-6924-1093-e0c0-e68456ceae5c', }, - isRemoteUser: { - message: 'Currently unavailable to display reactions of remote users. See https://github.com/misskey-dev/misskey/issues/12964', - code: 'IS_REMOTE_USER', - id: '6b95fa98-8cf9-2350-e284-f0ffdb54a805', - }, }, } as const; @@ -59,8 +45,9 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @@ -68,59 +55,28 @@ export default class extends Endpoint { // eslint- @Inject(DI.noteReactionsRepository) private noteReactionsRepository: NoteReactionsRepository, - private cacheService: CacheService, - private userEntityService: UserEntityService, private noteReactionEntityService: NoteReactionEntityService, private queryService: QueryService, - private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set(); - const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users - if (!iAmModerator) { - const user = await this.cacheService.findUserById(ps.userId); - if (this.userEntityService.isRemoteUser(user)) { - throw new ApiError(meta.errors.isRemoteUser); - } + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); - const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); - if ((me == null || me.id !== ps.userId) && !profile.publicReactions) { - throw new ApiError(meta.errors.reactionsNotPublic); - } - - // early return if me is blocked by requesting user - if (userIdsWhoBlockingMe.has(ps.userId)) { - return []; - } + if ((me == null || me.id !== ps.userId) && !profile.publicReactions) { + throw new ApiError(meta.errors.reactionsNotPublic); } - const userIdsWhoMeMuting = me ? await this.cacheService.userMutingsCache.fetch(me.id) : new Set(); - const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('reaction.userId = :userId', { userId: ps.userId }) - .leftJoinAndSelect('reaction.note', 'note') - .leftJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); + .leftJoinAndSelect('reaction.note', 'note'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - const reactions = (await query - .limit(ps.limit) - .getMany()).filter(reaction => { - if (reaction.note?.userId === ps.userId) return true; // we can see reactions to note of requesting user - if (me && isUserRelated(reaction.note, userIdsWhoBlockingMe)) return false; - if (me && isUserRelated(reaction.note, userIdsWhoMeMuting)) return false; + const reactions = await query + .take(ps.limit) + .getMany(); - return true; - }); - - return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true }); + return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me, { withNote: true }))); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index 769a72d7a1..6fcc04e2c5 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -1,11 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, FollowingsRepository } from '@/models/_.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -40,8 +35,9 @@ export const paramDef = { required: [], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -63,8 +59,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateMutedUserQueryForUsers(query, me); this.queryService.generateBlockQueryForUsers(query, me); - this.queryService.generateBlockedUserQueryForNotes(query, me); - this.queryService.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' }); + this.queryService.generateBlockedUserQuery(query, me); const followingQuery = this.followingsRepository.createQueryBuilder('following') .select('following.followeeId') @@ -75,9 +70,9 @@ export default class extends Endpoint { // eslint- query.setParameters(followingQuery.getParameters()); - const users = await query.limit(ps.limit).offset(ps.offset).getMany(); + const users = await query.take(ps.limit).skip(ps.offset).getMany(); - return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' }); + return await this.userEntityService.packMany(users, me, { detail: true }); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index f146095cf1..3267c18846 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -1,17 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['users'], requireCredential: true, - kind: 'read:account', description: 'Show the different kinds of relations between the authenticated user and the specified user(s).', @@ -114,7 +110,7 @@ export const paramDef = { type: 'object', properties: { userId: { - oneOf: [ + anyOf: [ { type: 'string', format: 'misskey:id' }, { type: 'array', @@ -126,15 +122,21 @@ export const paramDef = { required: ['userId'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { - return Array.isArray(ps.userId) - ? await this.userEntityService.getRelations(me.id, ps.userId).then(it => [...it.values()]) - : await this.userEntityService.getRelation(me.id, ps.userId).then(it => [it]); + const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId]; + + const relations = await Promise.all(ids.map(id => this.userEntityService.getRelation(me.id, id))); + + return Array.isArray(ps.userId) ? relations : relations[0]; }); } } diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 5ff6de37d2..be361e02c4 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -1,20 +1,20 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import sanitizeHtml from 'sanitize-html'; +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository, AbuseUserReportsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { EmailService } from '@/core/EmailService.js'; +import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { RoleService } from '@/core/RoleService.js'; -import { AbuseReportService } from '@/core/AbuseReportService.js'; import { ApiError } from '../../error.js'; export const meta = { tags: ['users'], requireCredential: true, - kind: 'write:report-abuse', description: 'File a report.', @@ -48,35 +48,68 @@ export const paramDef = { required: ['userId', 'comment'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, + + private idService: IdService, + private metaService: MetaService, + private emailService: EmailService, private getterService: GetterService, private roleService: RoleService, - private abuseReportService: AbuseReportService, + private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { // Lookup user - const targetUser = await this.getterService.getUser(ps.userId).catch(err => { + const user = await this.getterService.getUser(ps.userId).catch(err => { if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); throw err; }); - if (targetUser.id === me.id) { + if (user.id === me.id) { throw new ApiError(meta.errors.cannotReportYourself); } - if (await this.roleService.isAdministrator(targetUser)) { + if (await this.roleService.isAdministrator(user)) { throw new ApiError(meta.errors.cannotReportAdmin); } - await this.abuseReportService.report([{ - targetUserId: targetUser.id, - targetUserHost: targetUser.host, + const report = await this.abuseUserReportsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + targetUserId: user.id, + targetUserHost: user.host, reporterId: me.id, reporterHost: null, comment: ps.comment, - }]); + }).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0])); + + // Publish event to moderators + setImmediate(async () => { + const moderators = await this.roleService.getModerators(); + + for (const moderator of moderators) { + this.globalEventService.publishAdminStream(moderator.id, 'newAbuseUserReport', { + id: report.id, + targetUserId: report.targetUserId, + reporterId: report.reporterId, + comment: report.comment, + }); + } + + const meta = await this.metaService.fetch(); + if (meta.email) { + this.emailService.sendEmail(meta.email, 'New abuse report', + sanitizeHtml(ps.comment), + sanitizeHtml(ps.comment)); + } + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index d1d6354d53..b001159ee8 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -1,11 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import type { Config } from '@/config.js'; +import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserSearchService } from '@/core/UserSearchService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['users'], @@ -26,48 +27,106 @@ export const meta = { } as const; export const paramDef = { - allOf: [ - { - anyOf: [ - { - type: 'object', - properties: { - username: { type: 'string', nullable: true }, - }, - required: ['username'], - }, - { - type: 'object', - properties: { - host: { type: 'string', nullable: true }, - }, - required: ['host'], - }, - ], - }, - { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - detail: { type: 'boolean', default: true }, - }, - }, + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + detail: { type: 'boolean', default: true }, + + username: { type: 'string', nullable: true }, + host: { type: 'string', nullable: true }, + }, + anyOf: [ + { required: ['username'] }, + { required: ['host'] }, ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - private userSearchService: UserSearchService, + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, ) { - super(meta, paramDef, (ps, me) => { - return this.userSearchService.searchByUsernameAndHost({ - username: 'username' in ps ? ps.username : undefined, - host: 'host' in ps ? ps.host : undefined, - }, { - limit: ps.limit, - detail: ps.detail, - }, me); + super(meta, paramDef, async (ps, me) => { + const setUsernameAndHostQuery = (query = this.usersRepository.createQueryBuilder('user')) => { + if (ps.username) { + query.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }); + } + + if (ps.host) { + if (ps.host === this.config.hostname || ps.host === '.') { + query.andWhere('user.host IS NULL'); + } else { + query.andWhere('user.host LIKE :host', { + host: sqlLikeEscape(ps.host.toLowerCase()) + '%', + }); + } + } + + return query; + }; + + const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 + + let users: User[] = []; + + if (me) { + const followingQuery = this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); + + const query = setUsernameAndHostQuery() + .andWhere(`user.id IN (${ followingQuery.getQuery() })`) + .andWhere('user.id != :meId', { meId: me.id }) + .andWhere('user.isSuspended = FALSE') + .andWhere(new Brackets(qb => { qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })); + + query.setParameters(followingQuery.getParameters()); + + users = await query + .orderBy('user.usernameLower', 'ASC') + .take(ps.limit) + .getMany(); + + if (users.length < ps.limit) { + const otherQuery = setUsernameAndHostQuery() + .andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`) + .andWhere('user.isSuspended = FALSE') + .andWhere('user.updatedAt IS NOT NULL'); + + otherQuery.setParameters(followingQuery.getParameters()); + + const otherUsers = await otherQuery + .orderBy('user.updatedAt', 'DESC') + .take(ps.limit - users.length) + .getMany(); + + users = users.concat(otherUsers); + } + } else { + const query = setUsernameAndHostQuery() + .andWhere('user.isSuspended = FALSE') + .andWhere('user.updatedAt IS NOT NULL'); + + users = await query + .orderBy('user.updatedAt', 'DESC') + .take(ps.limit - users.length) + .getMany(); + } + + return await this.userEntityService.packMany(users, me, { detail: !!ps.detail }); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index 5d36847e03..d7a60f0437 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -1,13 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - +import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; -import { UserSearchService } from '@/core/UserSearchService.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['users'], @@ -39,20 +37,103 @@ export const paramDef = { required: ['query'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + private userEntityService: UserEntityService, - private userSearchService: UserSearchService, ) { super(meta, paramDef, async (ps, me) => { - const users = await this.userSearchService.search(ps.query.trim(), me?.id ?? null, { - offset: ps.offset, - limit: ps.limit, - origin: ps.origin, - }); + const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 - return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' }); + const isUsername = ps.query.startsWith('@'); + + let users: User[] = []; + + if (isUsername) { + const usernameQuery = this.usersRepository.createQueryBuilder('user') + .where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' }) + .andWhere(new Brackets(qb => { qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })) + .andWhere('user.isSuspended = FALSE'); + + if (ps.origin === 'local') { + usernameQuery.andWhere('user.host IS NULL'); + } else if (ps.origin === 'remote') { + usernameQuery.andWhere('user.host IS NOT NULL'); + } + + users = await usernameQuery + .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') + .take(ps.limit) + .skip(ps.offset) + .getMany(); + } else { + const nameQuery = this.usersRepository.createQueryBuilder('user') + .where(new Brackets(qb => { + qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); + + // Also search username if it qualifies as username + if (this.userEntityService.validateLocalUsername(ps.query)) { + qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' }); + } + })) + .andWhere(new Brackets(qb => { qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })) + .andWhere('user.isSuspended = FALSE'); + + if (ps.origin === 'local') { + nameQuery.andWhere('user.host IS NULL'); + } else if (ps.origin === 'remote') { + nameQuery.andWhere('user.host IS NOT NULL'); + } + + users = await nameQuery + .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') + .take(ps.limit) + .skip(ps.offset) + .getMany(); + + if (users.length < ps.limit) { + const profQuery = this.userProfilesRepository.createQueryBuilder('prof') + .select('prof.userId') + .where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); + + if (ps.origin === 'local') { + profQuery.andWhere('prof.userHost IS NULL'); + } else if (ps.origin === 'remote') { + profQuery.andWhere('prof.userHost IS NOT NULL'); + } + + const query = this.usersRepository.createQueryBuilder('user') + .where(`user.id IN (${ profQuery.getQuery() })`) + .andWhere(new Brackets(qb => { qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })) + .andWhere('user.isSuspended = FALSE') + .setParameters(profQuery.getParameters()); + + users = users.concat(await query + .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') + .take(ps.limit) + .skip(ps.offset) + .getMany(), + ); + } + } + + return await this.userEntityService.packMany(users, me, { detail: ps.detail }); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/show.test.ts b/packages/backend/src/server/api/endpoints/users/show.test.ts deleted file mode 100644 index 068ffd8bc9..0000000000 --- a/packages/backend/src/server/api/endpoints/users/show.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -process.env.NODE_ENV = 'test'; - -import { getValidator } from '../../../../../test/prelude/get-api-validator.js'; -import { paramDef } from './show.js'; - -const VALID = true; -const INVALID = false; - -describe('api:users/show', () => { - describe('validation', () => { - const v = getValidator(paramDef); - - test('Reject empty', () => expect(v({})).toBe(INVALID)); - test('Reject host only', () => expect(v({ host: 'misskey.test' })).toBe(INVALID)); - test('Accept userId only', () => expect(v({ userId: '1' })).toBe(VALID)); - test('Accept username and host', () => expect(v({ username: 'alice', host: 'misskey.test' })).toBe(VALID)); - }); -}); diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 5ff3a63d6a..ba432c273b 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { In, IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { MiMeta, UsersRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; +import type { UsersRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; @@ -59,53 +54,30 @@ export const meta = { } as const; export const paramDef = { - allOf: [ - { - anyOf: [ - { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], - }, - { - type: 'object', - properties: { - userIds: { type: 'array', uniqueItems: true, items: { - type: 'string', format: 'misskey:id', - } }, - }, - required: ['userIds'], - }, - { - type: 'object', - properties: { - username: { type: 'string' }, - }, - required: ['username'], - }, - ], - }, - { - type: 'object', - properties: { - host: { - type: 'string', - nullable: true, - description: 'The local host is represented with `null`.', - }, - }, + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + userIds: { type: 'array', uniqueItems: true, items: { + type: 'string', format: 'misskey:id', + } }, + username: { type: 'string' }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', }, + }, + anyOf: [ + { required: ['userId'] }, + { required: ['userIds'] }, + { required: ['username'] }, ], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -116,19 +88,11 @@ export default class extends Endpoint { // eslint- private apiLoggerService: ApiLoggerService, ) { super(meta, paramDef, async (ps, me, _1, _2, _3, ip) => { - // ログイン時にusers/showできなくなってしまう - //if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) { - // throw new ApiError(meta.errors.noSuchUser); - //} - let user; const isModerator = await this.roleService.isModerator(me); - if ('username' in ps) { - ps.username = ps.username.trim(); - } - if ('userIds' in ps) { + if (ps.userIds) { if (ps.userIds.length === 0) { return []; } @@ -141,29 +105,23 @@ export default class extends Endpoint { // eslint- }); // リクエストされた通りに並べ替え - // 順番は保持されるけど数は減ってる可能性がある - const _users: MiUser[] = []; + const _users: User[] = []; for (const id of ps.userIds) { - const user = users.find(x => x.id === id); - if (user != null) _users.push(user); + _users.push(users.find(x => x.id === id)!); } - const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' }) - .then(users => new Map(users.map(u => [u.id, u]))); - return _users.map(u => _userMap.get(u.id)!); + return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, { + detail: true, + }))); } else { // Lookup user - if (typeof ps.host === 'string' && 'username' in ps) { - if (this.serverSettings.ugcVisibilityForVisitor === 'local' && me == null) { - throw new ApiError(meta.errors.noSuchUser); - } - + if (typeof ps.host === 'string' && typeof ps.username === 'string') { user = await this.remoteUserResolveService.resolveUser(ps.username, ps.host).catch(err => { this.apiLoggerService.logger.warn(`failed to resolve remote user: ${err}`); throw new ApiError(meta.errors.failedToResolveRemoteUser); }); } else { - const q: FindOptionsWhere = 'userId' in ps + const q: FindOptionsWhere = ps.userId != null ? { id: ps.userId } : { usernameLower: ps.username!.toLowerCase(), host: IsNull() }; @@ -174,10 +132,6 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchUser); } - if (this.serverSettings.ugcVisibilityForVisitor === 'local' && user.host != null && me == null) { - throw new ApiError(meta.errors.noSuchUser); - } - if (user.host == null) { if (me == null && ip != null) { this.perUserPvChart.commitByVisitor(user, ip); @@ -187,7 +141,7 @@ export default class extends Endpoint { // eslint- } return await this.userEntityService.pack(user, me, { - schema: 'UserDetailed', + detail: true, }); } }); diff --git a/packages/backend/src/server/api/endpoints/users/update-memo.ts b/packages/backend/src/server/api/endpoints/users/update-memo.ts index 5a10de0c40..ca7756ef75 100644 --- a/packages/backend/src/server/api/endpoints/users/update-memo.ts +++ b/packages/backend/src/server/api/endpoints/users/update-memo.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; -import type { UserMemoRepository } from '@/models/_.js'; +import type { UserMemoRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; @@ -40,8 +35,9 @@ export const paramDef = { required: ['userId', 'memo'], } as const; +// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export +export default class extends Endpoint { constructor( @Inject(DI.userMemosRepository) private userMemosRepository: UserMemoRepository, @@ -72,7 +68,7 @@ export default class extends Endpoint { // eslint- if (!previousMemo) { await this.userMemosRepository.insert({ - id: this.idService.gen(), + id: this.idService.genId(), userId: me.id, targetUserId: target.id, memo: ps.memo, diff --git a/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts deleted file mode 100644 index 7139715293..0000000000 --- a/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import { CustomEmojiService, fetchEmojisHostTypes, fetchEmojisSortKeys } from '@/core/CustomEmojiService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requiredRolePolicy: 'canManageCustomEmojis', - kind: 'read:admin:emoji', - - res: { - type: 'object', - properties: { - emojis: { - type: 'array', - items: { - type: 'object', - ref: 'EmojiDetailedAdmin', - }, - }, - count: { type: 'integer' }, - allCount: { type: 'integer' }, - allPages: { type: 'integer' }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - query: { - type: 'object', - nullable: true, - properties: { - updatedAtFrom: { type: 'string' }, - updatedAtTo: { type: 'string' }, - name: { type: 'string' }, - host: { type: 'string' }, - uri: { type: 'string' }, - publicUrl: { type: 'string' }, - originalUrl: { type: 'string' }, - type: { type: 'string' }, - aliases: { type: 'string' }, - category: { type: 'string' }, - license: { type: 'string' }, - isSensitive: { type: 'boolean' }, - localOnly: { type: 'boolean' }, - hostType: { - type: 'string', - enum: fetchEmojisHostTypes, - default: 'all', - }, - roleIds: { - type: 'array', - items: { type: 'string', format: 'misskey:id' }, - }, - }, - }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - page: { type: 'integer' }, - sortKeys: { - type: 'array', - default: ['-id'], - items: { - type: 'string', - enum: fetchEmojisSortKeys, - }, - }, - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private customEmojiService: CustomEmojiService, - private emojiEntityService: EmojiEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const q = ps.query; - const result = await this.customEmojiService.fetchEmojis( - { - query: { - updatedAtFrom: q?.updatedAtFrom, - updatedAtTo: q?.updatedAtTo, - name: q?.name, - host: q?.host, - uri: q?.uri, - publicUrl: q?.publicUrl, - type: q?.type, - aliases: q?.aliases, - category: q?.category, - license: q?.license, - isSensitive: q?.isSensitive, - localOnly: q?.localOnly, - hostType: q?.hostType, - roleIds: q?.roleIds, - }, - sinceId: ps.sinceId, - untilId: ps.untilId, - }, - { - limit: ps.limit, - page: ps.page, - sortKeys: ps.sortKeys, - }, - ); - - return { - emojis: await this.emojiEntityService.packDetailedAdminMany(result.emojis), - count: result.count, - allCount: result.allCount, - allPages: result.allPages, - }; - }); - } -} diff --git a/packages/backend/src/server/api/error.ts b/packages/backend/src/server/api/error.ts index 2f8322a568..34f4521606 100644 --- a/packages/backend/src/server/api/error.ts +++ b/packages/backend/src/server/api/error.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - type E = { message: string, code: string, id: string, kind?: 'client' | 'server' | 'permission', httpStatusCode?: number }; export class ApiError extends Error { diff --git a/packages/backend/src/server/api/openapi/OpenApiServerService.ts b/packages/backend/src/server/api/openapi/OpenApiServerService.ts index f124aa9f39..e804ba276c 100644 --- a/packages/backend/src/server/api/openapi/OpenApiServerService.ts +++ b/packages/backend/src/server/api/openapi/OpenApiServerService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { fileURLToPath } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import type { Config } from '@/config.js'; @@ -25,7 +20,7 @@ export class OpenApiServerService { public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.get('/api-doc', async (_request, reply) => { reply.header('Cache-Control', 'public, max-age=86400'); - return await reply.sendFile('/api-doc.html', staticAssets); + return await reply.sendFile('/redoc.html', staticAssets); }); fastify.get('/api.json', (_request, reply) => { reply.header('Cache-Control', 'public, max-age=600'); diff --git a/packages/backend/src/server/api/openapi/errors.ts b/packages/backend/src/server/api/openapi/errors.ts index 7c50122f90..d7f791c6da 100644 --- a/packages/backend/src/server/api/openapi/errors.ts +++ b/packages/backend/src/server/api/openapi/errors.ts @@ -1,7 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ export const errors = { '400': { diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index e1dead07cf..fa62480c02 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -1,20 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import type { Config } from '@/config.js'; -import endpoints, { IEndpoint } from '../endpoints.js'; +import endpoints from '../endpoints.js'; import { errors as basicErrors } from './errors.js'; -import { getSchemas, convertSchemaToOpenApiSchema } from './schemas.js'; +import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; -export function genOpenapiSpec(config: Config, includeSelfRef = false) { +export function genOpenapiSpec(config: Config) { const spec = { - openapi: '3.1.0', + openapi: '3.0.0', info: { version: config.version, title: 'Misskey API', + 'x-logo': { url: '/static-assets/api-doc.png' }, }, externalDocs: { @@ -29,20 +25,19 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { paths: {} as any, components: { - schemas: getSchemas(includeSelfRef), + schemas: schemas, securitySchemes: { - bearerAuth: { - type: 'http', - scheme: 'bearer', + ApiKeyAuth: { + type: 'apiKey', + in: 'body', + name: 'i', }, }, }, }; - // 書き換えたりするのでディープコピーしておく。そのまま編集するとメモリ上の値が汚れて次回以降の出力に影響する - const copiedEndpoints = JSON.parse(JSON.stringify(endpoints)) as IEndpoint[]; - for (const endpoint of copiedEndpoints) { + for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { const errors = {} as any; if (endpoint.meta.errors) { @@ -55,14 +50,9 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { } } - const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res, 'res', includeSelfRef) : {}; + const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n'; - - if (endpoint.meta.secure) { - desc += '**Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.\n'; - } - desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`; if (endpoint.meta.kind) { const kind = endpoint.meta.kind; @@ -70,7 +60,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { } const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json'; - const schema = { ...convertSchemaToOpenApiSchema(endpoint.params, 'param', false) }; + const schema = { ...endpoint.params }; if (endpoint.meta.requireFile) { schema.properties = { @@ -84,16 +74,8 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { schema.required = [...schema.required ?? [], 'file']; } - if (schema.required && schema.required.length <= 0) { - // 空配列は許可されない - schema.required = undefined; - } - - const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1) - || ['allOf', 'oneOf', 'anyOf'].some(o => (Array.isArray(schema[o]) && schema[o].length >= 0)); - const info = { - operationId: endpoint.name.replaceAll('/', '___'), // NOTE: スラッシュは使えない + operationId: endpoint.name, summary: endpoint.name, description: desc, externalDocs: { @@ -105,19 +87,17 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { } : {}), ...(endpoint.meta.requireCredential ? { security: [{ - bearerAuth: [], + ApiKeyAuth: [], }], } : {}), - ...(hasBody ? { - requestBody: { - required: true, - content: { - [requestType]: { - schema, - }, + requestBody: { + required: true, + content: { + [requestType]: { + schema, }, }, - } : {}), + }, responses: { ...(endpoint.meta.res ? { '200': { @@ -133,11 +113,6 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { description: 'OK (without any results)', }, }), - ...(endpoint.meta.res?.optional === true || endpoint.meta.res?.nullable === true ? { - '204': { - description: 'OK (without any results)', - }, - } : {}), '400': { description: 'Client error', content: { @@ -184,7 +159,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { }, ...(endpoint.meta.limit ? { '429': { - description: 'Too many requests', + description: 'To many requests', content: { 'application/json': { schema: { @@ -210,16 +185,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { }; spec.paths['/' + endpoint.name] = { - ...(endpoint.meta.allowGet ? { - get: { - ...info, - operationId: 'get___' + info.operationId, - }, - } : {}), - post: { - ...info, - operationId: 'post___' + info.operationId, - }, + post: info, }; } diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts index 1cdcbebd1a..0cef361caf 100644 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ b/packages/backend/src/server/api/openapi/schemas.ts @@ -1,91 +1,61 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { deepClone } from '@/misc/clone.js'; import type { Schema } from '@/misc/json-schema.js'; import { refs } from '@/misc/json-schema.js'; -export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res', includeSelfRef: boolean): any { - // optional, nullable, refはスキーマ定義に含まれないので分離しておく - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { optional, nullable, ref, selfRef, ..._res }: any = schema; - const res = deepClone(_res); +export function convertSchemaToOpenApiSchema(schema: Schema) { + const res: any = schema; if (schema.type === 'object' && schema.properties) { - if (type === 'res') { - const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); - if (required.length > 0) { - // 空配列は許可されない - res.required = required; - } - } + res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); for (const k of Object.keys(schema.properties)) { - res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k], type, includeSelfRef); + res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]); } } if (schema.type === 'array' && schema.items) { - res.items = convertSchemaToOpenApiSchema(schema.items, type, includeSelfRef); + res.items = convertSchemaToOpenApiSchema(schema.items); } - for (const o of ['anyOf', 'oneOf', 'allOf'] as const) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (o in schema) res[o] = schema[o]!.map(schema => convertSchemaToOpenApiSchema(schema, type, includeSelfRef)); - } + if (schema.anyOf) res.anyOf = schema.anyOf.map(convertSchemaToOpenApiSchema); + if (schema.oneOf) res.oneOf = schema.oneOf.map(convertSchemaToOpenApiSchema); + if (schema.allOf) res.allOf = schema.allOf.map(convertSchemaToOpenApiSchema); - if (type === 'res' && schema.ref && (!schema.selfRef || includeSelfRef)) { - const $ref = `#/components/schemas/${schema.ref}`; - if (schema.nullable) { - res.oneOf = [{ $ref }, { type: 'null' }]; - } else { - res.$ref = $ref; - } - delete res.type; - } else if (schema.nullable) { - if (Array.isArray(schema.type) && !schema.type.includes('null')) { - res.type.push('null'); - } else if (typeof schema.type === 'string') { - res.type = [res.type, 'null']; - } + if (schema.ref) { + res.$ref = `#/components/schemas/${schema.ref}`; } return res; } -export function getSchemas(includeSelfRef: boolean) { - return { - Error: { - type: 'object', - properties: { - error: { - type: 'object', - description: 'An error object.', - properties: { - code: { - type: 'string', - description: 'An error code. Unique within the endpoint.', - }, - message: { - type: 'string', - description: 'An error message.', - }, - id: { - type: 'string', - format: 'uuid', - description: 'An error ID. This ID is static.', - }, +export const schemas = { + Error: { + type: 'object', + properties: { + error: { + type: 'object', + description: 'An error object.', + properties: { + code: { + type: 'string', + description: 'An error code. Unique within the endpoint.', + }, + message: { + type: 'string', + description: 'An error message.', + }, + id: { + type: 'string', + format: 'uuid', + description: 'An error ID. This ID is static.', }, - required: ['code', 'id', 'message'], }, + required: ['code', 'id', 'message'], }, - required: ['error'], }, + required: ['error'], + }, - ...Object.fromEntries( - Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema, 'res', includeSelfRef)]), - ), - }; -} + ...Object.fromEntries( + Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema)]), + ), +}; diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index c0ef589dea..c77ba66028 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; import { HybridTimelineChannelService } from './channels/hybrid-timeline.js'; @@ -19,11 +14,6 @@ import { AntennaChannelService } from './channels/antenna.js'; import { DriveChannelService } from './channels/drive.js'; import { HashtagChannelService } from './channels/hashtag.js'; import { RoleTimelineChannelService } from './channels/role-timeline.js'; -import { ChatUserChannelService } from './channels/chat-user.js'; -import { ChatRoomChannelService } from './channels/chat-room.js'; -import { ReversiChannelService } from './channels/reversi.js'; -import { ReversiGameChannelService } from './channels/reversi-game.js'; -import { type MiChannelService } from './channel.js'; @Injectable() export class ChannelsService { @@ -42,15 +32,11 @@ export class ChannelsService { private serverStatsChannelService: ServerStatsChannelService, private queueStatsChannelService: QueueStatsChannelService, private adminChannelService: AdminChannelService, - private chatUserChannelService: ChatUserChannelService, - private chatRoomChannelService: ChatRoomChannelService, - private reversiChannelService: ReversiChannelService, - private reversiGameChannelService: ReversiGameChannelService, ) { } @bindThis - public getChannelService(name: string): MiChannelService { + public getChannelService(name: string) { switch (name) { case 'main': return this.mainChannelService; case 'homeTimeline': return this.homeTimelineChannelService; @@ -66,11 +52,7 @@ export class ChannelsService { case 'serverStats': return this.serverStatsChannelService; case 'queueStats': return this.queueStatsChannelService; case 'admin': return this.adminChannelService; - case 'chatUser': return this.chatUserChannelService; - case 'chatRoom': return this.chatRoomChannelService; - case 'reversi': return this.reversiChannelService; - case 'reversiGame': return this.reversiGameChannelService; - + default: throw new Error(`no such channel: ${name}`); } diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 686aea423c..94b92e02ef 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -1,27 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { bindThis } from '@/decorators.js'; -import { isInstanceMuted } from '@/misc/is-instance-muted.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; -import type { Packed } from '@/misc/json-schema.js'; -import type { JsonObject, JsonValue } from '@/misc/json-value.js'; -import type Connection from './Connection.js'; +import type Connection from './index.js'; /** * Stream channel */ -// eslint-disable-next-line import/no-default-export export default abstract class Channel { protected connection: Connection; public id: string; public abstract readonly chName: string; public static readonly shouldShare: boolean; public static readonly requireCredential: boolean; - public static readonly kind?: string | null; protected get user() { return this.connection.user; @@ -47,10 +35,6 @@ export default abstract class Channel { return this.connection.userIdsWhoBlockingMe; } - protected get userMutedInstances() { - return this.connection.userMutedInstances; - } - protected get followingChannels() { return this.connection.followingChannels; } @@ -59,35 +43,15 @@ export default abstract class Channel { return this.connection.subscriber; } - /* - * ミュートとブロックされてるを処理する - */ - protected isNoteMutedOrBlocked(note: Packed<'Note'>): boolean { - // 流れてきたNoteがインスタンスミュートしたインスタンスが関わる - if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return true; - - // 流れてきたNoteがミュートしているユーザーが関わる - if (isUserRelated(note, this.userIdsWhoMeMuting)) return true; - // 流れてきたNoteがブロックされているユーザーが関わる - if (isUserRelated(note, this.userIdsWhoBlockingMe)) return true; - - // 流れてきたNoteがリノートをミュートしてるユーザが行ったもの - if (isRenotePacked(note) && !isQuotePacked(note) && this.userIdsWhoMeMutingRenotes.has(note.user.id)) return true; - - return false; - } - constructor(id: string, connection: Connection) { this.id = id; this.connection = connection; } - public send(payload: { type: string, body: JsonValue }): void; - public send(type: string, payload: JsonValue): void; @bindThis - public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) { - const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string); - const body = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).body : payload; + public send(typeOrPayload: any, payload?: any) { + const type = payload === undefined ? typeOrPayload.type : typeOrPayload; + const body = payload === undefined ? typeOrPayload.body : payload; this.connection.sendMessageToWs('channel', { id: this.id, @@ -96,16 +60,7 @@ export default abstract class Channel { }); } - public abstract init(params: JsonObject): void; - + public abstract init(params: any): void; public dispose?(): void; - - public onMessage?(type: string, body: JsonValue): void; + public onMessage?(type: string, body: any): void; } - -export type MiChannelService = { - shouldShare: boolean; - requireCredential: T; - kind: T extends true ? string : string | null | undefined; - create: (id: string, connection: Connection) => Channel; -}; diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts index 355d5dba21..157fcd6aa3 100644 --- a/packages/backend/src/server/api/stream/channels/admin.ts +++ b/packages/backend/src/server/api/stream/channels/admin.ts @@ -1,21 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; +import Channel from '../channel.js'; class AdminChannel extends Channel { public readonly chName = 'admin'; public static shouldShare = true; - public static requireCredential = true as const; - public static kind = 'read:admin:stream'; + public static requireCredential = true; @bindThis - public async init(params: JsonObject) { + public async init(params: any) { // Subscribe admin stream this.subscriber.on(`adminStream:${this.user!.id}`, data => { this.send(data); @@ -24,10 +17,9 @@ class AdminChannel extends Channel { } @Injectable() -export class AdminChannelService implements MiChannelService { +export class AdminChannelService { public readonly shouldShare = AdminChannel.shouldShare; public readonly requireCredential = AdminChannel.requireCredential; - public readonly kind = AdminChannel.kind; constructor( ) { diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index 53dc7f18b6..d48dea7258 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -1,20 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; +import Channel from '../channel.js'; +import type { StreamMessages } from '../types.js'; class AntennaChannel extends Channel { public readonly chName = 'antenna'; public static shouldShare = false; - public static requireCredential = true as const; - public static kind = 'read:account'; + public static requireCredential = false; private antennaId: string; constructor( @@ -28,20 +22,24 @@ class AntennaChannel extends Channel { } @bindThis - public async init(params: JsonObject) { - if (typeof params.antennaId !== 'string') return; - this.antennaId = params.antennaId; + public async init(params: any) { + this.antennaId = params.antennaId as string; // Subscribe stream this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent); } @bindThis - private async onEvent(data: GlobalEvents['antenna']['payload']) { + private async onEvent(data: StreamMessages['antenna']['payload']) { if (data.type === 'note') { const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true }); - if (this.isNoteMutedOrBlocked(note)) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; + + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; this.connection.cacheNote(note); @@ -59,10 +57,9 @@ class AntennaChannel extends Channel { } @Injectable() -export class AntennaChannelService implements MiChannelService { +export class AntennaChannelService { public readonly shouldShare = AntennaChannel.shouldShare; public readonly requireCredential = AntennaChannel.requireCredential; - public readonly kind = AntennaChannel.kind; constructor( private noteEntityService: NoteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 7108e0cd6e..9e5b40997b 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -1,20 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; +import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; +import Channel from '../channel.js'; class ChannelChannel extends Channel { public readonly chName = 'channel'; public static shouldShare = false; - public static requireCredential = false as const; + public static requireCredential = false; private channelId: string; constructor( @@ -28,9 +22,8 @@ class ChannelChannel extends Channel { } @bindThis - public async init(params: JsonObject) { - if (typeof params.channelId !== 'string') return; - this.channelId = params.channelId; + public async init(params: any) { + this.channelId = params.channelId as string; // Subscribe stream this.subscriber.on('notesStream', this.onNote); @@ -40,14 +33,25 @@ class ChannelChannel extends Channel { private async onNote(note: Packed<'Note'>) { if (note.channelId !== this.channelId) return; - if (this.isNoteMutedOrBlocked(note)) return; - - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { - if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); - note.renote.myReaction = myRenoteReaction; - } + // リプライなら再pack + if (note.replyId != null) { + note.reply = await this.noteEntityService.pack(note.replyId, this.user, { + detail: true, + }); } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { + detail: true, + }); + } + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; + + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; this.connection.cacheNote(note); @@ -62,10 +66,9 @@ class ChannelChannel extends Channel { } @Injectable() -export class ChannelChannelService implements MiChannelService { +export class ChannelChannelService { public readonly shouldShare = ChannelChannel.shouldShare; public readonly requireCredential = ChannelChannel.requireCredential; - public readonly kind = ChannelChannel.kind; constructor( private noteEntityService: NoteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/chat-room.ts b/packages/backend/src/server/api/stream/channels/chat-room.ts deleted file mode 100644 index eda333dd30..0000000000 --- a/packages/backend/src/server/api/stream/channels/chat-room.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { bindThis } from '@/decorators.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import { ChatService } from '@/core/ChatService.js'; -import Channel, { type MiChannelService } from '../channel.js'; - -class ChatRoomChannel extends Channel { - public readonly chName = 'chatRoom'; - public static shouldShare = false; - public static requireCredential = true as const; - public static kind = 'read:chat'; - private roomId: string; - - constructor( - private chatService: ChatService, - - id: string, - connection: Channel['connection'], - ) { - super(id, connection); - } - - @bindThis - public async init(params: JsonObject) { - if (typeof params.roomId !== 'string') return; - this.roomId = params.roomId; - - this.subscriber.on(`chatRoomStream:${this.roomId}`, this.onEvent); - } - - @bindThis - private async onEvent(data: GlobalEvents['chatRoom']['payload']) { - this.send(data.type, data.body); - } - - @bindThis - public onMessage(type: string, body: any) { - switch (type) { - case 'read': - if (this.roomId) { - this.chatService.readRoomChatMessage(this.user!.id, this.roomId); - } - break; - } - } - - @bindThis - public dispose() { - this.subscriber.off(`chatRoomStream:${this.roomId}`, this.onEvent); - } -} - -@Injectable() -export class ChatRoomChannelService implements MiChannelService { - public readonly shouldShare = ChatRoomChannel.shouldShare; - public readonly requireCredential = ChatRoomChannel.requireCredential; - public readonly kind = ChatRoomChannel.kind; - - constructor( - private chatService: ChatService, - ) { - } - - @bindThis - public create(id: string, connection: Channel['connection']): ChatRoomChannel { - return new ChatRoomChannel( - this.chatService, - id, - connection, - ); - } -} diff --git a/packages/backend/src/server/api/stream/channels/chat-user.ts b/packages/backend/src/server/api/stream/channels/chat-user.ts deleted file mode 100644 index 5323484ed7..0000000000 --- a/packages/backend/src/server/api/stream/channels/chat-user.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { bindThis } from '@/decorators.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import { ChatService } from '@/core/ChatService.js'; -import Channel, { type MiChannelService } from '../channel.js'; - -class ChatUserChannel extends Channel { - public readonly chName = 'chatUser'; - public static shouldShare = false; - public static requireCredential = true as const; - public static kind = 'read:chat'; - private otherId: string; - - constructor( - private chatService: ChatService, - - id: string, - connection: Channel['connection'], - ) { - super(id, connection); - } - - @bindThis - public async init(params: JsonObject) { - if (typeof params.otherId !== 'string') return; - this.otherId = params.otherId; - - this.subscriber.on(`chatUserStream:${this.user!.id}-${this.otherId}`, this.onEvent); - } - - @bindThis - private async onEvent(data: GlobalEvents['chatUser']['payload']) { - this.send(data.type, data.body); - } - - @bindThis - public onMessage(type: string, body: any) { - switch (type) { - case 'read': - if (this.otherId) { - this.chatService.readUserChatMessage(this.user!.id, this.otherId); - } - break; - } - } - - @bindThis - public dispose() { - this.subscriber.off(`chatUserStream:${this.user!.id}-${this.otherId}`, this.onEvent); - } -} - -@Injectable() -export class ChatUserChannelService implements MiChannelService { - public readonly shouldShare = ChatUserChannel.shouldShare; - public readonly requireCredential = ChatUserChannel.requireCredential; - public readonly kind = ChatUserChannel.kind; - - constructor( - private chatService: ChatService, - ) { - } - - @bindThis - public create(id: string, connection: Channel['connection']): ChatUserChannel { - return new ChatUserChannel( - this.chatService, - id, - connection, - ); - } -} diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts index 03768f3d23..52bb29fabe 100644 --- a/packages/backend/src/server/api/stream/channels/drive.ts +++ b/packages/backend/src/server/api/stream/channels/drive.ts @@ -1,21 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; +import Channel from '../channel.js'; class DriveChannel extends Channel { public readonly chName = 'drive'; public static shouldShare = true; - public static requireCredential = true as const; - public static kind = 'read:account'; + public static requireCredential = true; @bindThis - public async init(params: JsonObject) { + public async init(params: any) { // Subscribe drive stream this.subscriber.on(`driveStream:${this.user!.id}`, data => { this.send(data); @@ -24,10 +17,9 @@ class DriveChannel extends Channel { } @Injectable() -export class DriveChannelService implements MiChannelService { +export class DriveChannelService { public readonly shouldShare = DriveChannel.shouldShare; public readonly requireCredential = DriveChannel.requireCredential; - public readonly kind = DriveChannel.kind; constructor( ) { diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 795980821b..d3339072c1 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -1,24 +1,19 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; +import { checkWordMute } from '@/misc/check-word-mute.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; +import Channel from '../channel.js'; class GlobalTimelineChannel extends Channel { public readonly chName = 'globalTimeline'; - public static shouldShare = false; - public static requireCredential = false as const; - private withRenotes: boolean; - private withFiles: boolean; + public static shouldShare = true; + public static requireCredential = false; + private withReplies: boolean; constructor( private metaService: MetaService, @@ -33,12 +28,11 @@ class GlobalTimelineChannel extends Channel { } @bindThis - public async init(params: JsonObject) { + public async init(params: any) { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.gtlAvailable) return; - this.withRenotes = !!(params.withRenotes ?? true); - this.withFiles = !!(params.withFiles ?? false); + this.withReplies = params.withReplies as boolean; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -46,24 +40,45 @@ class GlobalTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { - if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; - if (note.visibility !== 'public') return; if (note.channelId != null) return; - if (note.user.requireSigninToViewContents && this.user == null) return; - if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return; - if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return; - if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return; - - if (this.isNoteMutedOrBlocked(note)) return; - - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { - if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); - note.renote.myReaction = myRenoteReaction; - } + // リプライなら再pack + if (note.replyId != null) { + note.reply = await this.noteEntityService.pack(note.replyId, this.user, { + detail: true, + }); } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { + detail: true, + }); + } + + // 関係ない返信は除外 + if (note.reply && !this.withReplies) { + const reply = note.reply; + // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 + if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; + } + + // Ignore notes from instances the user has muted + if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return; + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; + + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; + + // 流れてきたNoteがミュートすべきNoteだったら無視する + // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) + // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 + // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 + // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる + if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; this.connection.cacheNote(note); @@ -78,10 +93,9 @@ class GlobalTimelineChannel extends Channel { } @Injectable() -export class GlobalTimelineChannelService implements MiChannelService { +export class GlobalTimelineChannelService { public readonly shouldShare = GlobalTimelineChannel.shouldShare; public readonly requireCredential = GlobalTimelineChannel.requireCredential; - public readonly kind = GlobalTimelineChannel.kind; constructor( private metaService: MetaService, diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 8105f15cb1..0268fdedde 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -1,21 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; +import Channel from '../channel.js'; class HashtagChannel extends Channel { public readonly chName = 'hashtag'; public static shouldShare = false; - public static requireCredential = false as const; + public static requireCredential = false; private q: string[][]; constructor( @@ -29,11 +23,11 @@ class HashtagChannel extends Channel { } @bindThis - public async init(params: JsonObject) { - if (!Array.isArray(params.q)) return; - if (!params.q.every(x => Array.isArray(x) && x.every(y => typeof y === 'string'))) return; + public async init(params: any) { this.q = params.q; + if (this.q == null) return; + // Subscribe stream this.subscriber.on('notesStream', this.onNote); } @@ -44,15 +38,20 @@ class HashtagChannel extends Channel { const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag)))); if (!matched) return; - if (this.isNoteMutedOrBlocked(note)) return; - - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { - if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); - note.renote.myReaction = myRenoteReaction; - } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { + detail: true, + }); } + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; + + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; + this.connection.cacheNote(note); this.send('note', note); @@ -66,10 +65,9 @@ class HashtagChannel extends Channel { } @Injectable() -export class HashtagChannelService implements MiChannelService { +export class HashtagChannelService { public readonly shouldShare = HashtagChannel.shouldShare; public readonly requireCredential = HashtagChannel.requireCredential; - public readonly kind = HashtagChannel.kind; constructor( private noteEntityService: NoteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 66644ed58c..1755aa94cf 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -1,23 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; +import { checkWordMute } from '@/misc/check-word-mute.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; +import Channel from '../channel.js'; class HomeTimelineChannel extends Channel { public readonly chName = 'homeTimeline'; - public static shouldShare = false; - public static requireCredential = true as const; - public static kind = 'read:account'; - private withRenotes: boolean; - private withFiles: boolean; + public static shouldShare = true; + public static requireCredential = true; + private withReplies: boolean; constructor( private noteEntityService: NoteEntityService, @@ -30,61 +24,67 @@ class HomeTimelineChannel extends Channel { } @bindThis - public async init(params: JsonObject) { - this.withRenotes = !!(params.withRenotes ?? true); - this.withFiles = !!(params.withFiles ?? false); - + public async init(params: any) { + this.withReplies = params.withReplies as boolean; + this.subscriber.on('notesStream', this.onNote); } @bindThis private async onNote(note: Packed<'Note'>) { - const isMe = this.user!.id === note.userId; - - if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; - if (note.channelId) { if (!this.followingChannels.has(note.channelId)) return; } else { // その投稿のユーザーをフォローしていなかったら弾く - if (!isMe && !Object.hasOwn(this.following, note.userId)) return; + if ((this.user!.id !== note.userId) && !this.following.has(note.userId)) return; } - if (note.visibility === 'followers') { - if (!isMe && !Object.hasOwn(this.following, note.userId)) return; - } else if (note.visibility === 'specified') { - if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return; + // Ignore notes from instances the user has muted + if (isInstanceMuted(note, new Set(this.userProfile!.mutedInstances ?? []))) return; + + if (['followers', 'specified'].includes(note.visibility)) { + note = await this.noteEntityService.pack(note.id, this.user!, { + detail: true, + }); + + if (note.isHidden) { + return; + } + } else { + // リプライなら再pack + if (note.replyId != null) { + note.reply = await this.noteEntityService.pack(note.replyId, this.user!, { + detail: true, + }); + } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user!, { + detail: true, + }); + } } - if (note.reply) { + // 関係ない返信は除外 + if (note.reply && !this.withReplies) { const reply = note.reply; - if (this.following[note.userId]?.withReplies) { - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; - } else { - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; - } + // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 + if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; } - // 純粋なリノート(引用リノートでないリノート)の場合 - if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) { - if (!this.withRenotes) return; - if (note.renote.reply) { - const reply = note.renote.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; - } - } + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (this.isNoteMutedOrBlocked(note)) return; + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { - if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); - note.renote.myReaction = myRenoteReaction; - } - } + // 流れてきたNoteがミュートすべきNoteだったら無視する + // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) + // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 + // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 + // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる + if (await checkWordMute(note, this.user, this.userProfile!.mutedWords)) return; this.connection.cacheNote(note); @@ -99,10 +99,9 @@ class HomeTimelineChannel extends Channel { } @Injectable() -export class HomeTimelineChannelService implements MiChannelService { +export class HomeTimelineChannelService { public readonly shouldShare = HomeTimelineChannel.shouldShare; public readonly requireCredential = HomeTimelineChannel.requireCredential; - public readonly kind = HomeTimelineChannel.kind; constructor( private noteEntityService: NoteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 5681311493..5a33e13cf5 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -1,26 +1,19 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; +import { checkWordMute } from '@/misc/check-word-mute.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import type { Packed } from '@/misc/json-schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; +import Channel from '../channel.js'; class HybridTimelineChannel extends Channel { public readonly chName = 'hybridTimeline'; - public static shouldShare = false; - public static requireCredential = true as const; - public static kind = 'read:account'; - private withRenotes: boolean; + public static shouldShare = true; + public static requireCredential = true; private withReplies: boolean; - private withFiles: boolean; constructor( private metaService: MetaService, @@ -35,13 +28,11 @@ class HybridTimelineChannel extends Channel { } @bindThis - public async init(params: JsonObject): Promise { + public async init(params: any): Promise { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; - this.withRenotes = !!(params.withRenotes ?? true); - this.withReplies = !!(params.withReplies ?? false); - this.withFiles = !!(params.withFiles ?? false); + this.withReplies = params.withReplies as boolean; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -49,56 +40,63 @@ class HybridTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { - const isMe = this.user!.id === note.userId; - - if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; - // チャンネルの投稿ではなく、自分自身の投稿 または // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または // チャンネルの投稿ではなく、全体公開のローカルの投稿 または // フォローしているチャンネルの投稿 の場合だけ if (!( - (note.channelId == null && isMe) || - (note.channelId == null && Object.hasOwn(this.following, note.userId)) || + (note.channelId == null && this.user!.id === note.userId) || + (note.channelId == null && this.following.has(note.userId)) || (note.channelId == null && (note.user.host == null && note.visibility === 'public')) || (note.channelId != null && this.followingChannels.has(note.channelId)) )) return; - if (note.visibility === 'followers') { - if (!isMe && !Object.hasOwn(this.following, note.userId)) return; - } else if (note.visibility === 'specified') { - if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return; + if (['followers', 'specified'].includes(note.visibility)) { + note = await this.noteEntityService.pack(note.id, this.user!, { + detail: true, + }); + + if (note.isHidden) { + return; + } + } else { + // リプライなら再pack + if (note.replyId != null) { + note.reply = await this.noteEntityService.pack(note.replyId, this.user!, { + detail: true, + }); + } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user!, { + detail: true, + }); + } } - if (this.isNoteMutedOrBlocked(note)) return; + // Ignore notes from instances the user has muted + if (isInstanceMuted(note, new Set(this.userProfile!.mutedInstances ?? []))) return; - if (note.reply) { + // 関係ない返信は除外 + if (note.reply && !this.withReplies) { const reply = note.reply; - if ((this.following[note.userId]?.withReplies ?? false) || this.withReplies) { - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; - } else { - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; - } + // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 + if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; } - // 純粋なリノート(引用リノートでないリノート)の場合 - if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) { - if (!this.withRenotes) return; - if (note.renote.reply) { - const reply = note.renote.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; - } - } + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (this.user && note.renoteId && !note.text) { - if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); - note.renote.myReaction = myRenoteReaction; - } - } + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; + + // 流れてきたNoteがミュートすべきNoteだったら無視する + // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) + // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 + // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 + // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる + if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; this.connection.cacheNote(note); @@ -113,10 +111,9 @@ class HybridTimelineChannel extends Channel { } @Injectable() -export class HybridTimelineChannelService implements MiChannelService { +export class HybridTimelineChannelService { public readonly shouldShare = HybridTimelineChannel.shouldShare; public readonly requireCredential = HybridTimelineChannel.requireCredential; - public readonly kind = HybridTimelineChannel.kind; constructor( private metaService: MetaService, diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 2984e18774..9ca4db8ced 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -1,25 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; +import { checkWordMute } from '@/misc/check-word-mute.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; +import Channel from '../channel.js'; class LocalTimelineChannel extends Channel { public readonly chName = 'localTimeline'; - public static shouldShare = false; - public static requireCredential = false as const; - private withRenotes: boolean; + public static shouldShare = true; + public static requireCredential = false; private withReplies: boolean; - private withFiles: boolean; constructor( private metaService: MetaService, @@ -34,13 +27,11 @@ class LocalTimelineChannel extends Channel { } @bindThis - public async init(params: JsonObject) { + public async init(params: any) { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; - this.withRenotes = !!(params.withRenotes ?? true); - this.withReplies = !!(params.withReplies ?? false); - this.withFiles = !!(params.withFiles ?? false); + this.withReplies = params.withReplies as boolean; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -48,32 +39,43 @@ class LocalTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { - if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; - if (note.user.host !== null) return; if (note.visibility !== 'public') return; - if (note.channelId != null) return; - if (note.user.requireSigninToViewContents && this.user == null) return; - if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return; - if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return; + if (note.channelId != null && !this.followingChannels.has(note.channelId)) return; + + // リプライなら再pack + if (note.replyId != null) { + note.reply = await this.noteEntityService.pack(note.replyId, this.user, { + detail: true, + }); + } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { + detail: true, + }); + } // 関係ない返信は除外 - if (note.reply && this.user && !this.following[note.userId]?.withReplies && !this.withReplies) { + if (note.reply && this.user && !this.withReplies) { const reply = note.reply; // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; } - if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (this.isNoteMutedOrBlocked(note)) return; + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { - if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); - note.renote.myReaction = myRenoteReaction; - } - } + // 流れてきたNoteがミュートすべきNoteだったら無視する + // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) + // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 + // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 + // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる + if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; this.connection.cacheNote(note); @@ -88,10 +90,9 @@ class LocalTimelineChannel extends Channel { } @Injectable() -export class LocalTimelineChannelService implements MiChannelService { +export class LocalTimelineChannelService { public readonly shouldShare = LocalTimelineChannel.shouldShare; public readonly requireCredential = LocalTimelineChannel.requireCredential; - public readonly kind = LocalTimelineChannel.kind; constructor( private metaService: MetaService, diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index 863d7f4c4e..139320ce35 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -1,20 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; +import Channel from '../channel.js'; class MainChannel extends Channel { public readonly chName = 'main'; public static shouldShare = true; - public static requireCredential = true as const; - public static kind = 'read:account'; + public static requireCredential = true; constructor( private noteEntityService: NoteEntityService, @@ -26,7 +19,7 @@ class MainChannel extends Channel { } @bindThis - public async init(params: JsonObject) { + public async init(params: any) { // Subscribe main stream channel this.subscriber.on(`mainStream:${this.user!.id}`, async data => { switch (data.type) { @@ -65,10 +58,9 @@ class MainChannel extends Channel { } @Injectable() -export class MainChannelService implements MiChannelService { +export class MainChannelService { public readonly shouldShare = MainChannel.shouldShare; public readonly requireCredential = MainChannel.requireCredential; - public readonly kind = MainChannel.kind; constructor( private noteEntityService: NoteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts index 91b62255b4..7f48c54999 100644 --- a/packages/backend/src/server/api/stream/channels/queue-stats.ts +++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts @@ -1,21 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Xev from 'xev'; import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; -import { isJsonObject } from '@/misc/json-value.js'; -import type { JsonObject, JsonValue } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; +import Channel from '../channel.js'; const ev = new Xev(); class QueueStatsChannel extends Channel { public readonly chName = 'queueStats'; public static shouldShare = true; - public static requireCredential = false as const; + public static requireCredential = false; constructor(id: string, connection: Channel['connection']) { super(id, connection); @@ -24,22 +17,19 @@ class QueueStatsChannel extends Channel { } @bindThis - public async init(params: JsonObject) { + public async init(params: any) { ev.addListener('queueStats', this.onStats); } @bindThis - private onStats(stats: JsonObject) { + private onStats(stats: any) { this.send('stats', stats); } @bindThis - public onMessage(type: string, body: JsonValue) { + public onMessage(type: string, body: any) { switch (type) { case 'requestLog': - if (!isJsonObject(body)) return; - if (typeof body.id !== 'string') return; - if (typeof body.length !== 'number') return; ev.once(`queueStatsLog:${body.id}`, statsLog => { this.send('statsLog', statsLog); }); @@ -58,10 +48,9 @@ class QueueStatsChannel extends Channel { } @Injectable() -export class QueueStatsChannelService implements MiChannelService { +export class QueueStatsChannelService { public readonly shouldShare = QueueStatsChannel.shouldShare; public readonly requireCredential = QueueStatsChannel.requireCredential; - public readonly kind = QueueStatsChannel.kind; constructor( ) { diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts deleted file mode 100644 index 7597a1cfa3..0000000000 --- a/packages/backend/src/server/api/stream/channels/reversi-game.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import type { MiReversiGame } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { ReversiService } from '@/core/ReversiService.js'; -import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; -import { isJsonObject } from '@/misc/json-value.js'; -import type { JsonObject, JsonValue } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; -import { reversiUpdateKeys } from 'misskey-js'; - -class ReversiGameChannel extends Channel { - public readonly chName = 'reversiGame'; - public static shouldShare = false; - public static requireCredential = false as const; - private gameId: MiReversiGame['id'] | null = null; - - constructor( - private reversiService: ReversiService, - private reversiGameEntityService: ReversiGameEntityService, - - id: string, - connection: Channel['connection'], - ) { - super(id, connection); - } - - @bindThis - public async init(params: JsonObject) { - if (typeof params.gameId !== 'string') return; - this.gameId = params.gameId; - - this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send); - } - - @bindThis - public onMessage(type: string, body: JsonValue) { - switch (type) { - case 'ready': - if (typeof body !== 'boolean') return; - this.ready(body); - break; - case 'updateSettings': - if (!isJsonObject(body)) return; - if (!this.reversiService.isValidReversiUpdateKey(body.key)) return; - if (!this.reversiService.isValidReversiUpdateValue(body.key, body.value)) return; - - this.updateSettings(body.key, body.value); - break; - case 'cancel': - this.cancelGame(); - break; - case 'putStone': - if (!isJsonObject(body)) return; - if (typeof body.pos !== 'number') return; - if (typeof body.id !== 'string') return; - this.putStone(body.pos, body.id); - break; - case 'claimTimeIsUp': this.claimTimeIsUp(); break; - } - } - - @bindThis - private async updateSettings(key: K, value: MiReversiGame[K]) { - if (this.user == null) return; - - this.reversiService.updateSettings(this.gameId!, this.user, key, value); - } - - @bindThis - private async ready(ready: boolean) { - if (this.user == null) return; - - this.reversiService.gameReady(this.gameId!, this.user, ready); - } - - @bindThis - private async cancelGame() { - if (this.user == null) return; - - this.reversiService.cancelGame(this.gameId!, this.user); - } - - @bindThis - private async putStone(pos: number, id: string) { - if (this.user == null) return; - - this.reversiService.putStoneToGame(this.gameId!, this.user, pos, id); - } - - @bindThis - private async claimTimeIsUp() { - if (this.user == null) return; - - this.reversiService.checkTimeout(this.gameId!); - } - - @bindThis - public dispose() { - // Unsubscribe events - this.subscriber.off(`reversiGameStream:${this.gameId}`, this.send); - } -} - -@Injectable() -export class ReversiGameChannelService implements MiChannelService { - public readonly shouldShare = ReversiGameChannel.shouldShare; - public readonly requireCredential = ReversiGameChannel.requireCredential; - public readonly kind = ReversiGameChannel.kind; - - constructor( - private reversiService: ReversiService, - private reversiGameEntityService: ReversiGameEntityService, - ) { - } - - @bindThis - public create(id: string, connection: Channel['connection']): ReversiGameChannel { - return new ReversiGameChannel( - this.reversiService, - this.reversiGameEntityService, - id, - connection, - ); - } -} diff --git a/packages/backend/src/server/api/stream/channels/reversi.ts b/packages/backend/src/server/api/stream/channels/reversi.ts deleted file mode 100644 index 6e88939724..0000000000 --- a/packages/backend/src/server/api/stream/channels/reversi.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { bindThis } from '@/decorators.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; - -class ReversiChannel extends Channel { - public readonly chName = 'reversi'; - public static shouldShare = true; - public static requireCredential = true as const; - public static kind = 'read:account'; - - constructor( - id: string, - connection: Channel['connection'], - ) { - super(id, connection); - } - - @bindThis - public async init(params: JsonObject) { - this.subscriber.on(`reversiStream:${this.user!.id}`, this.send); - } - - @bindThis - public dispose() { - // Unsubscribe events - this.subscriber.off(`reversiStream:${this.user!.id}`, this.send); - } -} - -@Injectable() -export class ReversiChannelService implements MiChannelService { - public readonly shouldShare = ReversiChannel.shouldShare; - public readonly requireCredential = ReversiChannel.requireCredential; - public readonly kind = ReversiChannel.kind; - - constructor( - ) { - } - - @bindThis - public create(id: string, connection: Channel['connection']): ReversiChannel { - return new ReversiChannel( - id, - connection, - ); - } -} diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts index fcfa26c38b..ab9c1aa0b5 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -1,22 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; +import Channel from '../channel.js'; +import { StreamMessages } from '../types.js'; import { RoleService } from '@/core/RoleService.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; class RoleTimelineChannel extends Channel { public readonly chName = 'roleTimeline'; public static shouldShare = false; - public static requireCredential = false as const; + public static requireCredential = false; private roleId: string; - + constructor( private noteEntityService: NoteEntityService, private roleservice: RoleService, @@ -29,15 +25,14 @@ class RoleTimelineChannel extends Channel { } @bindThis - public async init(params: JsonObject) { - if (typeof params.roleId !== 'string') return; - this.roleId = params.roleId; + public async init(params: any) { + this.roleId = params.roleId as string; this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent); } @bindThis - private async onEvent(data: GlobalEvents['roleTimeline']['payload']) { + private async onEvent(data: StreamMessages['roleTimeline']['payload']) { if (data.type === 'note') { const note = data.body; @@ -46,7 +41,12 @@ class RoleTimelineChannel extends Channel { } if (note.visibility !== 'public') return; - if (this.isNoteMutedOrBlocked(note)) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; + + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; this.send('note', note); } else { @@ -62,10 +62,9 @@ class RoleTimelineChannel extends Channel { } @Injectable() -export class RoleTimelineChannelService implements MiChannelService { +export class RoleTimelineChannelService { public readonly shouldShare = RoleTimelineChannel.shouldShare; public readonly requireCredential = RoleTimelineChannel.requireCredential; - public readonly kind = RoleTimelineChannel.kind; constructor( private noteEntityService: NoteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts index ec5352d12d..9eae0cf2d3 100644 --- a/packages/backend/src/server/api/stream/channels/server-stats.ts +++ b/packages/backend/src/server/api/stream/channels/server-stats.ts @@ -1,21 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import Xev from 'xev'; import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; -import { isJsonObject } from '@/misc/json-value.js'; -import type { JsonObject, JsonValue } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; +import Channel from '../channel.js'; const ev = new Xev(); class ServerStatsChannel extends Channel { public readonly chName = 'serverStats'; public static shouldShare = true; - public static requireCredential = false as const; + public static requireCredential = false; constructor(id: string, connection: Channel['connection']) { super(id, connection); @@ -24,20 +17,19 @@ class ServerStatsChannel extends Channel { } @bindThis - public async init(params: JsonObject) { + public async init(params: any) { ev.addListener('serverStats', this.onStats); } @bindThis - private onStats(stats: JsonObject) { + private onStats(stats: any) { this.send('stats', stats); } @bindThis - public onMessage(type: string, body: JsonValue) { + public onMessage(type: string, body: any) { switch (type) { case 'requestLog': - if (!isJsonObject(body)) return; ev.once(`serverStatsLog:${body.id}`, statsLog => { this.send('statsLog', statsLog); }); @@ -56,10 +48,9 @@ class ServerStatsChannel extends Channel { } @Injectable() -export class ServerStatsChannelService implements MiChannelService { +export class ServerStatsChannelService { public readonly shouldShare = ServerStatsChannel.shouldShare; public readonly requireCredential = ServerStatsChannel.requireCredential; - public readonly kind = ServerStatsChannel.kind; constructor( ) { diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 4f38351e94..8802fc5ab8 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -1,33 +1,26 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; +import type { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; +import type { User } from '@/models/entities/User.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; -import type { JsonObject } from '@/misc/json-value.js'; -import Channel, { type MiChannelService } from '../channel.js'; +import Channel from '../channel.js'; class UserListChannel extends Channel { public readonly chName = 'userList'; public static shouldShare = false; - public static requireCredential = false as const; + public static requireCredential = false; private listId: string; - private membershipsMap: Record | undefined> = {}; - private listUsersClock: NodeJS.Timeout; - private withFiles: boolean; - private withRenotes: boolean; + public listUsers: User['id'][] = []; + private listUsersClock: NodeJS.Timer; constructor( private userListsRepository: UserListsRepository, - private userListMembershipsRepository: UserListMembershipsRepository, + private userListJoiningsRepository: UserListJoiningsRepository, private noteEntityService: NoteEntityService, - + id: string, connection: Channel['connection'], ) { @@ -37,20 +30,15 @@ class UserListChannel extends Channel { } @bindThis - public async init(params: JsonObject) { - if (typeof params.listId !== 'string') return; - this.listId = params.listId; - this.withFiles = !!(params.withFiles ?? false); - this.withRenotes = !!(params.withRenotes ?? true); + public async init(params: any) { + this.listId = params.listId as string; // Check existence and owner - const listExist = await this.userListsRepository.exists({ - where: { - id: this.listId, - userId: this.user!.id, - }, + const list = await this.userListsRepository.findOneBy({ + id: this.listId, + userId: this.user!.id, }); - if (!listExist) return; + if (!list) return; // Subscribe stream this.subscriber.on(`userListStream:${this.listId}`, this.send); @@ -63,62 +51,49 @@ class UserListChannel extends Channel { @bindThis private async updateListUsers() { - const memberships = await this.userListMembershipsRepository.find({ + const users = await this.userListJoiningsRepository.find({ where: { userListId: this.listId, }, select: ['userId'], }); - const membershipsMap: Record | undefined> = {}; - for (const membership of memberships) { - membershipsMap[membership.userId] = { - withReplies: membership.withReplies, - }; - } - this.membershipsMap = membershipsMap; + this.listUsers = users.map(x => x.userId); } @bindThis private async onNote(note: Packed<'Note'>) { - const isMe = this.user!.id === note.userId; + if (!this.listUsers.includes(note.userId)) return; - // チャンネル投稿は無視する - if (note.channelId) return; + if (['followers', 'specified'].includes(note.visibility)) { + note = await this.noteEntityService.pack(note.id, this.user, { + detail: true, + }); - if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; - - if (!Object.hasOwn(this.membershipsMap, note.userId)) return; - - if (note.visibility === 'followers') { - if (!isMe && !Object.hasOwn(this.following, note.userId)) return; - } else if (note.visibility === 'specified') { - if (!note.visibleUserIds!.includes(this.user!.id)) return; - } - - if (note.reply) { - const reply = note.reply; - if (this.membershipsMap[note.userId]?.withReplies) { - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return; - } else { - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; + if (note.isHidden) { + return; + } + } else { + // リプライなら再pack + if (note.replyId != null) { + note.reply = await this.noteEntityService.pack(note.replyId, this.user, { + detail: true, + }); + } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { + detail: true, + }); } } - if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (this.isNoteMutedOrBlocked(note)) return; - - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { - if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); - note.renote.myReaction = myRenoteReaction; - } - } - - this.connection.cacheNote(note); + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; this.send('note', note); } @@ -134,17 +109,16 @@ class UserListChannel extends Channel { } @Injectable() -export class UserListChannelService implements MiChannelService { +export class UserListChannelService { public readonly shouldShare = UserListChannel.shouldShare; public readonly requireCredential = UserListChannel.requireCredential; - public readonly kind = UserListChannel.kind; constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, private noteEntityService: NoteEntityService, ) { @@ -154,7 +128,7 @@ export class UserListChannelService implements MiChannelService { public create(id: string, connection: Channel['connection']): UserListChannel { return new UserListChannel( this.userListsRepository, - this.userListMembershipsRepository, + this.userListJoiningsRepository, this.noteEntityService, id, connection, diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/index.ts similarity index 64% rename from packages/backend/src/server/api/stream/Connection.ts rename to packages/backend/src/server/api/stream/index.ts index c9801d8314..8b1c2c09c9 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -1,55 +1,44 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as WebSocket from 'ws'; -import type { MiUser } from '@/models/User.js'; -import type { MiAccessToken } from '@/models/AccessToken.js'; +import type { User } from '@/models/entities/User.js'; +import type { AccessToken } from '@/models/entities/AccessToken.js'; import type { Packed } from '@/misc/json-schema.js'; +import type { NoteReadService } from '@/core/NoteReadService.js'; import type { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; -import { MiFollowing, MiUserProfile } from '@/models/_.js'; -import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js'; -import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; -import { isJsonObject } from '@/misc/json-value.js'; -import type { JsonObject, JsonValue } from '@/misc/json-value.js'; +import { UserProfile } from '@/models/index.js'; import type { ChannelsService } from './ChannelsService.js'; import type { EventEmitter } from 'events'; import type Channel from './channel.js'; - -const MAX_CHANNELS_PER_CONNECTION = 32; +import type { StreamEventEmitter, StreamMessages } from './types.js'; /** * Main stream connection */ -// eslint-disable-next-line import/no-default-export export default class Connection { - public user?: MiUser; - public token?: MiAccessToken; + public user?: User; + public token?: AccessToken; private wsConnection: WebSocket.WebSocket; public subscriber: StreamEventEmitter; private channels: Channel[] = []; - private subscribingNotes: Partial> = {}; + private subscribingNotes: any = {}; private cachedNotes: Packed<'Note'>[] = []; - public userProfile: MiUserProfile | null = null; - public following: Record | undefined> = {}; + public userProfile: UserProfile | null = null; + public following: Set = new Set(); public followingChannels: Set = new Set(); public userIdsWhoMeMuting: Set = new Set(); public userIdsWhoBlockingMe: Set = new Set(); public userIdsWhoMeMutingRenotes: Set = new Set(); - public userMutedInstances: Set = new Set(); - private fetchIntervalId: NodeJS.Timeout | null = null; + private fetchIntervalId: NodeJS.Timer | null = null; constructor( private channelsService: ChannelsService, + private noteReadService: NoteReadService, private notificationService: NotificationService, private cacheService: CacheService, - private channelFollowingService: ChannelFollowingService, - user: MiUser | null | undefined, - token: MiAccessToken | null | undefined, + user: User | null | undefined, + token: AccessToken | null | undefined, ) { if (user) this.user = user; if (token) this.token = token; @@ -61,7 +50,7 @@ export default class Connection { const [userProfile, following, followingChannels, userIdsWhoMeMuting, userIdsWhoBlockingMe, userIdsWhoMeMutingRenotes] = await Promise.all([ this.cacheService.userProfileCache.fetch(this.user.id), this.cacheService.userFollowingsCache.fetch(this.user.id), - this.channelFollowingService.userFollowingChannelsCache.fetch(this.user.id), + this.cacheService.userFollowingChannelsCache.fetch(this.user.id), this.cacheService.userMutingsCache.fetch(this.user.id), this.cacheService.userBlockedCache.fetch(this.user.id), this.cacheService.renoteMutingsCache.fetch(this.user.id), @@ -72,7 +61,6 @@ export default class Connection { this.userIdsWhoMeMuting = userIdsWhoMeMuting; this.userIdsWhoBlockingMe = userIdsWhoBlockingMe; this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes; - this.userMutedInstances = new Set(userProfile.mutedInstances); } @bindThis @@ -103,7 +91,7 @@ export default class Connection { */ @bindThis private async onWsConnectionMessage(data: WebSocket.RawData) { - let obj: JsonObject; + let obj: Record; try { obj = JSON.parse(data.toString()); @@ -117,7 +105,7 @@ export default class Connection { case 'readNotification': this.onReadNotification(body); break; case 'subNote': this.onSubscribeNote(body); break; case 's': this.onSubscribeNote(body); break; // alias - case 'sr': this.onSubscribeNote(body); break; + case 'sr': this.onSubscribeNote(body); this.readNote(body); break; case 'unsubNote': this.onUnsubscribeNote(body); break; case 'un': this.onUnsubscribeNote(body); break; // alias case 'connect': this.onChannelConnectRequested(body); break; @@ -128,7 +116,7 @@ export default class Connection { } @bindThis - private onBroadcastMessage(data: GlobalEvents['broadcast']['payload']) { + private onBroadcastMessage(data: StreamMessages['broadcast']['payload']) { this.sendMessageToWs(data.type, data.body); } @@ -153,7 +141,19 @@ export default class Connection { } @bindThis - private onReadNotification(payload: JsonValue | undefined) { + private readNote(body: any) { + const id = body.id; + + const note = this.cachedNotes.find(n => n.id === id); + if (note == null) return; + + if (this.user && (note.userId !== this.user.id)) { + this.noteReadService.read(this.user.id, [note]); + } + } + + @bindThis + private onReadNotification(payload: any) { this.notificationService.readAllNotification(this.user!.id); } @@ -161,15 +161,16 @@ export default class Connection { * 投稿購読要求時 */ @bindThis - private onSubscribeNote(payload: JsonValue | undefined) { - if (!isJsonObject(payload)) return; - if (!payload.id || typeof payload.id !== 'string') return; + private onSubscribeNote(payload: any) { + if (!payload.id) return; - const current = this.subscribingNotes[payload.id] ?? 0; - const updated = current + 1; - this.subscribingNotes[payload.id] = updated; + if (this.subscribingNotes[payload.id] == null) { + this.subscribingNotes[payload.id] = 0; + } - if (updated === 1) { + this.subscribingNotes[payload.id]++; + + if (this.subscribingNotes[payload.id] === 1) { this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage); } } @@ -178,22 +179,18 @@ export default class Connection { * 投稿購読解除要求時 */ @bindThis - private onUnsubscribeNote(payload: JsonValue | undefined) { - if (!isJsonObject(payload)) return; - if (!payload.id || typeof payload.id !== 'string') return; + private onUnsubscribeNote(payload: any) { + if (!payload.id) return; - const current = this.subscribingNotes[payload.id]; - if (current == null) return; - const updated = current - 1; - this.subscribingNotes[payload.id] = updated; - if (updated <= 0) { + this.subscribingNotes[payload.id]--; + if (this.subscribingNotes[payload.id] <= 0) { delete this.subscribingNotes[payload.id]; this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage); } } @bindThis - private async onNoteStreamMessage(data: GlobalEvents['note']['payload']) { + private async onNoteStreamMessage(data: StreamMessages['note']['payload']) { this.sendMessageToWs('noteUpdated', { id: data.body.id, type: data.type, @@ -205,24 +202,17 @@ export default class Connection { * チャンネル接続要求時 */ @bindThis - private onChannelConnectRequested(payload: JsonValue | undefined) { - if (!isJsonObject(payload)) return; + private onChannelConnectRequested(payload: any) { const { channel, id, params, pong } = payload; - if (typeof id !== 'string') return; - if (typeof channel !== 'string') return; - if (typeof pong !== 'boolean' && typeof pong !== 'undefined' && pong !== null) return; - if (typeof params !== 'undefined' && !isJsonObject(params)) return; - this.connectChannel(id, params, channel, pong ?? undefined); + this.connectChannel(id, params, channel, pong); } /** * チャンネル切断要求時 */ @bindThis - private onChannelDisconnectRequested(payload: JsonValue | undefined) { - if (!isJsonObject(payload)) return; + private onChannelDisconnectRequested(payload: any) { const { id } = payload; - if (typeof id !== 'string') return; this.disconnectChannel(id); } @@ -230,7 +220,7 @@ export default class Connection { * クライアントにメッセージ送信 */ @bindThis - public sendMessageToWs(type: string, payload: JsonObject) { + public sendMessageToWs(type: string, payload: any) { this.wsConnection.send(JSON.stringify({ type: type, body: payload, @@ -241,22 +231,13 @@ export default class Connection { * チャンネルに接続 */ @bindThis - public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) { - if (this.channels.length >= MAX_CHANNELS_PER_CONNECTION) { - return; - } - + public connectChannel(id: string, params: any, channel: string, pong = false) { const channelService = this.channelsService.getChannelService(channel); if (channelService.requireCredential && this.user == null) { return; } - if (this.token && ((channelService.kind && !this.token.permission.some(p => p === channelService.kind)) - || (!channelService.kind && channelService.requireCredential))) { - return; - } - // 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視 if (channelService.shouldShare && this.channels.some(c => c.chName === channel)) { return; @@ -292,12 +273,7 @@ export default class Connection { * @param data メッセージ */ @bindThis - private onChannelMessageRequested(data: JsonValue | undefined) { - if (!isJsonObject(data)) return; - if (typeof data.id !== 'string') return; - if (typeof data.type !== 'string') return; - if (typeof data.body === 'undefined') return; - + private onChannelMessageRequested(data: any) { const channel = this.channels.find(c => c.id === data.id); if (channel != null && channel.onMessage != null) { channel.onMessage(data.type, data.body); diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts new file mode 100644 index 0000000000..f239b06637 --- /dev/null +++ b/packages/backend/src/server/api/stream/types.ts @@ -0,0 +1,244 @@ +import type { Channel } from '@/models/entities/Channel.js'; +import type { User } from '@/models/entities/User.js'; +import type { UserProfile } from '@/models/entities/UserProfile.js'; +import type { Note } from '@/models/entities/Note.js'; +import type { Antenna } from '@/models/entities/Antenna.js'; +import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { DriveFolder } from '@/models/entities/DriveFolder.js'; +import type { UserList } from '@/models/entities/UserList.js'; +import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; +import type { Signin } from '@/models/entities/Signin.js'; +import type { Page } from '@/models/entities/Page.js'; +import type { Packed } from '@/misc/json-schema.js'; +import type { Webhook } from '@/models/entities/Webhook.js'; +import type { Meta } from '@/models/entities/Meta.js'; +import { Role, RoleAssignment } from '@/models/index.js'; +import type Emitter from 'strict-event-emitter-types'; +import type { EventEmitter } from 'events'; + +//#region Stream type-body definitions +export interface InternalStreamTypes { + userChangeSuspendedState: { id: User['id']; isSuspended: User['isSuspended']; }; + userTokenRegenerated: { id: User['id']; oldToken: string; newToken: string; }; + remoteUserUpdated: { id: User['id']; }; + follow: { followerId: User['id']; followeeId: User['id']; }; + unfollow: { followerId: User['id']; followeeId: User['id']; }; + blockingCreated: { blockerId: User['id']; blockeeId: User['id']; }; + blockingDeleted: { blockerId: User['id']; blockeeId: User['id']; }; + policiesUpdated: Role['policies']; + roleCreated: Role; + roleDeleted: Role; + roleUpdated: Role; + userRoleAssigned: RoleAssignment; + userRoleUnassigned: RoleAssignment; + webhookCreated: Webhook; + webhookDeleted: Webhook; + webhookUpdated: Webhook; + antennaCreated: Antenna; + antennaDeleted: Antenna; + antennaUpdated: Antenna; + metaUpdated: Meta; + followChannel: { userId: User['id']; channelId: Channel['id']; }; + unfollowChannel: { userId: User['id']; channelId: Channel['id']; }; + updateUserProfile: UserProfile; + mute: { muterId: User['id']; muteeId: User['id']; }; + unmute: { muterId: User['id']; muteeId: User['id']; }; +} + +export interface BroadcastTypes { + emojiAdded: { + emoji: Packed<'EmojiDetailed'>; + }; + emojiUpdated: { + emojis: Packed<'EmojiDetailed'>[]; + }; + emojiDeleted: { + emojis: { + id?: string; + name: string; + [other: string]: any; + }[]; + }; +} + +export interface MainStreamTypes { + notification: Packed<'Notification'>; + mention: Packed<'Note'>; + reply: Packed<'Note'>; + renote: Packed<'Note'>; + follow: Packed<'UserDetailedNotMe'>; + followed: Packed<'User'>; + unfollow: Packed<'User'>; + meUpdated: Packed<'User'>; + pageEvent: { + pageId: Page['id']; + event: string; + var: any; + userId: User['id']; + user: Packed<'User'>; + }; + urlUploadFinished: { + marker?: string | null; + file: Packed<'DriveFile'>; + }; + readAllNotifications: undefined; + unreadNotification: Packed<'Notification'>; + unreadMention: Note['id']; + readAllUnreadMentions: undefined; + unreadSpecifiedNote: Note['id']; + readAllUnreadSpecifiedNotes: undefined; + readAllAntennas: undefined; + unreadAntenna: Antenna; + readAllAnnouncements: undefined; + myTokenRegenerated: undefined; + signin: Signin; + registryUpdated: { + scope?: string[]; + key: string; + value: any | null; + }; + driveFileCreated: Packed<'DriveFile'>; + readAntenna: Antenna; + receiveFollowRequest: Packed<'User'>; +} + +export interface DriveStreamTypes { + fileCreated: Packed<'DriveFile'>; + fileDeleted: DriveFile['id']; + fileUpdated: Packed<'DriveFile'>; + folderCreated: Packed<'DriveFolder'>; + folderDeleted: DriveFolder['id']; + folderUpdated: Packed<'DriveFolder'>; +} + +export interface NoteStreamTypes { + pollVoted: { + choice: number; + userId: User['id']; + }; + deleted: { + deletedAt: Date; + }; + reacted: { + reaction: string; + emoji?: { + name: string; + url: string; + } | null; + userId: User['id']; + }; + unreacted: { + reaction: string; + userId: User['id']; + }; +} +type NoteStreamEventTypes = { + [key in keyof NoteStreamTypes]: { + id: Note['id']; + body: NoteStreamTypes[key]; + }; +}; + +export interface UserListStreamTypes { + userAdded: Packed<'User'>; + userRemoved: Packed<'User'>; +} + +export interface AntennaStreamTypes { + note: Note; +} + +export interface RoleTimelineStreamTypes { + note: Packed<'Note'>; +} + +export interface AdminStreamTypes { + newAbuseUserReport: { + id: AbuseUserReport['id']; + targetUserId: User['id'], + reporterId: User['id'], + comment: string; + }; +} +//#endregion + +// 辞書(interface or type)から{ type, body }ユニオンを定義 +// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type +// VS Codeの展開を防止するためにEvents型を定義 +type Events = { [K in keyof T]: { type: K; body: T[K]; } }; +type EventUnionFromDictionary< + T extends object, + U = Events +> = U[keyof U]; + +// redis通すとDateのインスタンスはstringに変換されるので +export type Serialized = { + [K in keyof T]: + T[K] extends Date + ? string + : T[K] extends (Date | null) + ? (string | null) + : T[K] extends Record + ? Serialized + : T[K]; +}; + +type SerializedAll = { + [K in keyof T]: Serialized; +}; + +// name/messages(spec) pairs dictionary +export type StreamMessages = { + internal: { + name: 'internal'; + payload: EventUnionFromDictionary>; + }; + broadcast: { + name: 'broadcast'; + payload: EventUnionFromDictionary>; + }; + main: { + name: `mainStream:${User['id']}`; + payload: EventUnionFromDictionary>; + }; + drive: { + name: `driveStream:${User['id']}`; + payload: EventUnionFromDictionary>; + }; + note: { + name: `noteStream:${Note['id']}`; + payload: EventUnionFromDictionary>; + }; + userList: { + name: `userListStream:${UserList['id']}`; + payload: EventUnionFromDictionary>; + }; + roleTimeline: { + name: `roleTimelineStream:${Role['id']}`; + payload: EventUnionFromDictionary>; + }; + antenna: { + name: `antennaStream:${Antenna['id']}`; + payload: EventUnionFromDictionary>; + }; + admin: { + name: `adminStream:${User['id']}`; + payload: EventUnionFromDictionary>; + }; + notes: { + name: 'notesStream'; + payload: Serialized>; + }; +}; + +// API event definitions +// ストリームごとのEmitterの辞書を用意 +type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter.default void }> }; +// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; +// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする +export type StreamEventEmitter = UnionToIntersection; +// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる + +// provide stream channels union +export type StreamChannels = StreamMessages[keyof StreamMessages]['name']; diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index cdd7102666..4a461949ad 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import dns from 'node:dns/promises'; import { fileURLToPath } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; @@ -11,27 +6,24 @@ import httpLinkHeader from 'http-link-header'; import ipaddr from 'ipaddr.js'; import oauth2orize, { type OAuth2, AuthorizationError, ValidateFunctionArity2, OAuth2Req, MiddlewareRequest } from 'oauth2orize'; import oauth2Pkce from 'oauth2orize-pkce'; -import fastifyCors from '@fastify/cors'; import fastifyView from '@fastify/view'; import pug from 'pug'; import bodyParser from 'body-parser'; import fastifyExpress from '@fastify/express'; import { verifyChallenge } from 'pkce-challenge'; -import { mf2 } from 'microformats-parser'; -import { permissions as kinds } from 'misskey-js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { kinds } from '@/misc/api-permissions.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import type { AccessTokensRepository, UsersRepository } from '@/models/_.js'; +import type { AccessTokensRepository, UsersRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; -import type { MiLocalUser } from '@/models/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import { MemoryKVCache } from '@/misc/cache.js'; import { LoggerService } from '@/core/LoggerService.js'; import Logger from '@/logger.js'; -import { StatusError } from '@/misc/status-error.js'; import type { ServerResponse } from 'node:http'; import type { FastifyInstance } from 'fastify'; @@ -55,6 +47,7 @@ function validateClientId(raw: string): URL { // https://datatracker.ietf.org/doc/html/rfc6749.html#section-3.1.2.1 // 'The redirection endpoint SHOULD require the use of TLS as described // in Section 1.6 when the requested response type is "code" or "token"' + // TODO: Consider allowing custom URIs per RFC 8252. const allowedProtocols = process.env.NODE_ENV === 'test' ? ['http:', 'https:'] : ['https:']; if (!allowedProtocols.includes(url.protocol)) { throw new AuthorizationError('client_id must be a valid HTTPS URL', 'invalid_request'); @@ -95,7 +88,6 @@ interface ClientInformation { id: string; redirectUris: string[]; name: string; - logo: string | null; } // https://indieauth.spec.indieweb.org/#client-information-discovery @@ -109,7 +101,7 @@ interface ClientInformation { // Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST // look for an exact match of the given redirect_uri in the request against the list of // redirect_uris discovered after resolving any relative URLs." -async function discoverClientInformation(logger: Logger, httpRequestService: HttpRequestService, id: string): Promise { +async function discoverClientInformation(httpRequestService: HttpRequestService, id: string): Promise { try { const res = await httpRequestService.send(id); const redirectUris: string[] = []; @@ -119,42 +111,19 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri)); } - const text = await res.text(); - const fragment = JSDOM.fragment(text); + const fragment = JSDOM.fragment(await res.text()); redirectUris.push(...[...fragment.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.href)); - let name = id; - let logo: string | null = null; - if (text) { - const microformats = mf2(text, { baseUrl: res.url }); - const correspondingProperties = microformats.items.find(item => item.type?.includes('h-app') && item.properties.url.includes(id)); - if (correspondingProperties) { - const nameProperty = correspondingProperties.properties.name?.[0]; - if (typeof nameProperty === 'string') { - name = nameProperty; - } - const logoProperty = correspondingProperties.properties.logo?.[0]; - if (typeof logoProperty === 'string') { - logo = logoProperty; - } - } - } + const name = fragment.querySelector('.h-app .p-name')?.textContent?.trim() ?? id; return { id, redirectUris: redirectUris.map(uri => new URL(uri, res.url).toString()), - name: typeof name === 'string' ? name : id, - logo, + name, }; - } catch (err) { - console.error(err); - logger.error('Error while fetching client information', { err }); - if (err instanceof StatusError) { - throw new AuthorizationError('Failed to fetch client information', 'invalid_request'); - } else { - throw new AuthorizationError('Failed to parse client information', 'server_error'); - } + } catch { + throw new AuthorizationError('Failed to fetch client information', 'server_error'); } } @@ -224,7 +193,7 @@ class OAuth2Store { } store(req: OAuth2DecisionRequest, oauth2: OAuth2, cb: (err: Error | null, transactionID?: string) => void): void { - const transactionId = secureRndstr(128); + const transactionId = secureRndstr(128, true); this.#cache.set(transactionId, oauth2); cb(null, transactionId); } @@ -282,14 +251,14 @@ export class OAuth2ProviderService { throw new AuthorizationError('No user', 'invalid_request'); } const user = await this.cacheService.localUserByNativeTokenCache.fetch(token, - () => this.usersRepository.findOneBy({ token }) as Promise); + () => this.usersRepository.findOneBy({ token }) as Promise); if (!user) { throw new AuthorizationError('No such user', 'invalid_request'); } this.#logger.info(`Sending authorization code on behalf of user ${user.id} to ${client.id} through ${redirectUri}, with scope: [${areq.scope}]`); - const code = secureRndstr(128); + const code = secureRndstr(128, true); grantCodeCache.set(code, { clientId: client.id, userId: user.id, @@ -331,12 +300,13 @@ export class OAuth2ProviderService { if (!body.code_verifier) return; if (!(await verifyChallenge(body.code_verifier as string, granted.codeChallenge))) return; - const accessToken = secureRndstr(128); + const accessToken = secureRndstr(128, true); const now = new Date(); // NOTE: we don't have a setup for automatic token expiration await accessTokensRepository.insert({ - id: idService.gen(now.getTime()), + id: idService.genId(), + createdAt: now, lastUsedAt: now, userId: granted.userId, token: accessToken, @@ -359,25 +329,25 @@ export class OAuth2ProviderService { })); } - // https://datatracker.ietf.org/doc/html/rfc8414.html - // https://indieauth.spec.indieweb.org/#indieauth-server-metadata - public generateRFC8414() { - return { - issuer: this.config.url, - authorization_endpoint: new URL('/oauth/authorize', this.config.url), - token_endpoint: new URL('/oauth/token', this.config.url), - scopes_supported: kinds, - response_types_supported: ['code'], - grant_types_supported: ['authorization_code'], - service_documentation: 'https://misskey-hub.net', - code_challenge_methods_supported: ['S256'], - authorization_response_iss_parameter_supported: true, - }; - } - @bindThis public async createServer(fastify: FastifyInstance): Promise { - fastify.get('/authorize', async (request, reply) => { + // https://datatracker.ietf.org/doc/html/rfc8414.html + // https://indieauth.spec.indieweb.org/#indieauth-server-metadata + fastify.get('/.well-known/oauth-authorization-server', async (_request, reply) => { + reply.send({ + issuer: this.config.url, + authorization_endpoint: new URL('/oauth/authorize', this.config.url), + token_endpoint: new URL('/oauth/token', this.config.url), + scopes_supported: kinds, + response_types_supported: ['code'], + grant_types_supported: ['authorization_code'], + service_documentation: 'https://misskey-hub.net', + code_challenge_methods_supported: ['S256'], + authorization_response_iss_parameter_supported: true, + }); + }); + + fastify.get('/oauth/authorize', async (request, reply) => { const oauth2 = (request.raw as MiddlewareRequest).oauth2; if (!oauth2) { throw new Error('Unexpected lack of authorization information'); @@ -389,11 +359,11 @@ export class OAuth2ProviderService { return await reply.view('oauth', { transactionId: oauth2.transactionID, clientName: oauth2.client.name, - clientLogo: oauth2.client.logo, scope: oauth2.req.scope.join(' '), }); }); - fastify.post('/decision', async () => { }); + fastify.post('/oauth/decision', async () => { }); + fastify.post('/oauth/token', async () => { }); fastify.register(fastifyView, { root: fileURLToPath(new URL('../web/views', import.meta.url)), @@ -405,7 +375,7 @@ export class OAuth2ProviderService { }); await fastify.register(fastifyExpress); - fastify.use('/authorize', this.#server.authorize(((areq, done) => { + fastify.use('/oauth/authorize', this.#server.authorize(((areq, done) => { (async (): Promise> => { // This should return client/redirectURI AND the error, or // the handler can't send error to the redirection URI @@ -416,10 +386,9 @@ export class OAuth2ProviderService { const clientUrl = validateClientId(clientID); - // https://indieauth.spec.indieweb.org/#client-information-discovery - // "the server may want to resolve the domain name first and avoid fetching the document - // if the IP address is within the loopback range defined by [RFC5735] - // or any other implementation-specific internal IP address." + // TODO: Consider allowing localhost for native apps (RFC 8252) + // This is currently blocked by the redirect_uri check below, but we can theoretically + // loosen the rule for localhost as the data never leaves the client machine. if (process.env.NODE_ENV !== 'test' || process.env.MISSKEY_TEST_CHECK_IP_RANGE === '1') { const lookup = await dns.lookup(clientUrl.hostname); if (ipaddr.parse(lookup.address).range() !== 'unicast') { @@ -428,7 +397,7 @@ export class OAuth2ProviderService { } // Find client information from the remote. - const clientInfo = await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href); + const clientInfo = await discoverClientInformation(this.httpRequestService, clientUrl.href); // Require the redirect URI to be included in an explicit list, per // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3 @@ -437,7 +406,7 @@ export class OAuth2ProviderService { } try { - const scopes = [...new Set(scope)].filter(s => (kinds).includes(s)); + const scopes = [...new Set(scope)].filter(s => kinds.includes(s)); if (!scopes.length) { throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope'); } @@ -459,24 +428,30 @@ export class OAuth2ProviderService { return [null, clientInfo, redirectURI]; })().then(args => done(...args), err => done(err)); }) as ValidateFunctionArity2)); - fastify.use('/authorize', this.#server.errorHandler({ + fastify.use('/oauth/authorize', this.#server.errorHandler({ mode: 'indirect', modes: getQueryMode(this.config.url), })); - fastify.use('/authorize', this.#server.errorHandler()); + fastify.use('/oauth/authorize', this.#server.errorHandler()); - fastify.use('/decision', bodyParser.urlencoded({ extended: false })); - fastify.use('/decision', this.#server.decision((req, done) => { + fastify.use('/oauth/decision', bodyParser.urlencoded({ extended: false })); + fastify.use('/oauth/decision', this.#server.decision((req, done) => { const { body } = req as OAuth2DecisionRequest; this.#logger.info(`Received the decision. Cancel: ${!!body.cancel}`); req.user = body.login_token; done(null, undefined); })); - fastify.use('/decision', this.#server.errorHandler()); + fastify.use('/oauth/decision', this.#server.errorHandler()); + + // Clients may use JSON or urlencoded + fastify.use('/oauth/token', bodyParser.urlencoded({ extended: false })); + fastify.use('/oauth/token', bodyParser.json({ strict: true })); + fastify.use('/oauth/token', this.#server.token()); + fastify.use('/oauth/token', this.#server.errorHandler()); // Return 404 for any unknown paths under /oauth so that clients can know // whether a certain endpoint is supported or not. - fastify.all('/*', async (_request, reply) => { + fastify.all('/oauth/*', async (_request, reply) => { reply.code(404); reply.send({ error: { @@ -488,17 +463,4 @@ export class OAuth2ProviderService { }); }); } - - @bindThis - public async createTokenServer(fastify: FastifyInstance): Promise { - fastify.register(fastifyCors); - fastify.post('', async () => { }); - - await fastify.register(fastifyExpress); - // Clients may use JSON or urlencoded - fastify.use('', bodyParser.urlencoded({ extended: false })); - fastify.use('', bodyParser.json({ strict: true })); - fastify.use('', this.#server.token()); - fastify.use('', this.#server.errorHandler()); - } } diff --git a/packages/backend/src/server/web/ClientLoggerService.ts b/packages/backend/src/server/web/ClientLoggerService.ts index 83d8b5bc38..6a882aa766 100644 --- a/packages/backend/src/server/web/ClientLoggerService.ts +++ b/packages/backend/src/server/web/ClientLoggerService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 8ca61a497d..07ba2731c3 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -1,63 +1,38 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { randomUUID } from 'node:crypto'; import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; +import { v4 as uuid } from 'uuid'; +import { createBullBoard } from '@bull-board/api'; +import { BullAdapter } from '@bull-board/api/bullAdapter.js'; +import { FastifyAdapter } from '@bull-board/fastify'; import ms from 'ms'; import sharp from 'sharp'; import pug from 'pug'; import { In, IsNull } from 'typeorm'; import fastifyStatic from '@fastify/static'; import fastifyView from '@fastify/view'; +import fastifyCookie from '@fastify/cookie'; import fastifyProxy from '@fastify/http-proxy'; import vary from 'vary'; -import htmlSafeJsonStringify from 'htmlescape'; import type { Config } from '@/config.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; import { DI } from '@/di-symbols.js'; import * as Acct from '@/misc/acct.js'; -import type { - DbQueue, - DeliverQueue, - EndedPollNotificationQueue, - InboxQueue, - ObjectStorageQueue, - RelationshipQueue, - SystemQueue, - UserWebhookDeliverQueue, - SystemWebhookDeliverQueue, -} from '@/core/QueueModule.js'; +import { MetaService } from '@/core/MetaService.js'; +import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; -import { MetaEntityService } from '@/core/entities/MetaEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; -import type { - AnnouncementsRepository, - ChannelsRepository, - ClipsRepository, - FlashsRepository, - GalleryPostsRepository, - MiMeta, - NotesRepository, - PagesRepository, - ReversiGamesRepository, - UserProfilesRepository, - UsersRepository, -} from '@/models/_.js'; +import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, Meta, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type Logger from '@/logger.js'; -import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; +import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { RoleService } from '@/core/RoleService.js'; -import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; -import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js'; +import manifest from './manifest.json' assert { type: 'json' }; import { FeedService } from './FeedService.js'; import { UrlPreviewService } from './UrlPreviewService.js'; import { ClientLoggerService } from './ClientLoggerService.js'; @@ -70,9 +45,7 @@ const staticAssets = `${_dirname}/../../../assets/`; const clientAssets = `${_dirname}/../../../../frontend/assets/`; const assets = `${_dirname}/../../../../../built/_frontend_dist_/`; const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`; -const frontendViteOut = `${_dirname}/../../../../../built/_frontend_vite_/`; -const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`; -const tarball = `${_dirname}/../../../../../built/tarball/`; +const viteOut = `${_dirname}/../../../../../built/_vite_/`; @Injectable() export class ClientServerService { @@ -82,9 +55,6 @@ export class ClientServerService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -109,22 +79,14 @@ export class ClientServerService { @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, - @Inject(DI.reversiGamesRepository) - private reversiGamesRepository: ReversiGamesRepository, - - @Inject(DI.announcementsRepository) - private announcementsRepository: AnnouncementsRepository, - private flashEntityService: FlashEntityService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private pageEntityService: PageEntityService, - private metaEntityService: MetaEntityService, private galleryPostEntityService: GalleryPostEntityService, private clipEntityService: ClipEntityService, private channelEntityService: ChannelEntityService, - private reversiGameEntityService: ReversiGameEntityService, - private announcementEntityService: AnnouncementEntityService, + private metaService: MetaService, private urlPreviewService: UrlPreviewService, private feedService: FeedService, private roleService: RoleService, @@ -135,89 +97,85 @@ export class ClientServerService { @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, - @Inject('queue:relationship') public relationshipQueue: RelationshipQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, - @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, - @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, + @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, ) { //this.createServer = this.createServer.bind(this); } @bindThis private async manifestHandler(reply: FastifyReply) { - let manifest = { - // 空文字列の場合右辺を使いたいため - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'short_name': this.meta.shortName || this.meta.name || this.config.host, - // 空文字列の場合右辺を使いたいため - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'name': this.meta.name || this.config.host, - 'start_url': '/', - 'display': 'standalone', - 'background_color': '#313a42', - // 空文字列の場合右辺を使いたいため - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'theme_color': this.meta.themeColor || '#86b300', - 'icons': [{ - // 空文字列の場合右辺を使いたいため - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'src': this.meta.app192IconUrl || '/static-assets/icons/192.png', - 'sizes': '192x192', - 'type': 'image/png', - 'purpose': 'maskable', - }, { - // 空文字列の場合右辺を使いたいため - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'src': this.meta.app512IconUrl || '/static-assets/icons/512.png', - 'sizes': '512x512', - 'type': 'image/png', - 'purpose': 'maskable', - }, { - 'src': '/static-assets/splash.png', - 'sizes': '300x300', - 'type': 'image/png', - 'purpose': 'any', - }], - 'share_target': { - 'action': '/share/', - 'method': 'GET', - 'enctype': 'application/x-www-form-urlencoded', - 'params': { - 'title': 'title', - 'text': 'text', - 'url': 'url', - }, - }, - }; + const res = deepClone(manifest); - manifest = { - ...manifest, - ...JSON.parse(this.meta.manifestJsonOverride === '' ? '{}' : this.meta.manifestJsonOverride), - }; + const instance = await this.metaService.fetch(true); + + res.short_name = instance.name ?? 'Misskey'; + res.name = instance.name ?? 'Misskey'; + if (instance.themeColor) res.theme_color = instance.themeColor; reply.header('Cache-Control', 'max-age=300'); - return (manifest); + return (res); } @bindThis - private async generateCommonPugData(meta: MiMeta) { + private generateCommonPugData(meta: Meta) { return { instanceName: meta.name ?? 'Misskey', icon: meta.iconUrl, - appleTouchIcon: meta.app512IconUrl, themeColor: meta.themeColor, serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg', infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg', notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg', - instanceUrl: this.config.url, - metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)), - now: Date.now(), - federationEnabled: this.meta.federation !== 'none', }; } @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { + fastify.register(fastifyCookie, {}); + + //#region Bull Dashboard + const bullBoardPath = '/queue'; + + // Authenticate + fastify.addHook('onRequest', async (request, reply) => { + if (request.url === bullBoardPath || request.url.startsWith(bullBoardPath + '/')) { + const token = request.cookies.token; + if (token == null) { + reply.code(401); + throw new Error('login required'); + } + const user = await this.usersRepository.findOneBy({ token }); + if (user == null) { + reply.code(403); + throw new Error('no such user'); + } + const isAdministrator = await this.roleService.isAdministrator(user); + if (!isAdministrator) { + reply.code(403); + throw new Error('access denied'); + } + } + }); + + const serverAdapter = new FastifyAdapter(); + + createBullBoard({ + queues: [ + this.systemQueue, + this.endedPollNotificationQueue, + this.deliverQueue, + this.inboxQueue, + this.dbQueue, + this.objectStorageQueue, + this.webhookDeliverQueue, + ].map(q => new BullAdapter(q)), + serverAdapter, + }); + + serverAdapter.setBasePath(bullBoardPath); + (fastify.register as any)(serverAdapter.registerPlugin(), { prefix: bullBoardPath }); + //#endregion + fastify.register(fastifyView, { root: _dirname + '/views', engine: { @@ -236,42 +194,19 @@ export class ClientServerService { }); //#region vite assets - if (this.config.frontendEmbedManifestExists) { - fastify.register((fastify, options, done) => { - fastify.register(fastifyStatic, { - root: frontendViteOut, - prefix: '/vite/', - maxAge: ms('30 days'), - immutable: true, - decorateReply: false, - }); - fastify.register(fastifyStatic, { - root: frontendEmbedViteOut, - prefix: '/embed_vite/', - maxAge: ms('30 days'), - immutable: true, - decorateReply: false, - }); - fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); - done(); + if (this.config.clientManifestExists) { + fastify.register(fastifyStatic, { + root: viteOut, + prefix: '/vite/', + maxAge: ms('30 days'), + decorateReply: false, }); } else { - const configUrl = new URL(this.config.url); - const urlOriginWithoutPort = configUrl.origin.replace(/:\d+$/, ''); - - const port = (process.env.VITE_PORT ?? '5173'); fastify.register(fastifyProxy, { - upstream: urlOriginWithoutPort + ':' + port, + upstream: 'http://localhost:5173', // TODO: port configuration prefix: '/vite', rewritePrefix: '/vite', }); - - const embedPort = (process.env.EMBED_VITE_PORT ?? '5174'); - fastify.register(fastifyProxy, { - upstream: urlOriginWithoutPort + ':' + embedPort, - prefix: '/embed_vite', - rewritePrefix: '/embed_vite', - }); } //#endregion @@ -298,18 +233,6 @@ export class ClientServerService { decorateReply: false, }); - fastify.register((fastify, options, done) => { - fastify.register(fastifyStatic, { - root: tarball, - prefix: '/tarball/', - maxAge: ms('30 days'), - immutable: true, - decorateReply: false, - }); - fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); - done(); - }); - fastify.get('/favicon.ico', async (request, reply) => { return reply.sendFile('/favicon.ico', staticAssets); }); @@ -401,20 +324,15 @@ export class ClientServerService { // Manifest fastify.get('/manifest.json', async (request, reply) => await this.manifestHandler(reply)); - // Embed Javascript - fastify.get('/embed.js', async (request, reply) => { - return await reply.sendFile('/embed.js', staticAssets, { - maxAge: ms('1 day'), - }); - }); - fastify.get('/robots.txt', async (request, reply) => { return await reply.sendFile('/robots.txt', staticAssets); }); // OpenSearch XML fastify.get('/opensearch.xml', async (request, reply) => { - const name = this.meta.name ?? 'Misskey'; + const meta = await this.metaService.fetch(); + + const name = meta.name ?? 'Misskey'; let content = ''; content += ''; content += `${name}`; @@ -430,15 +348,15 @@ export class ClientServerService { //#endregion - const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => { + const renderBase = async (reply: FastifyReply) => { + const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=30'); return await reply.view('base', { - img: this.meta.bannerUrl, + img: meta.bannerUrl, url: this.config.url, - title: this.meta.name ?? 'Misskey', - desc: this.meta.description, - ...await this.generateCommonPugData(this.meta), - ...data, + title: meta.name ?? 'Misskey', + desc: meta.description, + ...this.generateCommonPugData(meta), }); }; @@ -451,16 +369,13 @@ export class ClientServerService { usernameLower: username.toLowerCase(), host: host ?? IsNull(), isSuspended: false, - requireSigninToViewContents: false, }); return user && await this.feedService.packFeed(user); }; // Atom - fastify.get<{ Params: { user?: string; } }>('/@:user.atom', async (request, reply) => { - if (request.params.user == null) return await renderBase(reply); - + fastify.get<{ Params: { user: string; } }>('/@:user.atom', async (request, reply) => { const feed = await getFeed(request.params.user); if (feed) { @@ -473,9 +388,7 @@ export class ClientServerService { }); // RSS - fastify.get<{ Params: { user?: string; } }>('/@:user.rss', async (request, reply) => { - if (request.params.user == null) return await renderBase(reply); - + fastify.get<{ Params: { user: string; } }>('/@:user.rss', async (request, reply) => { const feed = await getFeed(request.params.user); if (feed) { @@ -488,9 +401,7 @@ export class ClientServerService { }); // JSON - fastify.get<{ Params: { user?: string; } }>('/@:user.json', async (request, reply) => { - if (request.params.user == null) return await renderBase(reply); - + fastify.get<{ Params: { user: string; } }>('/@:user.json', async (request, reply) => { const feed = await getFeed(request.params.user); if (feed) { @@ -502,7 +413,7 @@ export class ClientServerService { } }); - //#region SSR + //#region SSR (for crawlers) // User fastify.get<{ Params: { user: string; sub?: string; } }>('/@:user/:sub?', async (request, reply) => { const { username, host } = Acct.parse(request.params.user); @@ -512,15 +423,9 @@ export class ClientServerService { isSuspended: false, }); - vary(reply.raw, 'Accept'); - - if ( - user != null && ( - this.meta.ugcVisibilityForVisitor === 'all' || - (this.meta.ugcVisibilityForVisitor === 'local' && user.host == null) - ) - ) { + if (user != null) { const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + const meta = await this.metaService.fetch(); const me = profile.fields ? profile.fields .filter(filed => filed.value != null && filed.value.match(/^https?:/)) @@ -532,20 +437,11 @@ export class ClientServerService { reply.header('X-Robots-Tag', 'noimageai'); reply.header('X-Robots-Tag', 'noai'); } - - const _user = await this.userEntityService.pack(user, null, { - schema: 'UserDetailed', - userProfile: profile, - }); - return await reply.view('user', { user, profile, me, - avatarUrl: _user.avatarUrl, + avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), sub: request.params.sub, - ...await this.generateCommonPugData(this.meta), - clientCtx: htmlSafeJsonStringify({ - user: _user, - }), + ...this.generateCommonPugData(meta), }); } else { // リモートユーザーなので @@ -566,8 +462,6 @@ export class ClientServerService { return; } - vary(reply.raw, 'Accept'); - reply.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`); }); @@ -575,23 +469,15 @@ export class ClientServerService { fastify.get<{ Params: { note: string; } }>('/notes/:note', async (request, reply) => { vary(reply.raw, 'Accept'); - const note = await this.notesRepository.findOne({ - where: { - id: request.params.note, - visibility: In(['public', 'home']), - }, - relations: ['user'], + const note = await this.notesRepository.findOneBy({ + id: request.params.note, + visibility: In(['public', 'home']), }); - if ( - note && - !note.user!.requireSigninToViewContents && - (this.meta.ugcVisibilityForVisitor === 'all' || - (this.meta.ugcVisibilityForVisitor === 'local' && note.userHost == null) - ) - ) { + if (note) { const _note = await this.noteEntityService.pack(note); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId }); + const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -603,10 +489,7 @@ export class ClientServerService { avatarUrl: _note.user.avatarUrl, // TODO: Let locale changeable by instance setting summary: getNoteSummary(_note), - ...await this.generateCommonPugData(this.meta), - clientCtx: htmlSafeJsonStringify({ - note: _note, - }), + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -631,6 +514,7 @@ export class ClientServerService { if (page) { const _page = await this.pageEntityService.pack(page); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: page.userId }); + const meta = await this.metaService.fetch(); if (['public'].includes(page.visibility)) { reply.header('Cache-Control', 'public, max-age=15'); } else { @@ -644,7 +528,7 @@ export class ClientServerService { page: _page, profile, avatarUrl: _page.user.avatarUrl, - ...await this.generateCommonPugData(this.meta), + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -660,6 +544,7 @@ export class ClientServerService { if (flash) { const _flash = await this.flashEntityService.pack(flash); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId }); + const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -669,7 +554,7 @@ export class ClientServerService { flash: _flash, profile, avatarUrl: _flash.user.avatarUrl, - ...await this.generateCommonPugData(this.meta), + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -685,6 +570,7 @@ export class ClientServerService { if (clip && clip.isPublic) { const _clip = await this.clipEntityService.pack(clip); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId }); + const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -694,10 +580,7 @@ export class ClientServerService { clip: _clip, profile, avatarUrl: _clip.user.avatarUrl, - ...await this.generateCommonPugData(this.meta), - clientCtx: htmlSafeJsonStringify({ - clip: _clip, - }), + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -711,6 +594,7 @@ export class ClientServerService { if (post) { const _post = await this.galleryPostEntityService.pack(post); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId }); + const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -720,7 +604,7 @@ export class ClientServerService { post: _post, profile, avatarUrl: _post.user.avatarUrl, - ...await this.generateCommonPugData(this.meta), + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -735,47 +619,11 @@ export class ClientServerService { if (channel) { const _channel = await this.channelEntityService.pack(channel); + const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); return await reply.view('channel', { channel: _channel, - ...await this.generateCommonPugData(this.meta), - }); - } else { - return await renderBase(reply); - } - }); - - // Reversi game - fastify.get<{ Params: { game: string; } }>('/reversi/g/:game', async (request, reply) => { - const game = await this.reversiGamesRepository.findOneBy({ - id: request.params.game, - }); - - if (game) { - const _game = await this.reversiGameEntityService.packDetail(game); - reply.header('Cache-Control', 'public, max-age=3600'); - return await reply.view('reversi-game', { - game: _game, - ...await this.generateCommonPugData(this.meta), - }); - } else { - return await renderBase(reply); - } - }); - - // 個別お知らせページ - fastify.get<{ Params: { announcementId: string; } }>('/announcements/:announcementId', async (request, reply) => { - const announcement = await this.announcementsRepository.findOneBy({ - id: request.params.announcementId, - userId: IsNull(), - }); - - if (announcement) { - const _announcement = await this.announcementEntityService.pack(announcement); - reply.header('Cache-Control', 'public, max-age=3600'); - return await reply.view('announcement', { - announcement: _announcement, - ...await this.generateCommonPugData(this.meta), + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -783,107 +631,19 @@ export class ClientServerService { }); //#endregion - //#region noindex pages - // Tags - fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => { - return await renderBase(reply, { noindex: true }); - }); - - // User with Tags - fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => { - return await renderBase(reply, { noindex: true }); - }); - //#endregion - - //#region embed pages - fastify.get<{ Params: { user: string; } }>('/embed/user-timeline/:user', async (request, reply) => { - reply.removeHeader('X-Frame-Options'); - - const user = await this.usersRepository.findOneBy({ - id: request.params.user, - }); - - if (user == null) return; - if (user.host != null) return; - - const _user = await this.userEntityService.pack(user); - - reply.header('Cache-Control', 'public, max-age=3600'); - return await reply.view('base-embed', { - title: this.meta.name ?? 'Misskey', - ...await this.generateCommonPugData(this.meta), - embedCtx: htmlSafeJsonStringify({ - user: _user, - }), - }); - }); - - fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => { - reply.removeHeader('X-Frame-Options'); - - const note = await this.notesRepository.findOneBy({ - id: request.params.note, - }); - - if (note == null) return; - if (['specified', 'followers'].includes(note.visibility)) return; - if (note.userHost != null) return; - - const _note = await this.noteEntityService.pack(note, null, { detail: true }); - - reply.header('Cache-Control', 'public, max-age=3600'); - return await reply.view('base-embed', { - title: this.meta.name ?? 'Misskey', - ...await this.generateCommonPugData(this.meta), - embedCtx: htmlSafeJsonStringify({ - note: _note, - }), - }); - }); - - fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => { - reply.removeHeader('X-Frame-Options'); - - const clip = await this.clipsRepository.findOneBy({ - id: request.params.clip, - }); - - if (clip == null) return; - - const _clip = await this.clipEntityService.pack(clip); - - reply.header('Cache-Control', 'public, max-age=3600'); - return await reply.view('base-embed', { - title: this.meta.name ?? 'Misskey', - ...await this.generateCommonPugData(this.meta), - embedCtx: htmlSafeJsonStringify({ - clip: _clip, - }), - }); - }); - - fastify.get('/embed/*', async (request, reply) => { - reply.removeHeader('X-Frame-Options'); - - reply.header('Cache-Control', 'public, max-age=3600'); - return await reply.view('base-embed', { - title: this.meta.name ?? 'Misskey', - ...await this.generateCommonPugData(this.meta), - }); - }); - fastify.get('/_info_card_', async (request, reply) => { + const meta = await this.metaService.fetch(true); + reply.removeHeader('X-Frame-Options'); return await reply.view('info-card', { version: this.config.version, host: this.config.host, - meta: this.meta, + meta: meta, originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }), originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }), }); }); - //#endregion fastify.get('/bios', async (request, reply) => { return await reply.view('bios', { @@ -916,9 +676,9 @@ export class ClientServerService { }); fastify.setErrorHandler(async (error, request, reply) => { - const errId = randomUUID(); - this.clientLoggerService.logger.error(`Internal error occurred in ${request.routeOptions.url}: ${error.message}`, { - path: request.routeOptions.url, + const errId = uuid(); + this.clientLoggerService.logger.error(`Internal error occured in ${request.routerPath}: ${error.message}`, { + path: request.routerPath, params: request.params, query: request.query, code: error.name, diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts index eae7645321..0c0e92cc04 100644 --- a/packages/backend/src/server/web/FeedService.ts +++ b/packages/backend/src/server/web/FeedService.ts @@ -1,21 +1,13 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; import { In, IsNull } from 'typeorm'; import { Feed } from 'feed'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, NotesRepository, UserProfilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { MiUser } from '@/models/User.js'; +import type { User } from '@/models/entities/User.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; -import { MfmService } from "@/core/MfmService.js"; -import { parse as mfmParse } from 'mfm-js'; @Injectable() export class FeedService { @@ -23,6 +15,9 @@ export class FeedService { @Inject(DI.config) private config: Config, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @@ -34,38 +29,36 @@ export class FeedService { private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, - private idService: IdService, - private mfmService: MfmService, ) { } @bindThis - public async packFeed(user: MiUser) { + public async packFeed(user: User) { const author = { link: `${this.config.url}/@${user.username}`, name: user.name ?? user.username, }; - + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - + const notes = await this.notesRepository.find({ where: { userId: user.id, renoteId: IsNull(), visibility: In(['public', 'home']), }, - order: { id: -1 }, + order: { createdAt: -1 }, take: 20, }); - + const feed = new Feed({ id: author.link, title: `${author.name} (@${user.username}@${this.config.host})`, - updated: notes.length !== 0 ? this.idService.parse(notes[0].id).date : undefined, + updated: notes[0].createdAt, generator: 'Misskey', - description: `${user.notesCount} Notes, ${profile.followingVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.followersVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, + description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, link: author.link, - image: (user.avatarId == null ? null : user.avatarUrl) ?? this.userEntityService.getIdenticonUrl(user), + image: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), feedLinks: { json: `${author.link}.json`, atom: `${author.link}.atom`, @@ -73,24 +66,23 @@ export class FeedService { author, copyright: user.name ?? user.username, }); - + for (const note of notes) { const files = note.fileIds.length > 0 ? await this.driveFilesRepository.findBy({ id: In(note.fileIds), }) : []; const file = files.find(file => file.type.startsWith('image/')); - const text = note.text; - + feed.addItem({ title: `New note by ${author.name}`, link: `${this.config.url}/notes/${note.id}`, - date: this.idService.parse(note.id).date, + date: note.createdAt, description: note.cw ?? undefined, - content: text ? this.mfmService.toHtml(mfmParse(text), JSON.parse(note.mentionedRemoteUsers)) ?? undefined : undefined, - image: file ? this.driveFileEntityService.getPublicUrl(file) : undefined, + content: note.text ?? undefined, + image: file ? this.driveFileEntityService.getPublicUrl(file) ?? undefined : undefined, }); } - + return feed; } } diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index b9a4015031..e61e92c623 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -1,20 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable } from '@nestjs/common'; -import { summaly } from '@misskey-dev/summaly'; -import { SummalyResult } from '@misskey-dev/summaly/built/summary.js'; +import { summaly } from 'summaly'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; +import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import type Logger from '@/logger.js'; import { query } from '@/misc/prelude/url.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { ApiError } from '@/server/api/error.js'; -import { MiMeta } from '@/models/Meta.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() @@ -25,9 +19,7 @@ export class UrlPreviewService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - + private metaService: MetaService, private httpRequestService: HttpRequestService, private loggerService: LoggerService, ) { @@ -37,10 +29,12 @@ export class UrlPreviewService { @bindThis private wrap(url?: string | null): string | null { return url != null - ? `${this.config.mediaProxy}/preview.webp?${query({ - url, - preview: '1', - })}` + ? url.match(/^https?:\/\//) + ? `${this.config.mediaProxy}/preview.webp?${query({ + url, + preview: '1', + })}` + : url : null; } @@ -61,25 +55,26 @@ export class UrlPreviewService { return; } - if (!this.meta.urlPreviewEnabled) { - reply.code(403); - return { - error: new ApiError({ - message: 'URL preview is disabled', - code: 'URL_PREVIEW_DISABLED', - id: '58b36e13-d2f5-0323-b0c6-76aa9dabefb8', - }), - }; - } + const meta = await this.metaService.fetch(); - this.logger.info(this.meta.urlPreviewSummaryProxyUrl + this.logger.info(meta.summalyProxy ? `(Proxy) Getting preview of ${url}@${lang} ...` : `Getting preview of ${url}@${lang} ...`); - try { - const summary = this.meta.urlPreviewSummaryProxyUrl - ? await this.fetchSummaryFromProxy(url, this.meta, lang) - : await this.fetchSummary(url, this.meta, lang); + const summary = meta.summalyProxy ? + await this.httpRequestService.getJson>(`${meta.summalyProxy}?${query({ + url: url, + lang: lang ?? 'ja-JP', + })}`) + : + await summaly(url, { + followRedirects: false, + lang: lang ?? 'ja-JP', + agent: this.config.proxy ? { + http: this.httpRequestService.httpAgent, + https: this.httpRequestService.httpsAgent, + } : undefined, + }); this.logger.succ(`Got preview of ${url}: ${summary.title}`); @@ -94,13 +89,12 @@ export class UrlPreviewService { summary.icon = this.wrap(summary.icon); summary.thumbnail = this.wrap(summary.thumbnail); - // Cache 1day - reply.header('Cache-Control', 'max-age=86400, immutable'); + // Cache 7days + reply.header('Cache-Control', 'max-age=604800, immutable'); return summary; } catch (err) { this.logger.warn(`Failed to get preview of ${url}: ${err}`); - reply.code(422); reply.header('Cache-Control', 'max-age=86400, immutable'); return { @@ -112,38 +106,4 @@ export class UrlPreviewService { }; } } - - private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise { - const agent = this.config.proxy - ? { - http: this.httpRequestService.httpAgent, - https: this.httpRequestService.httpsAgent, - } - : undefined; - - return summaly(url, { - followRedirects: this.meta.urlPreviewAllowRedirect, - lang: lang ?? 'ja-JP', - agent: agent, - userAgent: meta.urlPreviewUserAgent ?? undefined, - operationTimeout: meta.urlPreviewTimeout, - contentLengthLimit: meta.urlPreviewMaximumContentLength, - contentLengthRequired: meta.urlPreviewRequireContentLength, - }); - } - - private fetchSummaryFromProxy(url: string, meta: MiMeta, lang?: string): Promise { - const proxy = meta.urlPreviewSummaryProxyUrl!; - const queryStr = query({ - url: url, - lang: lang ?? 'ja-JP', - followRedirects: this.meta.urlPreviewAllowRedirect, - userAgent: meta.urlPreviewUserAgent ?? undefined, - operationTimeout: meta.urlPreviewTimeout, - contentLengthLimit: meta.urlPreviewMaximumContentLength, - contentLengthRequired: meta.urlPreviewRequireContentLength, - }); - - return this.httpRequestService.getJson(`${proxy}?${queryStr}`, 'application/json, */*', undefined, true); - } } diff --git a/packages/backend/src/server/web/bios.css b/packages/backend/src/server/web/bios.css index 91d1af10b4..b0da3ee39b 100644 --- a/packages/backend/src/server/web/bios.css +++ b/packages/backend/src/server/web/bios.css @@ -1,9 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - * { font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; } diff --git a/packages/backend/src/server/web/bios.js b/packages/backend/src/server/web/bios.js index 9ff5dca72a..c2ce5c3814 100644 --- a/packages/backend/src/server/web/bios.js +++ b/packages/backend/src/server/web/bios.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - 'use strict'; window.onload = async () => { @@ -13,7 +8,7 @@ window.onload = async () => { const promise = new Promise((resolve, reject) => { // Append a credential if (i) data.i = i; - + // Send request window.fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, { method: 'POST', @@ -22,7 +17,7 @@ window.onload = async () => { cache: 'no-cache' }).then(async (res) => { const body = res.status === 204 ? null : await res.json(); - + if (res.status === 200) { resolve(body); } else if (res.status === 204) { @@ -32,7 +27,7 @@ window.onload = async () => { } }).catch(reject); }); - + return promise; }; diff --git a/packages/backend/src/server/web/boot.embed.js b/packages/backend/src/server/web/boot.embed.js deleted file mode 100644 index 9de1275380..0000000000 --- a/packages/backend/src/server/web/boot.embed.js +++ /dev/null @@ -1,223 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -'use strict'; - -// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので -(async () => { - window.onerror = (e) => { - console.error(e); - renderError('SOMETHING_HAPPENED'); - }; - window.onunhandledrejection = (e) => { - console.error(e); - renderError('SOMETHING_HAPPENED_IN_PROMISE'); - }; - - let forceError = localStorage.getItem('forceError'); - if (forceError != null) { - renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.'); - return; - } - - // パラメータに応じてsplashのスタイルを変更 - const params = new URLSearchParams(location.search); - if (params.has('rounded') && params.get('rounded') === 'false') { - document.documentElement.classList.add('norounded'); - } - if (params.has('border') && params.get('border') === 'false') { - document.documentElement.classList.add('noborder'); - } - - //#region Detect language & fetch translations - if (!localStorage.hasOwnProperty('locale')) { - const supportedLangs = LANGS; - let lang = localStorage.getItem('lang'); - if (lang == null || !supportedLangs.includes(lang)) { - if (supportedLangs.includes(navigator.language)) { - lang = navigator.language; - } else { - lang = supportedLangs.find(x => x.split('-')[0] === navigator.language); - - // Fallback - if (lang == null) lang = 'en-US'; - } - } - - const metaRes = await window.fetch('/api/meta', { - method: 'POST', - body: JSON.stringify({}), - credentials: 'omit', - cache: 'no-cache', - headers: { - 'Content-Type': 'application/json', - }, - }); - if (metaRes.status !== 200) { - renderError('META_FETCH'); - return; - } - const meta = await metaRes.json(); - const v = meta.version; - if (v == null) { - renderError('META_FETCH_V'); - return; - } - - // for https://github.com/misskey-dev/misskey/issues/10202 - if (lang == null || lang.toString == null || lang.toString() === 'null') { - console.error('invalid lang value detected!!!', typeof lang, lang); - lang = 'en-US'; - } - - const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`); - if (localRes.status === 200) { - localStorage.setItem('lang', lang); - localStorage.setItem('locale', await localRes.text()); - localStorage.setItem('localeVersion', v); - } else { - renderError('LOCALE_FETCH'); - return; - } - } - //#endregion - - //#region Script - async function importAppScript() { - await import(`/embed_vite/${CLIENT_ENTRY}`) - .catch(async e => { - console.error(e); - renderError('APP_IMPORT'); - }); - } - - // タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある - if (document.readyState !== 'loading') { - importAppScript(); - } else { - window.addEventListener('DOMContentLoaded', () => { - importAppScript(); - }); - } - //#endregion - - async function addStyle(styleText) { - let css = document.createElement('style'); - css.appendChild(document.createTextNode(styleText)); - document.head.appendChild(css); - } - - async function renderError(code) { - // Cannot set property 'innerHTML' of null を回避 - if (document.readyState === 'loading') { - await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); - } - - const locale = JSON.parse(localStorage.getItem('locale') || '{}'); - - const title = locale?._bootErrors?.title || 'Failed to initialize Misskey'; - const reload = locale?.reload || 'Reload'; - - document.body.innerHTML = ` -

${title}
-
Error Code: ${code}
- `; - addStyle(` - #misskey_app, - #splash { - display: none !important; - } - - html, - body { - margin: 0; - } - - body { - position: relative; - color: #dee7e4; - font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; - line-height: 1.35; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 100vh; - margin: 0; - padding: 24px; - box-sizing: border-box; - overflow: hidden; - - border-radius: var(--radius, 12px); - border: 1px solid rgba(231, 255, 251, 0.14); - } - - body::before { - content: ''; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: #192320; - border-radius: var(--radius, 12px); - z-index: -1; - } - - html.embed.norounded body, - html.embed.norounded body::before { - border-radius: 0; - } - - html.embed.noborder body { - border: none; - } - - .icon { - max-width: 60px; - width: 100%; - height: auto; - margin-bottom: 20px; - color: #dec340; - } - - .message { - text-align: center; - font-size: 20px; - font-weight: 700; - margin-bottom: 20px; - } - - .submessage { - text-align: center; - font-size: 90%; - margin-bottom: 7.5px; - } - - .submessage:last-of-type { - margin-bottom: 20px; - } - - button { - padding: 7px 14px; - min-width: 100px; - font-weight: 700; - font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; - line-height: 1.35; - border-radius: 99rem; - background-color: #b4e900; - color: #192320; - border: none; - cursor: pointer; - -webkit-tap-highlight-color: transparent; - } - - button:hover { - background-color: #c6ff03; - }`); - } -})(); diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 24794cbf2a..38ae8ad2e5 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -1,6 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only +/** + * BOOT LOADER + * サーバーからレスポンスされるHTMLに埋め込まれるスクリプトで、以下の役割を持ちます。 + * - 翻訳ファイルをフェッチする。 + * - バージョンに基づいて適切なメインスクリプトを読み込む。 + * - キャッシュされたコンパイル済みテーマを適用する。 + * - クライアントの設定値に基づいて対応するHTMLクラス等を設定する。 + * テーマをこの段階で設定するのは、メインスクリプトが読み込まれる間もテーマを適用したいためです。 + * 注: webpackは介さないため、このファイルではrequireやimportは使えません。 */ 'use strict'; @@ -18,8 +24,7 @@ let forceError = localStorage.getItem('forceError'); if (forceError != null) { - renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.'); - return; + renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.') } //#region Detect language & fetch translations @@ -76,8 +81,8 @@ //#endregion //#region Script - async function importAppScript() { - await import(`/vite/${CLIENT_ENTRY}`) + function importAppScript() { + import(`/vite/${CLIENT_ENTRY}`) .catch(async e => { console.error(e); renderError('APP_IMPORT', e); @@ -98,7 +103,7 @@ const theme = localStorage.getItem('theme'); if (theme) { for (const [k, v] of Object.entries(JSON.parse(theme))) { - document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); + document.documentElement.style.setProperty(`--${k}`, v.toString()); // HTMLの theme-color 適用 if (k === 'htmlThemeColor') { @@ -127,6 +132,11 @@ document.documentElement.classList.add('useSystemFont'); } + const wallpaper = localStorage.getItem('wallpaper'); + if (wallpaper) { + document.documentElement.style.backgroundImage = `url(${wallpaper})`; + } + const customCss = localStorage.getItem('customCss'); if (customCss && customCss.length > 0) { const style = document.createElement('style'); @@ -140,63 +150,41 @@ document.head.appendChild(css); } - async function renderError(code, details) { - // Cannot set property 'innerHTML' of null を回避 - if (document.readyState === 'loading') { - await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); - } - - const locale = JSON.parse(localStorage.getItem('locale') || '{}'); - - const messages = Object.assign({ - title: 'Failed to initialize Misskey', - solution: 'The following actions may solve the problem.', - solution1: 'Update your os and browser', - solution2: 'Disable an adblocker', - solution3: 'Clear the browser cache', - solution4: '(Tor Browser) Set dom.webaudio.enabled to true', - otherOption: 'Other options', - otherOption1: 'Clear preferences and cache', - otherOption2: 'Start the simple client', - otherOption3: 'Start the repair tool', - }, locale?._bootErrors || {}); - const reload = locale?.reload || 'Reload'; - + function renderError(code, details) { let errorsElement = document.getElementById('errors'); if (!errorsElement) { document.body.innerHTML = ` - + -

${messages.title}

+

Failed to load
読み込みに失敗しました

-

${messages.solution}

-

${messages.solution1}

-

${messages.solution2}

-

${messages.solution3}

-

${messages.solution4}

+

The following actions may solve the problem. / 以下を行うと解決する可能性があります。

+

Clear the browser cache / ブラウザのキャッシュをクリアする

+

Update your os and browser / ブラウザおよびOSを最新バージョンに更新する

+

Disable an adblocker / アドブロッカーを無効にする

- ${messages.otherOption} + Other options / その他のオプション

@@ -212,7 +200,7 @@ ERROR CODE: ${code} - ${details.toString()} ${JSON.stringify(details)}`; + ${JSON.stringify(details)}`; errorsElement.appendChild(detailsElement); addStyle(` * { @@ -320,6 +308,6 @@ #errorInfo { width: 50%; } - }`); + `) } })(); diff --git a/packages/backend/src/server/web/cli.css b/packages/backend/src/server/web/cli.css index 4e6136d59c..07cd27830b 100644 --- a/packages/backend/src/server/web/cli.css +++ b/packages/backend/src/server/web/cli.css @@ -1,9 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - * { font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; } diff --git a/packages/backend/src/server/web/cli.js b/packages/backend/src/server/web/cli.js index 30ee77f4d9..3467f7ac2a 100644 --- a/packages/backend/src/server/web/cli.js +++ b/packages/backend/src/server/web/cli.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - 'use strict'; window.onload = async () => { @@ -13,7 +8,7 @@ window.onload = async () => { const promise = new Promise((resolve, reject) => { // Append a credential if (i) data.i = i; - + // Send request fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, { headers: { @@ -25,7 +20,7 @@ window.onload = async () => { cache: 'no-cache' }).then(async (res) => { const body = res.status === 204 ? null : await res.json(); - + if (res.status === 200) { resolve(body); } else if (res.status === 204) { @@ -35,7 +30,7 @@ window.onload = async () => { } }).catch(reject); }); - + return promise; }; diff --git a/packages/backend/src/server/web/error.css b/packages/backend/src/server/web/error.css index 803bd1b4b5..ab913f7a9f 100644 --- a/packages/backend/src/server/web/error.css +++ b/packages/backend/src/server/web/error.css @@ -1,111 +1,110 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - * { - font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; + font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; } #misskey_app, #splash { - display: none !important; + display: none !important; } body, html { - background-color: #222; - color: #dfddcc; - justify-content: center; - margin: auto; - padding: 10px; - text-align: center; + background-color: #222; + color: #dfddcc; + justify-content: center; + margin: auto; + padding: 10px; + text-align: center; } button { - border-radius: 999px; - padding: 0px 12px 0px 12px; - border: none; - cursor: pointer; - margin-bottom: 12px; + border-radius: 999px; + padding: 0px 12px 0px 12px; + border: none; + cursor: pointer; + margin-bottom: 12px; } .button-big { - background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0)); - line-height: 50px; + background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0)); + line-height: 50px; } .button-big:hover { - background: rgb(153, 204, 0); + background: rgb(153, 204, 0); } .button-small { - background: #444; - line-height: 40px; + background: #444; + line-height: 40px; } .button-small:hover { - background: #555; + background: #555; } .button-label-big { - color: #222; - font-weight: bold; - font-size: 1.2em; - padding: 12px; + color: #222; + font-weight: bold; + font-size: 20px; + padding: 12px; } .button-label-small { - color: rgb(153, 204, 0); - font-size: 16px; - padding: 12px; + color: rgb(153, 204, 0); + font-size: 16px; + padding: 12px; } a { - color: rgb(134, 179, 0); - text-decoration: none; + color: rgb(134, 179, 0); + text-decoration: none; } p, li { - font-size: 16px; + font-size: 16px; +} + +.dont-worry, +#msg { + font-size: 18px; } .icon-warning { - color: #dec340; - height: 4rem; - padding-top: 2rem; + color: #dec340; + height: 4rem; + padding-top: 2rem; } h1 { - font-size: 1.5em; - margin: 1em; + font-size: 32px; } code { - display: block; - font-family: Fira, FiraCode, monospace; - background: #333; - padding: 0.5rem 1rem; - max-width: 40rem; - border-radius: 10px; - justify-content: center; - margin: auto; - white-space: pre-wrap; - word-break: break-word; + display: block; + font-family: Fira, FiraCode, monospace; + background: #333; + padding: 0.5rem 1rem; + max-width: 40rem; + border-radius: 10px; + justify-content: center; + margin: auto; + white-space: pre-wrap; + word-break: break-word; } -#errorInfo summary { - cursor: pointer; +summary { + cursor: pointer; } -#errorInfo summary>* { - display: inline; +summary > * { + display: inline; + white-space: pre-wrap; } @media screen and (max-width: 500px) { - #errorInfo { - width: 50%; - } -} + details { + width: 50%; + } +} \ No newline at end of file diff --git a/packages/backend/src/server/web/error.js b/packages/backend/src/server/web/error.js deleted file mode 100644 index 4838dd6ef3..0000000000 --- a/packages/backend/src/server/web/error.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -'use strict'; - -(() => { - document.addEventListener('DOMContentLoaded', () => { - const locale = JSON.parse(localStorage.getItem('locale') || '{}'); - - const messages = Object.assign({ - title: 'Failed to initialize Misskey', - serverError: 'If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID.', - solution: 'The following actions may solve the problem.', - solution1: 'Update your os and browser', - solution2: 'Disable an adblocker', - solution3: 'Clear the browser cache', - solution4: '(Tor Browser) Set dom.webaudio.enabled to true', - otherOption: 'Other options', - otherOption1: 'Clear preferences and cache', - otherOption2: 'Start the simple client', - otherOption3: 'Start the repair tool', - }, locale?._bootErrors || {}); - const reload = locale?.reload || 'Reload'; - - const reloadEls = document.querySelectorAll('[data-i18n-reload]'); - for (const el of reloadEls) { - el.textContent = reload; - } - - const i18nEls = document.querySelectorAll('[data-i18n]'); - for (const el of i18nEls) { - const key = el.dataset.i18n; - if (key && messages[key]) { - el.textContent = messages[key]; - } - } - }); -})(); diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index 8e63a2ea66..d59f00fe16 100644 --- a/packages/backend/src/server/web/style.css +++ b/packages/backend/src/server/web/style.css @@ -1,12 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - html { - background-color: var(--MI_THEME-bg); - color: var(--MI_THEME-fg); + background-color: var(--bg); + color: var(--fg); } #splash { @@ -17,7 +11,7 @@ html { width: 100vw; height: 100vh; cursor: wait; - background-color: var(--MI_THEME-bg); + background-color: var(--bg); opacity: 1; transition: opacity 0.5s ease; } @@ -31,7 +25,6 @@ html { margin: auto; width: 64px; height: 64px; - border-radius: 10px; pointer-events: none; } @@ -46,9 +39,8 @@ html { width: 28px; height: 28px; transform: translateY(70px); - color: var(--MI_THEME-accent); + color: var(--accent); } - #splashSpinner > .spinner { position: absolute; top: 0; diff --git a/packages/backend/src/server/web/style.embed.css b/packages/backend/src/server/web/style.embed.css deleted file mode 100644 index 0911d562bf..0000000000 --- a/packages/backend/src/server/web/style.embed.css +++ /dev/null @@ -1,100 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -html { - background-color: var(--MI_THEME-bg); - color: var(--MI_THEME-fg); -} - -html.embed { - box-sizing: border-box; - background-color: transparent; - color-scheme: light dark; - max-width: 500px; -} - -#splash { - position: fixed; - z-index: 10000; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - cursor: wait; - background-color: var(--MI_THEME-bg); - opacity: 1; - transition: opacity 0.5s ease; -} - -html.embed #splash { - box-sizing: border-box; - min-height: 300px; - border-radius: var(--radius, 12px); - border: 1px solid var(--MI_THEME-divider, #e8e8e8); -} - -html.embed.norounded #splash { - border-radius: 0; -} - -html.embed.noborder #splash { - border: none; -} - -#splashIcon { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - margin: auto; - width: 64px; - height: 64px; - border-radius: 10px; - pointer-events: none; -} - -#splashSpinner { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - margin: auto; - display: inline-block; - width: 28px; - height: 28px; - transform: translateY(70px); - color: var(--MI_THEME-accent); -} - -#splashSpinner > .spinner { - position: absolute; - top: 0; - left: 0; - width: 28px; - height: 28px; - fill-rule: evenodd; - clip-rule: evenodd; - stroke-linecap: round; - stroke-linejoin: round; - stroke-miterlimit: 1.5; -} -#splashSpinner > .spinner.bg { - opacity: 0.275; -} -#splashSpinner > .spinner.fg { - animation: splashSpinner 0.5s linear infinite; -} - -@keyframes splashSpinner { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} diff --git a/packages/backend/src/server/web/views/announcement.pug b/packages/backend/src/server/web/views/announcement.pug deleted file mode 100644 index 7a4052e8a4..0000000000 --- a/packages/backend/src/server/web/views/announcement.pug +++ /dev/null @@ -1,21 +0,0 @@ -extends ./base - -block vars - - const title = announcement.title; - - const description = announcement.text.length > 100 ? announcement.text.slice(0, 100) + '…' : announcement.text; - - const url = `${config.url}/announcements/${announcement.id}`; - -block title - = `${title} | ${instanceName}` - -block desc - meta(name='description' content=description) - -block og - meta(property='og:type' content='article') - meta(property='og:title' content= title) - meta(property='og:description' content= description) - meta(property='og:url' content= url) - if announcement.imageUrl - meta(property='og:image' content=announcement.imageUrl) - meta(property='twitter:card' content='summary_large_image') diff --git a/packages/backend/src/server/web/views/base-embed.pug b/packages/backend/src/server/web/views/base-embed.pug deleted file mode 100644 index baa0909676..0000000000 --- a/packages/backend/src/server/web/views/base-embed.pug +++ /dev/null @@ -1,72 +0,0 @@ -block vars - -block loadClientEntry - - const entry = config.frontendEmbedEntry; - -doctype html - -html(class='embed') - - head - meta(charset='utf-8') - meta(name='application-name' content='Misskey') - meta(name='referrer' content='origin') - meta(name='theme-color' content= themeColor || '#86b300') - meta(name='theme-color-orig' content= themeColor || '#86b300') - meta(property='og:site_name' content= instanceName || 'Misskey') - meta(property='instance_url' content= instanceUrl) - meta(name='viewport' content='width=device-width, initial-scale=1') - meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no') - link(rel='icon' href= icon || '/favicon.ico') - link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png') - link(rel='modulepreload' href=`/embed_vite/${entry.file}`) - - if !config.frontendEmbedManifestExists - script(type="module" src="/embed_vite/@vite/client") - - if Array.isArray(entry.css) - each href in entry.css - link(rel='stylesheet' href=`/embed_vite/${href}`) - - title - block title - = title || 'Misskey' - - block meta - meta(name='robots' content='noindex') - - style - include ../style.embed.css - - script. - var VERSION = "#{version}"; - var CLIENT_ENTRY = "#{entry.file}"; - - script(type='application/json' id='misskey_meta' data-generated-at=now) - != metaJson - - script(type='application/json' id='misskey_embedCtx' data-generated-at=now) - != embedCtx - - script - include ../boot.embed.js - - body - noscript: p - | JavaScriptを有効にしてください - br - | Please turn on your JavaScript - div#splash - img#splashIcon(src= icon || '/static-assets/splash.png') - div#splashSpinner - - - - - - - - - - - block content diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 3883b5e5ab..74e7ae2bca 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -1,22 +1,21 @@ block vars block loadClientEntry - - const entry = config.frontendEntry; - - const baseUrl = config.url; + - const clientEntry = config.clientEntry; doctype html // - - _____ _ _ - | |_|___ ___| |_ ___ _ _ + _____ _ _ + | |_|___ ___| |_ ___ _ _ | | | | |_ -|_ -| '_| -_| | | |_|_|_|_|___|___|_,_|___|_ | |___| Thank you for using Misskey! If you are reading this message... how about joining the development? https://github.com/misskey-dev/misskey - + html @@ -27,40 +26,37 @@ html meta(name='theme-color' content= themeColor || '#86b300') meta(name='theme-color-orig' content= themeColor || '#86b300') meta(property='og:site_name' content= instanceName || 'Misskey') - meta(property='instance_url' content= instanceUrl) meta(name='viewport' content='width=device-width, initial-scale=1') - meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no') link(rel='icon' href= icon || '/favicon.ico') - link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png') + link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png') link(rel='manifest' href='/manifest.json') - link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${baseUrl}/opensearch.xml`) + link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`) link(rel='prefetch' href=serverErrorImageUrl) link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=notFoundImageUrl) - link(rel='modulepreload' href=`/vite/${entry.file}`) + //- https://github.com/misskey-dev/misskey/issues/9842 + link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.22.0') + link(rel='modulepreload' href=`/vite/${clientEntry.file}`) - if !config.frontendManifestExists + if !config.clientManifestExists script(type="module" src="/vite/@vite/client") - if Array.isArray(entry.css) - each href in entry.css + if Array.isArray(clientEntry.css) + each href in clientEntry.css link(rel='stylesheet' href=`/vite/${href}`) title block title = title || 'Misskey' - if noindex - meta(name='robots' content='noindex') - block desc meta(name='description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨') block meta block og - meta(property='og:title' content= title || 'Misskey') - meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨') + meta(property='og:title' content= title || 'Misskey') + meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨') meta(property='og:image' content= img) meta(property='twitter:card' content='summary') @@ -69,13 +65,7 @@ html script. var VERSION = "#{version}"; - var CLIENT_ENTRY = "#{entry.file}"; - - script(type='application/json' id='misskey_meta' data-generated-at=now) - != metaJson - - script(type='application/json' id='misskey_clientCtx' data-generated-at=now) - != clientCtx + var CLIENT_ENTRY = "#{clientEntry.file}"; script include ../boot.js diff --git a/packages/backend/src/server/web/views/error.pug b/packages/backend/src/server/web/views/error.pug index 6a78d1878c..b177ae4110 100644 --- a/packages/backend/src/server/web/views/error.pug +++ b/packages/backend/src/server/web/views/error.pug @@ -2,15 +2,15 @@ doctype html // - - _____ _ _ - | |_|___ ___| |_ ___ _ _ + _____ _ _ + | |_|___ ___| |_ ___ _ _ | | | | |_ -|_ -| '_| -_| | | |_|_|_|_|___|___|_,_|___|_ | - |___| + |___| Thank you for using Misskey! If you are reading this message... how about joining the development? https://github.com/misskey-dev/misskey - + html @@ -27,45 +27,39 @@ html style include ../error.css - script - include ../error.js - body svg.icon-warning(xmlns="http://www.w3.org/2000/svg", viewBox="0 0 24 24", stroke-width="2", stroke="currentColor", fill="none", stroke-linecap="round", stroke-linejoin="round") path(stroke="none", d="M0 0h24v24H0z", fill="none") path(d="M12 9v2m0 4v.01") path(d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75") - - h1(data-i18n="title") Failed to initialize Misskey + + h1 An error has occurred! button.button-big(onclick="location.reload();") - span.button-label-big(data-i18n-reload) Reload + span.button-label-big Refresh + + p.dont-worry Don't worry, it's (probably) not your fault. - p(data-i18n="serverError") If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID. + p If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID. div#errors code. ERROR CODE: #{code} ERROR ID: #{id} - p - b(data-i18n="solution") The following actions may solve the problem. + p You may also try the following options: - p(data-i18n="solution1") Update your os and browser - p(data-i18n="solution2") Disable an adblocker - p(data-i18n="solution3") Clear your browser cache - p(data-i18n="solution4") (Tor Browser) Set dom.webaudio.enabled to true + p Update your os and browser. + p Disable an adblocker. - details(style="color: #86b300;") - summary(data-i18n="otherOption") Other options - a(href="/flush") - button.button-small - span.button-label-small(data-i18n="otherOption1") Clear preferences and cache - br - a(href="/cli") - button.button-small - span.button-label-small(data-i18n="otherOption2") Start the simple client - br - a(href="/bios") - button.button-small - span.button-label-small(data-i18n="otherOption3") Start the repair tool + a(href="/flush") + button.button-small + span.button-label-small Clear preferences and cache + br + a(href="/cli") + button.button-small + span.button-label-small Start the simple client + br + a(href="/bios") + button.button-small + span.button-label-small Start the repair tool diff --git a/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug index 9ae25d9ac8..a458d7f8c7 100644 --- a/packages/backend/src/server/web/views/gallery-post.pug +++ b/packages/backend/src/server/web/views/gallery-post.pug @@ -16,12 +16,8 @@ block og meta(property='og:title' content= title) meta(property='og:description' content= post.description) meta(property='og:url' content= url) - if post.isSensitive - meta(property='og:image' content= avatarUrl) - meta(property='twitter:card' content='summary') - else - meta(property='og:image' content= post.files[0].thumbnailUrl) - meta(property='twitter:card' content='summary_large_image') + meta(property='og:image' content= post.files[0].thumbnailUrl) + meta(property='twitter:card' content='summary_large_image') block meta if user.host || profile.noCrawle diff --git a/packages/backend/src/server/web/views/info-card.pug b/packages/backend/src/server/web/views/info-card.pug index 2a4954ec8b..1d62778ce1 100644 --- a/packages/backend/src/server/web/views/info-card.pug +++ b/packages/backend/src/server/web/views/info-card.pug @@ -47,4 +47,4 @@ html header#banner(style=`background-image: url(${meta.bannerUrl})`) div#title= meta.name || host div#content - div#description!= meta.description + div#description= meta.description diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug index ea1993aed0..ea0917a80e 100644 --- a/packages/backend/src/server/web/views/note.pug +++ b/packages/backend/src/server/web/views/note.pug @@ -2,11 +2,11 @@ extends ./base block vars - const user = note.user; - - const title = user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}${user.host ? `@${user.host}` : ''}`; + - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`; - const url = `${config.url}/notes/${note.id}`; - const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null; - - const images = (note.files || []).filter(file => file.type.startsWith('image/') && !file.isSensitive) - - const videos = (note.files || []).filter(file => file.type.startsWith('video/') && !file.isSensitive) + - const image = (note.files || []).find(file => file.type.startsWith('image/') && !file.isSensitive) + - const video = (note.files || []).find(file => file.type.startsWith('video/') && !file.isSensitive) block title = `${title} | ${instanceName}` @@ -19,17 +19,15 @@ block og meta(property='og:title' content= title) meta(property='og:description' content= summary) meta(property='og:url' content= url) - if videos.length - each video in videos - meta(property='og:video:url' content= video.url) - meta(property='og:video:secure_url' content= video.url) - meta(property='og:video:type' content= video.type) - // FIXME: add width and height - // FIXME: add embed player for Twitter - if images.length + if video + meta(property='og:video:url' content= video.url) + meta(property='og:video:secure_url' content= video.url) + meta(property='og:video:type' content= video.type) + // FIXME: add width and height + // FIXME: add embed player for Twitter + if image meta(property='twitter:card' content='summary_large_image') - each image in images - meta(property='og:image' content= image.url) + meta(property='og:image' content= image.url) else meta(property='twitter:card' content='summary') meta(property='og:image' content= avatarUrl) @@ -45,7 +43,7 @@ block meta meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-id' content=user.id) meta(name='misskey:note-id' content=note.id) - + // todo if user.twitter meta(name='twitter:creator' content=`@${user.twitter.screenName}`) @@ -55,8 +53,7 @@ block meta if note.next link(rel='next' href=`${config.url}/notes/${note.next}`) - if federationEnabled - if !user.host - link(rel='alternate' href=url type='application/activity+json') - if note.uri - link(rel='alternate' href=note.uri type='application/activity+json') + if !user.host + link(rel='alternate' href=url type='application/activity+json') + if note.uri + link(rel='alternate' href=note.uri type='application/activity+json') diff --git a/packages/backend/src/server/web/views/oauth.pug b/packages/backend/src/server/web/views/oauth.pug index 4195ccc3a3..1470dbfbdf 100644 --- a/packages/backend/src/server/web/views/oauth.pug +++ b/packages/backend/src/server/web/views/oauth.pug @@ -6,6 +6,4 @@ block meta //- XXX: Remove navigation bar in auth page? meta(name='misskey:oauth:transaction-id' content=transactionId) meta(name='misskey:oauth:client-name' content=clientName) - if clientLogo - meta(name='misskey:oauth:client-logo' content=clientLogo) meta(name='misskey:oauth:scope' content=scope) diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug index 03c50eca8a..08bb08ffe7 100644 --- a/packages/backend/src/server/web/views/page.pug +++ b/packages/backend/src/server/web/views/page.pug @@ -3,7 +3,7 @@ extends ./base block vars - const user = page.user; - const title = page.title; - - const url = `${config.url}/@${user.username}/pages/${page.name}`; + - const url = `${config.url}/@${user.username}/${page.name}`; block title = `${title} | ${instanceName}` diff --git a/packages/backend/src/server/web/views/reversi-game.pug b/packages/backend/src/server/web/views/reversi-game.pug deleted file mode 100644 index 0b5ffb2bb0..0000000000 --- a/packages/backend/src/server/web/views/reversi-game.pug +++ /dev/null @@ -1,20 +0,0 @@ -extends ./base - -block vars - - const user1 = game.user1; - - const user2 = game.user2; - - const title = `${user1.username} vs ${user2.username}`; - - const url = `${config.url}/reversi/g/${game.id}`; - -block title - = `${title} | ${instanceName}` - -block desc - meta(name='description' content='⚫⚪Misskey Reversi⚪⚫') - -block og - meta(property='og:type' content='article') - meta(property='og:title' content= title) - meta(property='og:description' content='⚫⚪Misskey Reversi⚪⚫') - meta(property='og:url' content= url) - meta(property='twitter:card' content='summary') diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug index b9f740f5b6..83d57349a6 100644 --- a/packages/backend/src/server/web/views/user.pug +++ b/packages/backend/src/server/web/views/user.pug @@ -1,7 +1,7 @@ extends ./base block vars - - const title = user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}${user.host ? `@${user.host}` : ''}`; + - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`; - const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`; block title @@ -32,13 +32,12 @@ block meta meta(name='twitter:creator' content=`@${profile.twitter.screenName}`) if !sub - if federationEnabled - if !user.host - link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json') - if user.uri - link(rel='alternate' href=user.uri type='application/activity+json') - if profile.url - link(rel='alternate' href=profile.url type='text/html') + if !user.host + link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json') + if user.uri + link(rel='alternate' href=user.uri type='application/activity+json') + if profile.url + link(rel='alternate' href=profile.url type='text/html') each m in me link(rel='me' href=`${m}`) diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 5d5f1e3b71..7c6a1e5199 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -1,410 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/** - * note - 通知オンにしているユーザーが投稿した - * follow - フォローされた - * mention - 投稿で自分が言及された - * reply - 投稿に返信された - * renote - 投稿がRenoteされた - * quote - 投稿が引用Renoteされた - * reaction - 投稿にリアクションされた - * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した - * receiveFollowRequest - フォローリクエストされた - * followRequestAccepted - 自分の送ったフォローリクエストが承認された - * roleAssigned - ロールが付与された - * chatRoomInvitationReceived - チャットルームに招待された - * achievementEarned - 実績を獲得 - * exportCompleted - エクスポートが完了 - * login - ログイン - * createToken - トークン作成 - * app - アプリ通知 - * test - テスト通知(サーバー側) - */ -export const notificationTypes = [ - 'note', - 'follow', - 'mention', - 'reply', - 'renote', - 'quote', - 'reaction', - 'pollEnded', - 'receiveFollowRequest', - 'followRequestAccepted', - 'roleAssigned', - 'chatRoomInvitationReceived', - 'achievementEarned', - 'exportCompleted', - 'login', - 'createToken', - 'app', - 'test', -] as const; - -export const groupedNotificationTypes = [ - ...notificationTypes, - 'reaction:grouped', - 'renote:grouped', -] as const; - +export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; -export const followingVisibilities = ['public', 'followers', 'private'] as const; -export const followersVisibilities = ['public', 'followers', 'private'] as const; - -/** - * ユーザーがエクスポートできるものの種類 - * - * (主にエクスポート完了通知で使用するものであり、既存のDBの名称等と必ずしも一致しない) - */ -export const userExportableEntities = ['antenna', 'blocking', 'clip', 'customEmoji', 'favorite', 'following', 'muting', 'note', 'userList'] as const; - -/** - * ユーザーがインポートできるものの種類 - * - * (主にインポート完了通知で使用するものであり、既存のDBの名称等と必ずしも一致しない) - */ -export const userImportableEntities = ['antenna', 'blocking', 'customEmoji', 'following', 'muting', 'userList'] as const; - -export const moderationLogTypes = [ - 'updateServerSettings', - 'suspend', - 'unsuspend', - 'updateUserNote', - 'addCustomEmoji', - 'updateCustomEmoji', - 'deleteCustomEmoji', - 'assignRole', - 'unassignRole', - 'createRole', - 'updateRole', - 'deleteRole', - 'clearQueue', - 'promoteQueue', - 'deleteDriveFile', - 'deleteNote', - 'createGlobalAnnouncement', - 'createUserAnnouncement', - 'updateGlobalAnnouncement', - 'updateUserAnnouncement', - 'deleteGlobalAnnouncement', - 'deleteUserAnnouncement', - 'resetPassword', - 'suspendRemoteInstance', - 'unsuspendRemoteInstance', - 'updateRemoteInstanceNote', - 'markSensitiveDriveFile', - 'unmarkSensitiveDriveFile', - 'resolveAbuseReport', - 'forwardAbuseReport', - 'updateAbuseReportNote', - 'createInvitation', - 'createAd', - 'updateAd', - 'deleteAd', - 'createAvatarDecoration', - 'updateAvatarDecoration', - 'deleteAvatarDecoration', - 'unsetUserAvatar', - 'unsetUserBanner', - 'createSystemWebhook', - 'updateSystemWebhook', - 'deleteSystemWebhook', - 'createAbuseReportNotificationRecipient', - 'updateAbuseReportNotificationRecipient', - 'deleteAbuseReportNotificationRecipient', - 'deleteAccount', - 'deletePage', - 'deleteFlash', - 'deleteGalleryPost', - 'deleteChatRoom', - 'updateProxyAccountDescription', -] as const; - -export type ModerationLogPayloads = { - updateServerSettings: { - before: any | null; - after: any | null; - }; - suspend: { - userId: string; - userUsername: string; - userHost: string | null; - }; - unsuspend: { - userId: string; - userUsername: string; - userHost: string | null; - }; - updateUserNote: { - userId: string; - userUsername: string; - userHost: string | null; - before: string | null; - after: string | null; - }; - addCustomEmoji: { - emojiId: string; - emoji: any; - }; - updateCustomEmoji: { - emojiId: string; - before: any; - after: any; - }; - deleteCustomEmoji: { - emojiId: string; - emoji: any; - }; - assignRole: { - userId: string; - userUsername: string; - userHost: string | null; - roleId: string; - roleName: string; - expiresAt: string | null; - }; - unassignRole: { - userId: string; - userUsername: string; - userHost: string | null; - roleId: string; - roleName: string; - }; - createRole: { - roleId: string; - role: any; - }; - updateRole: { - roleId: string; - before: any; - after: any; - }; - deleteRole: { - roleId: string; - role: any; - }; - clearQueue: Record; - promoteQueue: Record; - deleteDriveFile: { - fileId: string; - fileUserId: string | null; - fileUserUsername: string | null; - fileUserHost: string | null; - }; - deleteNote: { - noteId: string; - noteUserId: string; - noteUserUsername: string; - noteUserHost: string | null; - note: any; - }; - createGlobalAnnouncement: { - announcementId: string; - announcement: any; - }; - createUserAnnouncement: { - announcementId: string; - announcement: any; - userId: string; - userUsername: string; - userHost: string | null; - }; - updateGlobalAnnouncement: { - announcementId: string; - before: any; - after: any; - }; - updateUserAnnouncement: { - announcementId: string; - before: any; - after: any; - userId: string; - userUsername: string; - userHost: string | null; - }; - deleteGlobalAnnouncement: { - announcementId: string; - announcement: any; - }; - deleteUserAnnouncement: { - announcementId: string; - announcement: any; - userId: string; - userUsername: string; - userHost: string | null; - }; - resetPassword: { - userId: string; - userUsername: string; - userHost: string | null; - }; - suspendRemoteInstance: { - id: string; - host: string; - }; - unsuspendRemoteInstance: { - id: string; - host: string; - }; - updateRemoteInstanceNote: { - id: string; - host: string; - before: string | null; - after: string | null; - }; - markSensitiveDriveFile: { - fileId: string; - fileUserId: string | null; - fileUserUsername: string | null; - fileUserHost: string | null; - }; - unmarkSensitiveDriveFile: { - fileId: string; - fileUserId: string | null; - fileUserUsername: string | null; - fileUserHost: string | null; - }; - resolveAbuseReport: { - reportId: string; - report: any; - forwarded?: boolean; - resolvedAs?: string | null; - }; - forwardAbuseReport: { - reportId: string; - report: any; - }; - updateAbuseReportNote: { - reportId: string; - report: any; - before: string; - after: string; - }; - createInvitation: { - invitations: any[]; - }; - createAd: { - adId: string; - ad: any; - }; - updateAd: { - adId: string; - before: any; - after: any; - }; - deleteAd: { - adId: string; - ad: any; - }; - createAvatarDecoration: { - avatarDecorationId: string; - avatarDecoration: any; - }; - updateAvatarDecoration: { - avatarDecorationId: string; - before: any; - after: any; - }; - deleteAvatarDecoration: { - avatarDecorationId: string; - avatarDecoration: any; - }; - unsetUserAvatar: { - userId: string; - userUsername: string; - userHost: string | null; - fileId: string; - }; - unsetUserBanner: { - userId: string; - userUsername: string; - userHost: string | null; - fileId: string; - }; - createSystemWebhook: { - systemWebhookId: string; - webhook: any; - }; - updateSystemWebhook: { - systemWebhookId: string; - before: any; - after: any; - }; - deleteSystemWebhook: { - systemWebhookId: string; - webhook: any; - }; - createAbuseReportNotificationRecipient: { - recipientId: string; - recipient: any; - }; - updateAbuseReportNotificationRecipient: { - recipientId: string; - before: any; - after: any; - }; - deleteAbuseReportNotificationRecipient: { - recipientId: string; - recipient: any; - }; - deleteAccount: { - userId: string; - userUsername: string; - userHost: string | null; - }; - deletePage: { - pageId: string; - pageUserId: string; - pageUserUsername: string; - page: any; - }; - deleteFlash: { - flashId: string; - flashUserId: string; - flashUserUsername: string; - flash: any; - }; - deleteGalleryPost: { - postId: string; - postUserId: string; - postUserUsername: string; - post: any; - }; - deleteChatRoom: { - roomId: string; - room: any; - }; - updateProxyAccountDescription: { - before: string | null; - after: string | null; - }; -}; - -export type Serialized = { - [K in keyof T]: - T[K] extends Date - ? string - : T[K] extends (Date | null) - ? (string | null) - : T[K] extends Record - ? Serialized - : T[K] extends (Record | null) - ? (Serialized | null) - : T[K] extends (Record | undefined) - ? (Serialized | undefined) - : T[K]; -}; - -export type FilterUnionByProperty< - Union, - Property extends string | number | symbol, - Condition, -> = Union extends Record ? Union : never; +export const ffVisibility = ['public', 'followers', 'private'] as const; diff --git a/packages/backend/test-federation/.config/example.conf b/packages/backend/test-federation/.config/example.conf deleted file mode 100644 index 83d04eb39d..0000000000 --- a/packages/backend/test-federation/.config/example.conf +++ /dev/null @@ -1,70 +0,0 @@ -# based on https://github.com/misskey-dev/misskey-hub/blob/7071f63a1c80ee35c71f0fd8a6d8722c118c7574/src/docs/admin/nginx.md - -# For WebSocket -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} - -proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off; - -server { - listen 80; - listen [::]:80; - server_name ${HOST}; - - # For SSL domain validation - root /var/www/html; - location /.well-known/acme-challenge/ { allow all; } - location /.well-known/pki-validation/ { allow all; } - location / { return 301 https://$server_name$request_uri; } -} - -server { - listen 443 ssl; - listen [::]:443 ssl; - http2 on; - server_name ${HOST}; - - ssl_session_timeout 1d; - ssl_session_cache shared:ssl_session_cache:10m; - ssl_session_tickets off; - - ssl_trusted_certificate /etc/nginx/certificates/rootCA.crt; - ssl_certificate /etc/nginx/certificates/$server_name.crt; - ssl_certificate_key /etc/nginx/certificates/$server_name.key; - - # SSL protocol settings - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; - ssl_prefer_server_ciphers off; - ssl_stapling on; - ssl_stapling_verify on; - - # Change to your upload limit - client_max_body_size 80m; - - # Proxy to Node - location / { - proxy_pass http://misskey.${HOST}:3000; - proxy_set_header Host $host; - proxy_http_version 1.1; - proxy_redirect off; - - # If it's behind another reverse proxy or CDN, remove the following. - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto https; - - # For WebSocket - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - - # Cache settings - proxy_cache cache1; - proxy_cache_lock on; - proxy_cache_use_stale updating; - proxy_force_ranges on; - add_header X-Cache $upstream_cache_status; - } -} diff --git a/packages/backend/test-federation/.config/example.default.yml b/packages/backend/test-federation/.config/example.default.yml deleted file mode 100644 index fd20613885..0000000000 --- a/packages/backend/test-federation/.config/example.default.yml +++ /dev/null @@ -1,22 +0,0 @@ -url: https://${HOST}/ -port: 3000 -db: - host: db.${HOST} - port: 5432 - db: misskey - user: postgres - pass: postgres -dbReplications: false -redis: - host: redis.test - port: 6379 -id: 'aidx' -proxyBypassHosts: - - api.deepl.com - - api-free.deepl.com - - www.recaptcha.net - - hcaptcha.com - - challenges.cloudflare.com -allowedPrivateNetworks: - - 127.0.0.1/32 - - 172.20.0.0/16 diff --git a/packages/backend/test-federation/.config/example.docker.env b/packages/backend/test-federation/.config/example.docker.env deleted file mode 100644 index a8af7cce49..0000000000 --- a/packages/backend/test-federation/.config/example.docker.env +++ /dev/null @@ -1,5 +0,0 @@ -NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt -POSTGRES_DB=misskey -POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres -MK_VERBOSE=true diff --git a/packages/backend/test-federation/.gitignore b/packages/backend/test-federation/.gitignore deleted file mode 100644 index e00f952cb5..0000000000 --- a/packages/backend/test-federation/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -certificates -volumes -.env -docker.env -*.test.conf -*.test.default.yml diff --git a/packages/backend/test-federation/README.md b/packages/backend/test-federation/README.md deleted file mode 100644 index 4ea88c1b80..0000000000 --- a/packages/backend/test-federation/README.md +++ /dev/null @@ -1,24 +0,0 @@ -## test-federation -Test federation between two Misskey servers: `a.test` and `b.test`. - -Before testing, you need to build the entire project, and change working directory to here: -```sh -pnpm build -cd packages/backend/test-federation -``` - -First, you need to start servers by executing following commands: -```sh -bash ./setup.sh -NODE_VERSION=22 docker compose up --scale tester=0 -``` - -Then you can run all tests by a following command: -```sh -NODE_VERSION=22 docker compose run --no-deps --rm tester -``` - -For testing a specific file, run a following command: -```sh -NODE_VERSION=22 docker compose run --no-deps --rm tester -- pnpm -F backend test:fed packages/backend/test-federation/test/user.test.ts -``` diff --git a/packages/backend/test-federation/compose.a.yml b/packages/backend/test-federation/compose.a.yml deleted file mode 100644 index 6a305b404c..0000000000 --- a/packages/backend/test-federation/compose.a.yml +++ /dev/null @@ -1,64 +0,0 @@ -services: - a.test: - extends: - file: ./compose.tpl.yml - service: nginx - depends_on: - misskey.a.test: - condition: service_healthy - networks: - - internal_network_a - volumes: - - type: bind - source: ./.config/a.test.conf - target: /etc/nginx/conf.d/a.test.conf - read_only: true - - type: bind - source: ./certificates/a.test.crt - target: /etc/nginx/certificates/a.test.crt - read_only: true - - type: bind - source: ./certificates/a.test.key - target: /etc/nginx/certificates/a.test.key - read_only: true - - misskey.a.test: - extends: - file: ./compose.tpl.yml - service: misskey - depends_on: - db.a.test: - condition: service_healthy - redis.test: - condition: service_healthy - setup: - condition: service_completed_successfully - networks: - - internal_network_a - volumes: - - type: bind - source: ./.config/a.test.default.yml - target: /misskey/.config/default.yml - read_only: true - - db.a.test: - extends: - file: ./compose.tpl.yml - service: db - networks: - - internal_network_a - volumes: - - type: bind - source: ./volumes/db.a - target: /var/lib/postgresql/data - bind: - create_host_path: true - -networks: - internal_network_a: - internal: true - driver: bridge - ipam: - config: - - subnet: 172.21.0.0/16 - ip_range: 172.21.0.0/24 diff --git a/packages/backend/test-federation/compose.b.yml b/packages/backend/test-federation/compose.b.yml deleted file mode 100644 index 1158b53bae..0000000000 --- a/packages/backend/test-federation/compose.b.yml +++ /dev/null @@ -1,64 +0,0 @@ -services: - b.test: - extends: - file: ./compose.tpl.yml - service: nginx - depends_on: - misskey.b.test: - condition: service_healthy - networks: - - internal_network_b - volumes: - - type: bind - source: ./.config/b.test.conf - target: /etc/nginx/conf.d/b.test.conf - read_only: true - - type: bind - source: ./certificates/b.test.crt - target: /etc/nginx/certificates/b.test.crt - read_only: true - - type: bind - source: ./certificates/b.test.key - target: /etc/nginx/certificates/b.test.key - read_only: true - - misskey.b.test: - extends: - file: ./compose.tpl.yml - service: misskey - depends_on: - db.b.test: - condition: service_healthy - redis.test: - condition: service_healthy - setup: - condition: service_completed_successfully - networks: - - internal_network_b - volumes: - - type: bind - source: ./.config/b.test.default.yml - target: /misskey/.config/default.yml - read_only: true - - db.b.test: - extends: - file: ./compose.tpl.yml - service: db - networks: - - internal_network_b - volumes: - - type: bind - source: ./volumes/db.b - target: /var/lib/postgresql/data - bind: - create_host_path: true - -networks: - internal_network_b: - internal: true - driver: bridge - ipam: - config: - - subnet: 172.22.0.0/16 - ip_range: 172.22.0.0/24 diff --git a/packages/backend/test-federation/compose.override.yaml b/packages/backend/test-federation/compose.override.yaml deleted file mode 100644 index 60a7631ab5..0000000000 --- a/packages/backend/test-federation/compose.override.yaml +++ /dev/null @@ -1,117 +0,0 @@ -services: - setup: - volumes: - - type: volume - source: node_modules - target: /misskey/node_modules - - type: volume - source: node_modules_backend - target: /misskey/packages/backend/node_modules - - type: volume - source: node_modules_misskey-js - target: /misskey/packages/misskey-js/node_modules - - type: volume - source: node_modules_misskey-reversi - target: /misskey/packages/misskey-reversi/node_modules - - tester: - networks: - external_network: - internal_network: - ipv4_address: 172.20.1.1 - volumes: - - type: volume - source: node_modules_dev - target: /misskey/node_modules - - type: volume - source: node_modules_backend_dev - target: /misskey/packages/backend/node_modules - - type: volume - source: node_modules_misskey-js_dev - target: /misskey/packages/misskey-js/node_modules - - daemon: - networks: - - external_network - - internal_network_a - - internal_network_b - volumes: - - type: volume - source: node_modules_dev - target: /misskey/node_modules - - type: volume - source: node_modules_backend_dev - target: /misskey/packages/backend/node_modules - - redis.test: - networks: - - internal_network_a - - internal_network_b - - a.test: - networks: - - internal_network - - misskey.a.test: - networks: - - external_network - - internal_network - volumes: - - type: volume - source: node_modules - target: /misskey/node_modules - - type: volume - source: node_modules_backend - target: /misskey/packages/backend/node_modules - - type: volume - source: node_modules_misskey-js - target: /misskey/packages/misskey-js/node_modules - - type: volume - source: node_modules_misskey-reversi - target: /misskey/packages/misskey-reversi/node_modules - - b.test: - networks: - - internal_network - - misskey.b.test: - networks: - - external_network - - internal_network - volumes: - - type: volume - source: node_modules - target: /misskey/node_modules - - type: volume - source: node_modules_backend - target: /misskey/packages/backend/node_modules - - type: volume - source: node_modules_misskey-js - target: /misskey/packages/misskey-js/node_modules - - type: volume - source: node_modules_misskey-reversi - target: /misskey/packages/misskey-reversi/node_modules - -networks: - external_network: - driver: bridge - ipam: - config: - - subnet: 172.23.0.0/16 - ip_range: 172.23.0.0/24 - internal_network: - internal: true - driver: bridge - ipam: - config: - - subnet: 172.20.0.0/16 - ip_range: 172.20.0.0/24 - -volumes: - node_modules: - node_modules_dev: - node_modules_backend: - node_modules_backend_dev: - node_modules_misskey-js: - node_modules_misskey-js_dev: - node_modules_misskey-reversi: diff --git a/packages/backend/test-federation/compose.tpl.yml b/packages/backend/test-federation/compose.tpl.yml deleted file mode 100644 index e4483acd7a..0000000000 --- a/packages/backend/test-federation/compose.tpl.yml +++ /dev/null @@ -1,101 +0,0 @@ -services: - nginx: - image: nginx:1.27 - volumes: - - type: bind - source: ./certificates/rootCA.crt - target: /etc/nginx/certificates/rootCA.crt - read_only: true - healthcheck: - test: service nginx status - interval: 5s - retries: 20 - - misskey: - image: node:${NODE_VERSION} - env_file: - - ./.config/docker.env - environment: - - NODE_ENV=production - volumes: - - type: bind - source: ../../../built - target: /misskey/built - read_only: true - - type: bind - source: ../assets - target: /misskey/packages/backend/assets - read_only: true - - type: bind - source: ../built - target: /misskey/packages/backend/built - read_only: true - - type: bind - source: ../migration - target: /misskey/packages/backend/migration - read_only: true - - type: bind - source: ../ormconfig.js - target: /misskey/packages/backend/ormconfig.js - read_only: true - - type: bind - source: ../package.json - target: /misskey/packages/backend/package.json - read_only: true - - type: bind - source: ../../misskey-js/built - target: /misskey/packages/misskey-js/built - read_only: true - - type: bind - source: ../../misskey-js/package.json - target: /misskey/packages/misskey-js/package.json - read_only: true - - type: bind - source: ../../misskey-reversi/built - target: /misskey/packages/misskey-reversi/built - read_only: true - - type: bind - source: ../../misskey-reversi/package.json - target: /misskey/packages/misskey-reversi/package.json - read_only: true - - type: bind - source: ../../../healthcheck.sh - target: /misskey/healthcheck.sh - read_only: true - - type: bind - source: ../../../package.json - target: /misskey/package.json - read_only: true - - type: bind - source: ../../../pnpm-lock.yaml - target: /misskey/pnpm-lock.yaml - read_only: true - - type: bind - source: ../../../pnpm-workspace.yaml - target: /misskey/pnpm-workspace.yaml - read_only: true - - type: bind - source: ./certificates/rootCA.crt - target: /usr/local/share/ca-certificates/rootCA.crt - read_only: true - working_dir: /misskey - command: > - bash -c " - npm install -g pnpm - pnpm -F backend migrate - pnpm -F backend start - " - healthcheck: - test: bash /misskey/healthcheck.sh - interval: 5s - retries: 20 - - db: - image: postgres:15-alpine - env_file: - - ./.config/docker.env - volumes: - healthcheck: - test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB - interval: 5s - retries: 20 diff --git a/packages/backend/test-federation/compose.yml b/packages/backend/test-federation/compose.yml deleted file mode 100644 index bd0ac15a31..0000000000 --- a/packages/backend/test-federation/compose.yml +++ /dev/null @@ -1,141 +0,0 @@ -include: - - ./compose.a.yml - - ./compose.b.yml - -services: - setup: - extends: - file: ./compose.tpl.yml - service: misskey - command: > - bash -c " - npm install -g pnpm - pnpm -F backend i - pnpm -F misskey-js i - pnpm -F misskey-reversi i - " - - tester: - image: node:${NODE_VERSION} - depends_on: - a.test: - condition: service_healthy - misskey.a.test: - condition: service_healthy - b.test: - condition: service_healthy - misskey.b.test: - condition: service_healthy - environment: - - NODE_ENV=development - - NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt - volumes: - - type: bind - source: ../package.json - target: /misskey/packages/backend/package.json - read_only: true - - type: bind - source: ../test/resources - target: /misskey/packages/backend/test/resources - read_only: true - - type: bind - source: ./test - target: /misskey/packages/backend/test-federation/test - read_only: true - - type: bind - source: ../jest.config.cjs - target: /misskey/packages/backend/jest.config.cjs - read_only: true - - type: bind - source: ../jest.config.fed.cjs - target: /misskey/packages/backend/jest.config.fed.cjs - read_only: true - - type: bind - source: ../jest.js - target: /misskey/packages/backend/jest.js - read_only: true - - type: bind - source: ../../misskey-js/built - target: /misskey/packages/misskey-js/built - read_only: true - - type: bind - source: ../../misskey-js/package.json - target: /misskey/packages/misskey-js/package.json - read_only: true - - type: bind - source: ../../../package.json - target: /misskey/package.json - read_only: true - - type: bind - source: ../../../pnpm-lock.yaml - target: /misskey/pnpm-lock.yaml - read_only: true - - type: bind - source: ../../../pnpm-workspace.yaml - target: /misskey/pnpm-workspace.yaml - read_only: true - - type: bind - source: ./certificates/rootCA.crt - target: /usr/local/share/ca-certificates/rootCA.crt - read_only: true - working_dir: /misskey - entrypoint: > - bash -c ' - npm install -g pnpm - pnpm -F misskey-js i --frozen-lockfile - pnpm -F backend i --frozen-lockfile - exec "$0" "$@" - ' - command: pnpm -F backend test:fed - - daemon: - image: node:${NODE_VERSION} - depends_on: - redis.test: - condition: service_healthy - volumes: - - type: bind - source: ../package.json - target: /misskey/packages/backend/package.json - read_only: true - - type: bind - source: ./daemon.ts - target: /misskey/packages/backend/test-federation/daemon.ts - read_only: true - - type: bind - source: ./tsconfig.json - target: /misskey/packages/backend/test-federation/tsconfig.json - read_only: true - - type: bind - source: ../../../package.json - target: /misskey/package.json - read_only: true - - type: bind - source: ../../../pnpm-lock.yaml - target: /misskey/pnpm-lock.yaml - read_only: true - - type: bind - source: ../../../pnpm-workspace.yaml - target: /misskey/pnpm-workspace.yaml - read_only: true - working_dir: /misskey - command: > - bash -c " - npm install -g pnpm - pnpm -F backend i --frozen-lockfile - pnpm exec tsc -p ./packages/backend/test-federation - node ./packages/backend/test-federation/built/daemon.js - " - - redis.test: - image: redis:7-alpine - volumes: - - type: bind - source: ./volumes/redis - target: /data - bind: - create_host_path: true - healthcheck: - test: redis-cli ping - interval: 5s - retries: 20 diff --git a/packages/backend/test-federation/daemon.ts b/packages/backend/test-federation/daemon.ts deleted file mode 100644 index 46b6963c79..0000000000 --- a/packages/backend/test-federation/daemon.ts +++ /dev/null @@ -1,38 +0,0 @@ -import IPCIDR from 'ip-cidr'; -import { Redis } from 'ioredis'; - -const TESTER_IP_ADDRESS = '172.20.1.1'; - -/** - * This should be same as {@link file://./../src/misc/get-ip-hash.ts}. - */ -function getIpHash(ip: string) { - const prefix = IPCIDR.createAddress(ip).mask(64); - return `ip-${BigInt('0b' + prefix).toString(36)}`; -} - -/** - * This prevents hitting rate limit when login. - */ -export async function purgeLimit(host: string, client: Redis) { - const ipHash = getIpHash(TESTER_IP_ADDRESS); - const key = `${host}:limit:${ipHash}:signin`; - const res = await client.zrange(key, 0, -1); - if (res.length !== 0) { - console.log(`${key} - ${JSON.stringify(res)}`); - await client.del(key); - } -} - -console.log('Daemon started running'); - -{ - const redisClient = new Redis({ - host: 'redis.test', - }); - - setInterval(() => { - purgeLimit('a.test', redisClient); - purgeLimit('b.test', redisClient); - }, 200); -} diff --git a/packages/backend/test-federation/eslint.config.js b/packages/backend/test-federation/eslint.config.js deleted file mode 100644 index e3bcf4c0fe..0000000000 --- a/packages/backend/test-federation/eslint.config.js +++ /dev/null @@ -1,21 +0,0 @@ -import globals from 'globals'; -import tsParser from '@typescript-eslint/parser'; -import sharedConfig from '../../shared/eslint.config.js'; - -export default [ - ...sharedConfig, - { - files: ['**/*.ts', '**/*.tsx'], - languageOptions: { - globals: { - ...globals.node, - }, - parserOptions: { - parser: tsParser, - project: ['./tsconfig.json'], - sourceType: 'module', - tsconfigRootDir: import.meta.dirname, - }, - }, - }, -]; diff --git a/packages/backend/test-federation/setup.sh b/packages/backend/test-federation/setup.sh deleted file mode 100644 index 1bc3a2a87c..0000000000 --- a/packages/backend/test-federation/setup.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -mkdir certificates - -# rootCA -openssl genrsa -des3 \ - -passout pass:rootCA \ - -out certificates/rootCA.key 4096 -openssl req -x509 -new -nodes -batch \ - -key certificates/rootCA.key \ - -sha256 \ - -days 1024 \ - -passin pass:rootCA \ - -out certificates/rootCA.crt - -# domain -function generate { - openssl req -new -newkey rsa:2048 -sha256 -nodes \ - -keyout certificates/$1.key \ - -subj "/CN=$1/emailAddress=admin@$1/C=JP/ST=/L=/O=Misskey Tester/OU=Some Unit" \ - -out certificates/$1.csr - openssl x509 -req -sha256 \ - -in certificates/$1.csr \ - -CA certificates/rootCA.crt \ - -CAkey certificates/rootCA.key \ - -CAcreateserial \ - -passin pass:rootCA \ - -out certificates/$1.crt \ - -days 500 - if [ ! -f .config/docker.env ]; then cp .config/example.docker.env .config/docker.env; fi - if [ ! -f .config/$1.conf ]; then sed "s/\${HOST}/$1/g" .config/example.conf > .config/$1.conf; fi - if [ ! -f .config/$1.default.yml ]; then sed "s/\${HOST}/$1/g" .config/example.default.yml > .config/$1.default.yml; fi -} - -generate a.test -generate b.test diff --git a/packages/backend/test-federation/test/abuse-report.test.ts b/packages/backend/test-federation/test/abuse-report.test.ts deleted file mode 100644 index ddc8e4f9d0..0000000000 --- a/packages/backend/test-federation/test/abuse-report.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { rejects, strictEqual } from 'node:assert'; -import * as Misskey from 'misskey-js'; -import { createAccount, createModerator, resolveRemoteUser, sleep, type LoginUser } from './utils.js'; - -describe('Abuse report', () => { - describe('Forwarding report', () => { - let alice: LoginUser, bob: LoginUser, aModerator: LoginUser, bModerator: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [aModerator, bModerator] = await Promise.all([ - createModerator('a.test'), - createModerator('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - }); - - test('Alice reports Bob, moderator in A forwards it, and B moderator receives it', async () => { - const comment = crypto.randomUUID(); - await alice.client.request('users/report-abuse', { userId: bobInA.id, comment }); - const reports = await aModerator.client.request('admin/abuse-user-reports', {}); - const report = reports.filter(report => report.comment === comment)[0]; - await aModerator.client.request('admin/forward-abuse-user-report', { reportId: report.id }); - await sleep(); - - const reportsInB = await bModerator.client.request('admin/abuse-user-reports', {}); - const reportInB = reportsInB.filter(report => report.comment.includes(comment))[0]; - // NOTE: reporter is not Alice, and is not moderator in A - strictEqual(reportInB.reporter.url, 'https://a.test/@system.actor'); - strictEqual(reportInB.targetUserId, bob.id); - - // NOTE: cannot forward multiple times - await rejects( - async () => await aModerator.client.request('admin/forward-abuse-user-report', { reportId: report.id }), - (err: any) => { - strictEqual(err.code, 'INTERNAL_ERROR'); - strictEqual(err.info.e.message, 'The report has already been forwarded.'); - return true; - }, - ); - }); - }); -}); diff --git a/packages/backend/test-federation/test/block.test.ts b/packages/backend/test-federation/test/block.test.ts deleted file mode 100644 index ef910eeaea..0000000000 --- a/packages/backend/test-federation/test/block.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { deepStrictEqual, rejects, strictEqual } from 'node:assert'; -import * as Misskey from 'misskey-js'; -import { assertNotificationReceived, createAccount, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js'; - -describe('Block', () => { - describe('Check follow', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - }); - - test('Cannot follow if blocked', async () => { - await alice.client.request('blocking/create', { userId: bobInA.id }); - await sleep(); - await rejects( - async () => await bob.client.request('following/create', { userId: aliceInB.id }), - (err: any) => { - strictEqual(err.code, 'BLOCKED'); - return true; - }, - ); - - const following = await bob.client.request('users/following', { userId: bob.id }); - strictEqual(following.length, 0); - const followers = await alice.client.request('users/followers', { userId: alice.id }); - strictEqual(followers.length, 0); - }); - - // FIXME: this is invalid case - test('Cannot follow even if unblocked', async () => { - // unblock here - await alice.client.request('blocking/delete', { userId: bobInA.id }); - await sleep(); - - // TODO: why still being blocked? - await rejects( - async () => await bob.client.request('following/create', { userId: aliceInB.id }), - (err: any) => { - strictEqual(err.code, 'BLOCKED'); - return true; - }, - ); - }); - - test.skip('Can follow if unblocked', async () => { - await alice.client.request('blocking/delete', { userId: bobInA.id }); - await sleep(); - - await bob.client.request('following/create', { userId: aliceInB.id }); - await sleep(); - - const following = await bob.client.request('users/following', { userId: bob.id }); - strictEqual(following.length, 1); - const followers = await alice.client.request('users/followers', { userId: alice.id }); - strictEqual(followers.length, 1); - }); - - test.skip('Remove follower when block them', async () => { - test('before block', async () => { - const following = await bob.client.request('users/following', { userId: bob.id }); - strictEqual(following.length, 1); - const followers = await alice.client.request('users/followers', { userId: alice.id }); - strictEqual(followers.length, 1); - }); - - await alice.client.request('blocking/create', { userId: bobInA.id }); - await sleep(); - - test('after block', async () => { - const following = await bob.client.request('users/following', { userId: bob.id }); - strictEqual(following.length, 0); - const followers = await alice.client.request('users/followers', { userId: alice.id }); - strictEqual(followers.length, 0); - }); - }); - }); - - describe('Check reply', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - }); - - test('Cannot reply if blocked', async () => { - await alice.client.request('blocking/create', { userId: bobInA.id }); - await sleep(); - - const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote; - const resolvedNote = await resolveRemoteNote('a.test', note.id, bob); - await rejects( - async () => await bob.client.request('notes/create', { text: 'b', replyId: resolvedNote.id }), - (err: any) => { - strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED'); - return true; - }, - ); - }); - - test('Can reply if unblocked', async () => { - await alice.client.request('blocking/delete', { userId: bobInA.id }); - await sleep(); - - const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote; - const resolvedNote = await resolveRemoteNote('a.test', note.id, bob); - const reply = (await bob.client.request('notes/create', { text: 'b', replyId: resolvedNote.id })).createdNote; - - await resolveRemoteNote('b.test', reply.id, alice); - }); - }); - - describe('Check reaction', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - }); - - test('Cannot reaction if blocked', async () => { - await alice.client.request('blocking/create', { userId: bobInA.id }); - await sleep(); - - const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote; - const resolvedNote = await resolveRemoteNote('a.test', note.id, bob); - await rejects( - async () => await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' }), - (err: any) => { - strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED'); - return true; - }, - ); - }); - - // FIXME: this is invalid case - test('Cannot reaction even if unblocked', async () => { - // unblock here - await alice.client.request('blocking/delete', { userId: bobInA.id }); - await sleep(); - - const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote; - const resolvedNote = await resolveRemoteNote('a.test', note.id, bob); - - // TODO: why still being blocked? - await rejects( - async () => await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' }), - (err: any) => { - strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED'); - return true; - }, - ); - }); - - test.skip('Can reaction if unblocked', async () => { - await alice.client.request('blocking/delete', { userId: bobInA.id }); - await sleep(); - - const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote; - const resolvedNote = await resolveRemoteNote('a.test', note.id, bob); - await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' }); - - const _note = await alice.client.request('notes/show', { noteId: note.id }); - deepStrictEqual(_note.reactions, { '😅': 1 }); - }); - }); - - describe('Check mention', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - }); - - /** NOTE: You should mute the target to stop receiving notifications */ - test('Can mention and notified even if blocked', async () => { - await alice.client.request('blocking/create', { userId: bobInA.id }); - await sleep(); - - const text = `@${alice.username}@a.test plz unblock me!`; - await assertNotificationReceived( - 'a.test', alice, - async () => await bob.client.request('notes/create', { text }), - notification => notification.type === 'mention' && notification.userId === bobInA.id && notification.note.text === text, - true, - ); - }); - }); -}); diff --git a/packages/backend/test-federation/test/drive.test.ts b/packages/backend/test-federation/test/drive.test.ts deleted file mode 100644 index f755183b4d..0000000000 --- a/packages/backend/test-federation/test/drive.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import assert, { strictEqual } from 'node:assert'; -import * as Misskey from 'misskey-js'; -import { createAccount, deepStrictEqualWithExcludedFields, fetchAdmin, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js'; - -const bAdmin = await fetchAdmin('b.test'); - -describe('Drive', () => { - describe('Upload image in a.test and resolve from b.test', () => { - let uploader: LoginUser; - - beforeAll(async () => { - uploader = await createAccount('a.test'); - }); - - let image: Misskey.entities.DriveFile, imageInB: Misskey.entities.DriveFile; - - describe('Upload', () => { - beforeAll(async () => { - image = await uploadFile('a.test', uploader); - const noteWithImage = (await uploader.client.request('notes/create', { fileIds: [image.id] })).createdNote; - const noteInB = await resolveRemoteNote('a.test', noteWithImage.id, bAdmin); - assert(noteInB.files != null); - strictEqual(noteInB.files.length, 1); - imageInB = noteInB.files[0]; - }); - - test('Check consistency of DriveFile', () => { - // console.log(`a.test: ${JSON.stringify(image, null, '\t')}`); - // console.log(`b.test: ${JSON.stringify(imageInB, null, '\t')}`); - - deepStrictEqualWithExcludedFields(image, imageInB, [ - 'id', - 'createdAt', - 'size', - 'url', - 'thumbnailUrl', - 'userId', - ]); - }); - }); - - let updatedImage: Misskey.entities.DriveFile, updatedImageInB: Misskey.entities.DriveFile; - - describe('Update', () => { - beforeAll(async () => { - updatedImage = await uploader.client.request('drive/files/update', { - fileId: image.id, - name: 'updated_192.jpg', - isSensitive: true, - }); - - updatedImageInB = await bAdmin.client.request('drive/files/show', { - fileId: imageInB.id, - }); - }); - - test('Check consistency', () => { - // console.log(`a.test: ${JSON.stringify(updatedImage, null, '\t')}`); - // console.log(`b.test: ${JSON.stringify(updatedImageInB, null, '\t')}`); - - // FIXME: not updated with `drive/files/update` - strictEqual(updatedImage.isSensitive, true); - strictEqual(updatedImage.name, 'updated_192.jpg'); - strictEqual(updatedImageInB.isSensitive, false); - strictEqual(updatedImageInB.name, '192.jpg'); - }); - }); - - let reupdatedImageInB: Misskey.entities.DriveFile; - - describe('Re-update with attaching to Note', () => { - beforeAll(async () => { - const noteWithUpdatedImage = (await uploader.client.request('notes/create', { fileIds: [updatedImage.id] })).createdNote; - const noteWithUpdatedImageInB = await resolveRemoteNote('a.test', noteWithUpdatedImage.id, bAdmin); - assert(noteWithUpdatedImageInB.files != null); - strictEqual(noteWithUpdatedImageInB.files.length, 1); - reupdatedImageInB = noteWithUpdatedImageInB.files[0]; - }); - - test('Check consistency', () => { - // console.log(`b.test: ${JSON.stringify(reupdatedImageInB, null, '\t')}`); - - // `isSensitive` is updated - strictEqual(reupdatedImageInB.isSensitive, true); - // FIXME: but `name` is not updated - strictEqual(reupdatedImageInB.name, '192.jpg'); - }); - }); - }); - - describe('Sensitive flag', () => { - describe('isSensitive is federated in delivering to followers', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - - await bob.client.request('following/create', { userId: aliceInB.id }); - await sleep(); - }); - - test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => { - const file = await uploadFile('a.test', alice); - await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true }); - await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id] }); - await sleep(); - - const notes = await bob.client.request('notes/timeline', {}); - strictEqual(notes.length, 1); - const noteInB = notes[0]; - assert(noteInB.files != null); - strictEqual(noteInB.files.length, 1); - strictEqual(noteInB.files[0].isSensitive, true); - }); - }); - - describe('isSensitive is federated in resolving', () => { - let alice: LoginUser, bob: LoginUser; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - }); - - test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => { - const file = await uploadFile('a.test', alice); - await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true }); - const note = (await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id] })).createdNote; - - const noteInB = await resolveRemoteNote('a.test', note.id, bob); - assert(noteInB.files != null); - strictEqual(noteInB.files.length, 1); - strictEqual(noteInB.files[0].isSensitive, true); - }); - }); - - /** @see https://github.com/misskey-dev/misskey/issues/12208 */ - describe('isSensitive is federated in replying', () => { - let alice: LoginUser, bob: LoginUser; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - }); - - test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => { - const bobNote = (await bob.client.request('notes/create', { text: 'I\'m Bob' })).createdNote; - - const file = await uploadFile('a.test', alice); - await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true }); - const bobNoteInA = await resolveRemoteNote('b.test', bobNote.id, alice); - const note = (await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id], replyId: bobNoteInA.id })).createdNote; - await sleep(); - - const noteInB = await resolveRemoteNote('a.test', note.id, bob); - assert(noteInB.files != null); - strictEqual(noteInB.files.length, 1); - strictEqual(noteInB.files[0].isSensitive, true); - }); - }); - }); -}); diff --git a/packages/backend/test-federation/test/emoji.test.ts b/packages/backend/test-federation/test/emoji.test.ts deleted file mode 100644 index 3119ca6e4d..0000000000 --- a/packages/backend/test-federation/test/emoji.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import assert, { deepStrictEqual, strictEqual } from 'assert'; -import * as Misskey from 'misskey-js'; -import { addCustomEmoji, createAccount, type LoginUser, resolveRemoteUser, sleep } from './utils.js'; - -describe('Emoji', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - - await bob.client.request('following/create', { userId: aliceInB.id }); - await sleep(); - }); - - test('Custom emoji are delivered with Note delivery', async () => { - const emoji = await addCustomEmoji('a.test'); - await alice.client.request('notes/create', { text: `I love :${emoji.name}:` }); - await sleep(); - - const notes = await bob.client.request('notes/timeline', {}); - const noteInB = notes[0]; - - strictEqual(noteInB.text, `I love \u200b:${emoji.name}:\u200b`); - assert(noteInB.emojis != null); - assert(emoji.name in noteInB.emojis); - strictEqual(noteInB.emojis[emoji.name], emoji.url); - }); - - test('Custom emoji are delivered with Reaction delivery', async () => { - const emoji = await addCustomEmoji('a.test'); - const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote; - await sleep(); - - await alice.client.request('notes/reactions/create', { noteId: note.id, reaction: `:${emoji.name}:` }); - await sleep(); - - const noteInB = (await bob.client.request('notes/timeline', {}))[0]; - deepStrictEqual(noteInB.reactions[`:${emoji.name}@a.test:`], 1); - deepStrictEqual(noteInB.reactionEmojis[`${emoji.name}@a.test`], emoji.url); - }); - - test('Custom emoji are delivered with Profile delivery', async () => { - const emoji = await addCustomEmoji('a.test'); - const renewedAlice = await alice.client.request('i/update', { name: `:${emoji.name}:` }); - await sleep(); - - const renewedaliceInB = await bob.client.request('users/show', { userId: aliceInB.id }); - strictEqual(renewedaliceInB.name, renewedAlice.name); - assert(emoji.name in renewedaliceInB.emojis); - strictEqual(renewedaliceInB.emojis[emoji.name], emoji.url); - }); - - test('Local-only custom emoji aren\'t delivered with Note delivery', async () => { - const emoji = await addCustomEmoji('a.test', { localOnly: true }); - await alice.client.request('notes/create', { text: `I love :${emoji.name}:` }); - await sleep(); - - const notes = await bob.client.request('notes/timeline', {}); - const noteInB = notes[0]; - - strictEqual(noteInB.text, `I love \u200b:${emoji.name}:\u200b`); - // deepStrictEqual(noteInB.emojis, {}); // TODO: this fails (why?) - deepStrictEqual({ ...noteInB.emojis }, {}); - }); - - test('Local-only custom emoji aren\'t delivered with Reaction delivery', async () => { - const emoji = await addCustomEmoji('a.test', { localOnly: true }); - const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote; - await sleep(); - - await alice.client.request('notes/reactions/create', { noteId: note.id, reaction: `:${emoji.name}:` }); - await sleep(); - - const noteInB = (await bob.client.request('notes/timeline', {}))[0]; - deepStrictEqual({ ...noteInB.reactions }, { '❤': 1 }); - deepStrictEqual({ ...noteInB.reactionEmojis }, {}); - }); - - test('Local-only custom emoji aren\'t delivered with Profile delivery', async () => { - const emoji = await addCustomEmoji('a.test', { localOnly: true }); - const renewedAlice = await alice.client.request('i/update', { name: `:${emoji.name}:` }); - await sleep(); - - const renewedaliceInB = await bob.client.request('users/show', { userId: aliceInB.id }); - strictEqual(renewedaliceInB.name, renewedAlice.name); - deepStrictEqual({ ...renewedaliceInB.emojis }, {}); - }); -}); diff --git a/packages/backend/test-federation/test/move.test.ts b/packages/backend/test-federation/test/move.test.ts deleted file mode 100644 index 56a57de8a4..0000000000 --- a/packages/backend/test-federation/test/move.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import assert, { strictEqual } from 'node:assert'; -import { createAccount, type LoginUser, sleep } from './utils.js'; - -describe('Move', () => { - test('Minimum move', async () => { - const [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - await bob.client.request('i/update', { alsoKnownAs: [`@${alice.username}@a.test`] }); - await alice.client.request('i/move', { moveToAccount: `@${bob.username}@b.test` }); - }); - - /** @see https://github.com/misskey-dev/misskey/issues/11320 */ - describe('Following relation is transferred after move', () => { - let alice: LoginUser, bob: LoginUser, carol: LoginUser; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - carol = await createAccount('a.test'); - - // Follow @carol@a.test ==> @alice@a.test - await carol.client.request('following/create', { userId: alice.id }); - - // Move @alice@a.test ==> @bob@b.test - await bob.client.request('i/update', { alsoKnownAs: [`@${alice.username}@a.test`] }); - await alice.client.request('i/move', { moveToAccount: `@${bob.username}@b.test` }); - await sleep(); - }); - - test('Check from follower', async () => { - const following = await carol.client.request('users/following', { userId: carol.id }); - strictEqual(following.length, 2); - const followees = following.map(({ followee }) => followee); - assert(followees.every(followee => followee != null)); - assert(followees.some(({ id, url }) => id === alice.id && url === null)); - assert(followees.some(({ url }) => url === `https://b.test/@${bob.username}`)); - }); - - test('Check from followee', async () => { - const followers = await bob.client.request('users/followers', { userId: bob.id }); - strictEqual(followers.length, 1); - const follower = followers[0].follower; - assert(follower != null); - strictEqual(follower.url, `https://a.test/@${carol.username}`); - }); - }); -}); diff --git a/packages/backend/test-federation/test/note.test.ts b/packages/backend/test-federation/test/note.test.ts deleted file mode 100644 index 1584f9587e..0000000000 --- a/packages/backend/test-federation/test/note.test.ts +++ /dev/null @@ -1,383 +0,0 @@ -import assert, { rejects, strictEqual } from 'node:assert'; -import * as Misskey from 'misskey-js'; -import { addCustomEmoji, createAccount, createModerator, deepStrictEqualWithExcludedFields, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js'; - -describe('Note', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - }); - - describe('Note content', () => { - test('Consistency of Public Note', async () => { - const image = await uploadFile('a.test', alice); - const note = (await alice.client.request('notes/create', { - text: 'I am Alice!', - fileIds: [image.id], - poll: { - choices: ['neko', 'inu'], - multiple: false, - expiredAfter: 60 * 60 * 1000, - }, - })).createdNote; - - const resolvedNote = await resolveRemoteNote('a.test', note.id, bob); - deepStrictEqualWithExcludedFields(note, resolvedNote, [ - 'id', - 'emojis', - /** Consistency of files is checked at {@link file://./drive.test.ts}, so let's skip. */ - 'fileIds', - 'files', - /** @see https://github.com/misskey-dev/misskey/issues/12409 */ - 'reactionAcceptance', - 'userId', - 'user', - 'uri', - ]); - strictEqual(aliceInB.id, resolvedNote.userId); - }); - - test('Consistency of reply', async () => { - const _replyedNote = (await alice.client.request('notes/create', { - text: 'a', - })).createdNote; - const note = (await alice.client.request('notes/create', { - text: 'b', - replyId: _replyedNote.id, - })).createdNote; - // NOTE: the repliedCount is incremented, so fetch again - const replyedNote = await alice.client.request('notes/show', { noteId: _replyedNote.id }); - strictEqual(replyedNote.repliesCount, 1); - - const resolvedNote = await resolveRemoteNote('a.test', note.id, bob); - deepStrictEqualWithExcludedFields(note, resolvedNote, [ - 'id', - 'emojis', - 'reactionAcceptance', - 'replyId', - 'reply', - 'userId', - 'user', - 'uri', - ]); - assert(resolvedNote.replyId != null); - assert(resolvedNote.reply != null); - deepStrictEqualWithExcludedFields(replyedNote, resolvedNote.reply, [ - 'id', - // TODO: why clippedCount loses consistency? - 'clippedCount', - 'emojis', - 'userId', - 'user', - 'uri', - // flaky because this is parallelly incremented, so let's check it below - 'repliesCount', - ]); - strictEqual(aliceInB.id, resolvedNote.userId); - - await sleep(); - - const resolvedReplyedNote = await bob.client.request('notes/show', { noteId: resolvedNote.replyId }); - strictEqual(resolvedReplyedNote.repliesCount, 1); - }); - - test('Consistency of Renote', async () => { - // NOTE: the renoteCount is not incremented, so no need to fetch again - const renotedNote = (await alice.client.request('notes/create', { - text: 'a', - })).createdNote; - const note = (await alice.client.request('notes/create', { - text: 'b', - renoteId: renotedNote.id, - })).createdNote; - - const resolvedNote = await resolveRemoteNote('a.test', note.id, bob); - deepStrictEqualWithExcludedFields(note, resolvedNote, [ - 'id', - 'emojis', - 'reactionAcceptance', - 'renoteId', - 'renote', - 'userId', - 'user', - 'uri', - ]); - assert(resolvedNote.renoteId != null); - assert(resolvedNote.renote != null); - deepStrictEqualWithExcludedFields(renotedNote, resolvedNote.renote, [ - 'id', - 'emojis', - 'userId', - 'user', - 'uri', - ]); - strictEqual(aliceInB.id, resolvedNote.userId); - }); - }); - - describe('Other props', () => { - test('localOnly', async () => { - const note = (await alice.client.request('notes/create', { text: 'a', localOnly: true })).createdNote; - rejects( - async () => await bob.client.request('ap/show', { uri: `https://a.test/notes/${note.id}` }), - (err: any) => { - strictEqual(err.code, 'REQUEST_FAILED'); - return true; - }, - ); - }); - }); - - describe('Deletion', () => { - describe('Check Delete is delivered', () => { - describe('To followers', () => { - let carol: LoginUser; - - beforeAll(async () => { - carol = await createAccount('a.test'); - - await carol.client.request('following/create', { userId: bobInA.id }); - await sleep(); - }); - - test('Check', async () => { - const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote; - const noteInA = await resolveRemoteNote('b.test', note.id, carol); - await bob.client.request('notes/delete', { noteId: note.id }); - await sleep(); - - await rejects( - async () => await carol.client.request('notes/show', { noteId: noteInA.id }), - (err: any) => { - strictEqual(err.code, 'NO_SUCH_NOTE'); - return true; - }, - ); - }); - - afterAll(async () => { - await carol.client.request('following/delete', { userId: bobInA.id }); - await sleep(); - }); - }); - - describe('To renoted and not followed user', () => { - test('Check', async () => { - const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote; - const noteInA = await resolveRemoteNote('b.test', note.id, alice); - await alice.client.request('notes/create', { renoteId: noteInA.id }); - await sleep(); - - await bob.client.request('notes/delete', { noteId: note.id }); - await sleep(); - - await rejects( - async () => await alice.client.request('notes/show', { noteId: noteInA.id }), - (err: any) => { - strictEqual(err.code, 'NO_SUCH_NOTE'); - return true; - }, - ); - }); - }); - - describe('To replied and not followed user', () => { - test('Check', async () => { - const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote; - const noteInA = await resolveRemoteNote('b.test', note.id, alice); - await alice.client.request('notes/create', { text: 'Hello Bob!', replyId: noteInA.id }); - await sleep(); - - await bob.client.request('notes/delete', { noteId: note.id }); - await sleep(); - - await rejects( - async () => await alice.client.request('notes/show', { noteId: noteInA.id }), - (err: any) => { - strictEqual(err.code, 'NO_SUCH_NOTE'); - return true; - }, - ); - }); - }); - - /** - * FIXME: not delivered - * @see https://github.com/misskey-dev/misskey/issues/15548 - */ - describe('To only resolved and not followed user', () => { - test.failing('Check', async () => { - const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote; - const noteInA = await resolveRemoteNote('b.test', note.id, alice); - await sleep(); - - await bob.client.request('notes/delete', { noteId: note.id }); - await sleep(); - - await rejects( - async () => await alice.client.request('notes/show', { noteId: noteInA.id }), - (err: any) => { - strictEqual(err.code, 'NO_SUCH_NOTE'); - return true; - }, - ); - }); - }); - }); - - describe('Deletion of remote user\'s note for moderation', () => { - let note: Misskey.entities.Note; - - test('Alice post is deleted in B', async () => { - note = (await alice.client.request('notes/create', { text: 'Hello' })).createdNote; - const noteInB = await resolveRemoteNote('a.test', note.id, bob); - const bMod = await createModerator('b.test'); - await bMod.client.request('notes/delete', { noteId: noteInB.id }); - await rejects( - async () => await bob.client.request('notes/show', { noteId: noteInB.id }), - (err: any) => { - strictEqual(err.code, 'NO_SUCH_NOTE'); - return true; - }, - ); - }); - - /** - * FIXME: implement soft deletion as well as user? - * @see https://github.com/misskey-dev/misskey/issues/11437 - */ - test.failing('Not found even if resolve again', async () => { - const noteInB = await resolveRemoteNote('a.test', note.id, bob); - await rejects( - async () => await bob.client.request('notes/show', { noteId: noteInB.id }), - (err: any) => { - strictEqual(err.code, 'NO_SUCH_NOTE'); - return true; - }, - ); - }); - }); - }); - - describe('Reaction', () => { - describe('Consistency', () => { - test('Unicode reaction', async () => { - const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote; - const resolvedNote = await resolveRemoteNote('a.test', note.id, bob); - const reaction = '😅'; - await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction }); - await sleep(); - - const reactions = await alice.client.request('notes/reactions', { noteId: note.id }); - strictEqual(reactions.length, 1); - strictEqual(reactions[0].type, reaction); - strictEqual(reactions[0].user.id, bobInA.id); - }); - - test('Custom emoji reaction', async () => { - const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote; - const resolvedNote = await resolveRemoteNote('a.test', note.id, bob); - const emoji = await addCustomEmoji('b.test'); - await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: `:${emoji.name}:` }); - await sleep(); - - const reactions = await alice.client.request('notes/reactions', { noteId: note.id }); - strictEqual(reactions.length, 1); - strictEqual(reactions[0].type, `:${emoji.name}@b.test:`); - strictEqual(reactions[0].user.id, bobInA.id); - }); - }); - - describe('Acceptance', () => { - test('Even if likeOnly, remote users can react with custom emoji, but it is converted to like', async () => { - const note = (await alice.client.request('notes/create', { text: 'a', reactionAcceptance: 'likeOnly' })).createdNote; - const noteInB = await resolveRemoteNote('a.test', note.id, bob); - const emoji = await addCustomEmoji('b.test'); - await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction: `:${emoji.name}:` }); - await sleep(); - - const reactions = await alice.client.request('notes/reactions', { noteId: note.id }); - strictEqual(reactions.length, 1); - strictEqual(reactions[0].type, '❤'); - }); - - /** - * TODO: this may be unexpected behavior? - * @see https://github.com/misskey-dev/misskey/issues/12409 - */ - test('Even if nonSensitiveOnly, remote users can react with sensitive emoji, and it is not converted', async () => { - const note = (await alice.client.request('notes/create', { text: 'a', reactionAcceptance: 'nonSensitiveOnly' })).createdNote; - const noteInB = await resolveRemoteNote('a.test', note.id, bob); - const emoji = await addCustomEmoji('b.test', { isSensitive: true }); - await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction: `:${emoji.name}:` }); - await sleep(); - - const reactions = await alice.client.request('notes/reactions', { noteId: note.id }); - strictEqual(reactions.length, 1); - strictEqual(reactions[0].type, `:${emoji.name}@b.test:`); - }); - }); - }); - - describe('Poll', () => { - describe('Any remote user\'s vote is delivered to the author', () => { - let carol: LoginUser; - - beforeAll(async () => { - carol = await createAccount('a.test'); - }); - - test('Bob creates poll and receives a vote from Carol', async () => { - const note = (await bob.client.request('notes/create', { poll: { choices: ['inu', 'neko'] } })).createdNote; - const noteInA = await resolveRemoteNote('b.test', note.id, carol); - await carol.client.request('notes/polls/vote', { noteId: noteInA.id, choice: 0 }); - await sleep(); - - const noteAfterVote = await bob.client.request('notes/show', { noteId: note.id }); - assert(noteAfterVote.poll != null); - strictEqual(noteAfterVote.poll.choices[0].votes, 1); - strictEqual(noteAfterVote.poll.choices[1].votes, 0); - }); - }); - - describe('Local user\'s vote is delivered to the author\'s remote followers', () => { - let bobRemoteFollower: LoginUser, localVoter: LoginUser; - - beforeAll(async () => { - [ - bobRemoteFollower, - localVoter, - ] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - await bobRemoteFollower.client.request('following/create', { userId: bobInA.id }); - await sleep(); - }); - - test('A vote in Bob\'s server is delivered to Bob\'s remote followers', async () => { - const note = (await bob.client.request('notes/create', { poll: { choices: ['inu', 'neko'] } })).createdNote; - // NOTE: resolve before voting - const noteInA = await resolveRemoteNote('b.test', note.id, bobRemoteFollower); - await localVoter.client.request('notes/polls/vote', { noteId: note.id, choice: 0 }); - await sleep(); - - const noteAfterVote = await bobRemoteFollower.client.request('notes/show', { noteId: noteInA.id }); - assert(noteAfterVote.poll != null); - strictEqual(noteAfterVote.poll.choices[0].votes, 1); - strictEqual(noteAfterVote.poll.choices[1].votes, 0); - }); - }); - }); -}); diff --git a/packages/backend/test-federation/test/notification.test.ts b/packages/backend/test-federation/test/notification.test.ts deleted file mode 100644 index 6d55353653..0000000000 --- a/packages/backend/test-federation/test/notification.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import * as Misskey from 'misskey-js'; -import { assertNotificationReceived, createAccount, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js'; - -describe('Notification', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - }); - - describe('Follow', () => { - test('Get notification when follow', async () => { - await assertNotificationReceived( - 'b.test', bob, - async () => await bob.client.request('following/create', { userId: aliceInB.id }), - notification => notification.type === 'followRequestAccepted' && notification.userId === aliceInB.id, - true, - ); - - await bob.client.request('following/delete', { userId: aliceInB.id }); - await sleep(); - }); - - test('Get notification when get followed', async () => { - await assertNotificationReceived( - 'a.test', alice, - async () => await bob.client.request('following/create', { userId: aliceInB.id }), - notification => notification.type === 'follow' && notification.userId === bobInA.id, - true, - ); - }); - - afterAll(async () => await bob.client.request('following/delete', { userId: aliceInB.id })); - }); - - describe('Note', () => { - test('Get notification when get a reaction', async () => { - const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote; - const noteInB = await resolveRemoteNote('a.test', note.id, bob); - const reaction = '😅'; - await assertNotificationReceived( - 'a.test', alice, - async () => await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction }), - notification => - notification.type === 'reaction' && notification.note.id === note.id && notification.userId === bobInA.id && notification.reaction === reaction, - true, - ); - }); - - test('Get notification when replied', async () => { - const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote; - const noteInB = await resolveRemoteNote('a.test', note.id, bob); - const text = crypto.randomUUID(); - await assertNotificationReceived( - 'a.test', alice, - async () => await bob.client.request('notes/create', { text, replyId: noteInB.id }), - notification => - notification.type === 'reply' && notification.note.reply!.id === note.id && notification.userId === bobInA.id && notification.note.text === text, - true, - ); - }); - - test('Get notification when renoted', async () => { - const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote; - const noteInB = await resolveRemoteNote('a.test', note.id, bob); - await assertNotificationReceived( - 'a.test', alice, - async () => await bob.client.request('notes/create', { renoteId: noteInB.id }), - notification => - notification.type === 'renote' && notification.note.renote!.id === note.id && notification.userId === bobInA.id, - true, - ); - }); - - test('Get notification when quoted', async () => { - const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote; - const noteInB = await resolveRemoteNote('a.test', note.id, bob); - const text = crypto.randomUUID(); - await assertNotificationReceived( - 'a.test', alice, - async () => await bob.client.request('notes/create', { text, renoteId: noteInB.id }), - notification => - notification.type === 'quote' && notification.note.renote!.id === note.id && notification.userId === bobInA.id && notification.note.text === text, - true, - ); - }); - - test('Get notification when mentioned', async () => { - const text = `@${alice.username}@a.test`; - await assertNotificationReceived( - 'a.test', alice, - async () => await bob.client.request('notes/create', { text }), - notification => notification.type === 'mention' && notification.userId === bobInA.id && notification.note.text === text, - true, - ); - }); - }); -}); diff --git a/packages/backend/test-federation/test/timeline.test.ts b/packages/backend/test-federation/test/timeline.test.ts deleted file mode 100644 index 00635e654b..0000000000 --- a/packages/backend/test-federation/test/timeline.test.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { strictEqual } from 'assert'; -import * as Misskey from 'misskey-js'; -import { createAccount, fetchAdmin, isNoteUpdatedEventFired, isFired, type LoginUser, type Request, resolveRemoteUser, sleep, createRole } from './utils.js'; - -const bAdmin = await fetchAdmin('b.test'); - -describe('Timeline', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - - await bob.client.request('following/create', { userId: aliceInB.id }); - await sleep(); - }); - - type TimelineChannel = keyof Misskey.Channels & (`${string}Timeline` | 'antenna' | 'userList' | 'hashtag'); - type TimelineEndpoint = keyof Misskey.Endpoints & (`notes/${string}timeline` | 'antennas/notes' | 'roles/notes' | 'notes/search-by-tag'); - const timelineMap = new Map([ - ['antenna', 'antennas/notes'], - ['globalTimeline', 'notes/global-timeline'], - ['homeTimeline', 'notes/timeline'], - ['hybridTimeline', 'notes/hybrid-timeline'], - ['localTimeline', 'notes/local-timeline'], - ['roleTimeline', 'roles/notes'], - ['hashtag', 'notes/search-by-tag'], - ['userList', 'notes/user-list-timeline'], - ]); - - async function postAndCheckReception( - timelineChannel: C, - expect: boolean, - noteParams: Misskey.entities.NotesCreateRequest = {}, - channelParams: Misskey.Channels[C]['params'] = {}, - ) { - let note: Misskey.entities.Note | undefined; - const text = noteParams.text ?? crypto.randomUUID(); - const streamingFired = await isFired( - 'b.test', bob, timelineChannel, - async () => { - note = (await alice.client.request('notes/create', { text, ...noteParams })).createdNote; - }, - 'note', msg => msg.text === text, - channelParams, - ); - strictEqual(streamingFired, expect); - - const endpoint = timelineMap.get(timelineChannel)!; - const params: Misskey.Endpoints[typeof endpoint]['req'] = - endpoint === 'antennas/notes' ? { antennaId: (channelParams as Misskey.Channels['antenna']['params']).antennaId } : - endpoint === 'notes/user-list-timeline' ? { listId: (channelParams as Misskey.Channels['userList']['params']).listId } : - endpoint === 'notes/search-by-tag' ? { query: (channelParams as Misskey.Channels['hashtag']['params']).q } : - endpoint === 'roles/notes' ? { roleId: (channelParams as Misskey.Channels['roleTimeline']['params']).roleId } : - {}; - - await sleep(); - const notes = await (bob.client.request as Request)(endpoint, params); - const noteInB = notes.filter(({ uri }) => uri === `https://a.test/notes/${note!.id}`).pop(); - const endpointFired = noteInB != null; - strictEqual(endpointFired, expect); - - // Let's check Delete reception - if (expect) { - const streamingFired = await isNoteUpdatedEventFired( - 'b.test', bob, noteInB!.id, - async () => await alice.client.request('notes/delete', { noteId: note!.id }), - msg => msg.type === 'deleted' && msg.id === noteInB!.id, - ); - strictEqual(streamingFired, true); - - await sleep(); - const notes = await (bob.client.request as Request)(endpoint, params); - const endpointFired = notes.every(({ uri }) => uri !== `https://a.test/notes/${note!.id}`); - strictEqual(endpointFired, true); - } - } - - describe('homeTimeline', () => { - // NOTE: narrowing scope intentionally to prevent mistakes by copy-and-paste - const homeTimeline = 'homeTimeline'; - - describe('Check reception of remote followee\'s Note', () => { - test('Receive remote followee\'s Note', async () => { - await postAndCheckReception(homeTimeline, true); - }); - - test('Receive remote followee\'s home-only Note', async () => { - await postAndCheckReception(homeTimeline, true, { visibility: 'home' }); - }); - - test('Receive remote followee\'s followers-only Note', async () => { - await postAndCheckReception(homeTimeline, true, { visibility: 'followers' }); - }); - - test('Receive remote followee\'s visible specified-only Note', async () => { - await postAndCheckReception(homeTimeline, true, { visibility: 'specified', visibleUserIds: [bobInA.id] }); - }); - - test('Don\'t receive remote followee\'s localOnly Note', async () => { - await postAndCheckReception(homeTimeline, false, { localOnly: true }); - }); - - test('Don\'t receive remote followee\'s invisible specified-only Note', async () => { - await postAndCheckReception(homeTimeline, false, { visibility: 'specified' }); - }); - - /** - * FIXME: can receive this - * @see https://github.com/misskey-dev/misskey/issues/14083 - */ - test.failing('Don\'t receive remote followee\'s invisible and mentioned specified-only Note', async () => { - await postAndCheckReception(homeTimeline, false, { text: `@${bob.username}@b.test Hello`, visibility: 'specified' }); - }); - - /** - * FIXME: cannot receive this - * @see https://github.com/misskey-dev/misskey/issues/14084 - */ - test.failing('Receive remote followee\'s visible specified-only reply to invisible specified-only Note', async () => { - const note = (await alice.client.request('notes/create', { text: 'a', visibility: 'specified' })).createdNote; - await postAndCheckReception(homeTimeline, true, { replyId: note.id, visibility: 'specified', visibleUserIds: [bobInA.id] }); - }); - }); - }); - - describe('localTimeline', () => { - const localTimeline = 'localTimeline'; - - describe('Check reception of remote followee\'s Note', () => { - test('Don\'t receive remote followee\'s Note', async () => { - await postAndCheckReception(localTimeline, false); - }); - }); - }); - - describe('hybridTimeline', () => { - const hybridTimeline = 'hybridTimeline'; - - describe('Check reception of remote followee\'s Note', () => { - test('Receive remote followee\'s Note', async () => { - await postAndCheckReception(hybridTimeline, true); - }); - - test('Receive remote followee\'s home-only Note', async () => { - await postAndCheckReception(hybridTimeline, true, { visibility: 'home' }); - }); - - test('Receive remote followee\'s followers-only Note', async () => { - await postAndCheckReception(hybridTimeline, true, { visibility: 'followers' }); - }); - - test('Receive remote followee\'s visible specified-only Note', async () => { - await postAndCheckReception(hybridTimeline, true, { visibility: 'specified', visibleUserIds: [bobInA.id] }); - }); - }); - }); - - describe('globalTimeline', () => { - const globalTimeline = 'globalTimeline'; - - describe('Check reception of remote followee\'s Note', () => { - test('Receive remote followee\'s Note', async () => { - await postAndCheckReception(globalTimeline, true); - }); - - test('Don\'t receive remote followee\'s home-only Note', async () => { - await postAndCheckReception(globalTimeline, false, { visibility: 'home' }); - }); - - test('Don\'t receive remote followee\'s followers-only Note', async () => { - await postAndCheckReception(globalTimeline, false, { visibility: 'followers' }); - }); - - test('Don\'t receive remote followee\'s visible specified-only Note', async () => { - await postAndCheckReception(globalTimeline, false, { visibility: 'specified', visibleUserIds: [bobInA.id] }); - }); - }); - }); - - describe('userList', () => { - const userList = 'userList'; - - let list: Misskey.entities.UserList; - - beforeAll(async () => { - list = await bob.client.request('users/lists/create', { name: 'Bob\'s List' }); - await bob.client.request('users/lists/push', { listId: list.id, userId: aliceInB.id }); - await sleep(); - }); - - describe('Check reception of remote followee\'s Note', () => { - test('Receive remote followee\'s Note', async () => { - await postAndCheckReception(userList, true, {}, { listId: list.id }); - }); - - test('Receive remote followee\'s home-only Note', async () => { - await postAndCheckReception(userList, true, { visibility: 'home' }, { listId: list.id }); - }); - - test('Receive remote followee\'s followers-only Note', async () => { - await postAndCheckReception(userList, true, { visibility: 'followers' }, { listId: list.id }); - }); - - test('Receive remote followee\'s visible specified-only Note', async () => { - await postAndCheckReception(userList, true, { visibility: 'specified', visibleUserIds: [bobInA.id] }, { listId: list.id }); - }); - }); - }); - - describe('hashtag', () => { - const hashtag = 'hashtag'; - - describe('Check reception of remote followee\'s Note', () => { - test('Receive remote followee\'s Note', async () => { - const tag = crypto.randomUUID(); - await postAndCheckReception(hashtag, true, { text: `#${tag}` }, { q: [[tag]] }); - }); - - test('Receive remote followee\'s home-only Note', async () => { - const tag = crypto.randomUUID(); - await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'home' }, { q: [[tag]] }); - }); - - test('Receive remote followee\'s followers-only Note', async () => { - const tag = crypto.randomUUID(); - await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'followers' }, { q: [[tag]] }); - }); - - test('Receive remote followee\'s visible specified-only Note', async () => { - const tag = crypto.randomUUID(); - await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'specified', visibleUserIds: [bobInA.id] }, { q: [[tag]] }); - }); - }); - }); - - describe('roleTimeline', () => { - const roleTimeline = 'roleTimeline'; - - let role: Misskey.entities.Role; - - beforeAll(async () => { - role = await createRole('b.test', { - name: 'Remote Users', - description: 'Remote users are assigned to this role.', - condFormula: { - /** TODO: @see https://github.com/misskey-dev/misskey/issues/14169 */ - type: 'isRemote' as never, - }, - }); - await sleep(); - }); - - describe('Check reception of remote followee\'s Note', () => { - test('Receive remote followee\'s Note', async () => { - await postAndCheckReception(roleTimeline, true, {}, { roleId: role.id }); - }); - - test('Don\'t receive remote followee\'s home-only Note', async () => { - await postAndCheckReception(roleTimeline, false, { visibility: 'home' }, { roleId: role.id }); - }); - - test('Don\'t receive remote followee\'s followers-only Note', async () => { - await postAndCheckReception(roleTimeline, false, { visibility: 'followers' }, { roleId: role.id }); - }); - - test('Don\'t receive remote followee\'s visible specified-only Note', async () => { - await postAndCheckReception(roleTimeline, false, { visibility: 'specified', visibleUserIds: [bobInA.id] }, { roleId: role.id }); - }); - }); - - afterAll(async () => { - await bAdmin.client.request('admin/roles/delete', { roleId: role.id }); - }); - }); - - // TODO: Cannot test - describe.skip('antenna', () => { - const antenna = 'antenna'; - - let bobAntenna: Misskey.entities.Antenna; - - beforeAll(async () => { - bobAntenna = await bob.client.request('antennas/create', { - name: 'Bob\'s Egosurfing Antenna', - src: 'all', - keywords: [['Bob']], - excludeKeywords: [], - users: [], - caseSensitive: false, - localOnly: false, - withReplies: true, - withFile: true, - }); - await sleep(); - }); - - describe('Check reception of remote followee\'s Note', () => { - test('Receive remote followee\'s Note', async () => { - await postAndCheckReception(antenna, true, { text: 'I love Bob (1)' }, { antennaId: bobAntenna.id }); - }); - - test('Don\'t receive remote followee\'s home-only Note', async () => { - await postAndCheckReception(antenna, false, { text: 'I love Bob (2)', visibility: 'home' }, { antennaId: bobAntenna.id }); - }); - - test('Don\'t receive remote followee\'s followers-only Note', async () => { - await postAndCheckReception(antenna, false, { text: 'I love Bob (3)', visibility: 'followers' }, { antennaId: bobAntenna.id }); - }); - - test('Don\'t receive remote followee\'s visible specified-only Note', async () => { - await postAndCheckReception(antenna, false, { text: 'I love Bob (4)', visibility: 'specified', visibleUserIds: [bobInA.id] }, { antennaId: bobAntenna.id }); - }); - }); - - afterAll(async () => { - await bob.client.request('antennas/delete', { antennaId: bobAntenna.id }); - }); - }); -}); diff --git a/packages/backend/test-federation/test/user.test.ts b/packages/backend/test-federation/test/user.test.ts deleted file mode 100644 index ee69e857bc..0000000000 --- a/packages/backend/test-federation/test/user.test.ts +++ /dev/null @@ -1,565 +0,0 @@ -import assert, { rejects, strictEqual } from 'node:assert'; -import * as Misskey from 'misskey-js'; -import { createAccount, deepStrictEqualWithExcludedFields, fetchAdmin, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js'; - -const [aAdmin, bAdmin] = await Promise.all([ - fetchAdmin('a.test'), - fetchAdmin('b.test'), -]); - -describe('User', () => { - describe('Profile', () => { - describe('Consistency of profile', () => { - let alice: LoginUser; - let aliceWatcher: LoginUser; - let aliceWatcherInB: LoginUser; - - beforeAll(async () => { - alice = await createAccount('a.test'); - [ - aliceWatcher, - aliceWatcherInB, - ] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - }); - - test('Check consistency', async () => { - const aliceInA = await aliceWatcher.client.request('users/show', { userId: alice.id }); - const resolved = await resolveRemoteUser('a.test', aliceInA.id, aliceWatcherInB); - const aliceInB = await aliceWatcherInB.client.request('users/show', { userId: resolved.id }); - - // console.log(`a.test: ${JSON.stringify(aliceInA, null, '\t')}`); - // console.log(`b.test: ${JSON.stringify(aliceInB, null, '\t')}`); - - deepStrictEqualWithExcludedFields(aliceInA, aliceInB, [ - 'id', - 'host', - 'avatarUrl', - 'avatarBlurhash', - 'instance', - 'badgeRoles', - 'url', - 'uri', - 'createdAt', - 'lastFetchedAt', - 'publicReactions', - ]); - }); - }); - - describe('ffVisibility is federated', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - - // NOTE: follow each other - await Promise.all([ - alice.client.request('following/create', { userId: bobInA.id }), - bob.client.request('following/create', { userId: aliceInB.id }), - ]); - await sleep(); - }); - - test('Visibility set public by default', async () => { - for (const user of await Promise.all([ - alice.client.request('users/show', { userId: bobInA.id }), - bob.client.request('users/show', { userId: aliceInB.id }), - ])) { - strictEqual(user.followersVisibility, 'public'); - strictEqual(user.followingVisibility, 'public'); - } - }); - - /** FIXME: not working */ - test.skip('Setting private for followersVisibility is federated', async () => { - await Promise.all([ - alice.client.request('i/update', { followersVisibility: 'private' }), - bob.client.request('i/update', { followersVisibility: 'private' }), - ]); - await sleep(); - - for (const user of await Promise.all([ - alice.client.request('users/show', { userId: bobInA.id }), - bob.client.request('users/show', { userId: aliceInB.id }), - ])) { - strictEqual(user.followersVisibility, 'private'); - strictEqual(user.followingVisibility, 'public'); - } - }); - - test.skip('Setting private for followingVisibility is federated', async () => { - await Promise.all([ - alice.client.request('i/update', { followingVisibility: 'private' }), - bob.client.request('i/update', { followingVisibility: 'private' }), - ]); - await sleep(); - - for (const user of await Promise.all([ - alice.client.request('users/show', { userId: bobInA.id }), - bob.client.request('users/show', { userId: aliceInB.id }), - ])) { - strictEqual(user.followersVisibility, 'private'); - strictEqual(user.followingVisibility, 'private'); - } - }); - }); - - describe('isCat is federated', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - }); - - test('Not isCat for default', () => { - strictEqual(aliceInB.isCat, false); - }); - - test('Becoming a cat is sent to their followers', async () => { - await bob.client.request('following/create', { userId: aliceInB.id }); - await sleep(); - - await alice.client.request('i/update', { isCat: true }); - await sleep(); - - const res = await bob.client.request('users/show', { userId: aliceInB.id }); - strictEqual(res.isCat, true); - }); - }); - - describe('Pinning Notes', () => { - let alice: LoginUser, bob: LoginUser; - let aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - aliceInB = await resolveRemoteUser('a.test', alice.id, bob); - - await bob.client.request('following/create', { userId: aliceInB.id }); - }); - - test('Pinning localOnly Note is not delivered', async () => { - const note = (await alice.client.request('notes/create', { text: 'a', localOnly: true })).createdNote; - await alice.client.request('i/pin', { noteId: note.id }); - await sleep(); - - const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id }); - strictEqual(_aliceInB.pinnedNoteIds.length, 0); - }); - - test('Pinning followers-only Note is not delivered', async () => { - const note = (await alice.client.request('notes/create', { text: 'a', visibility: 'followers' })).createdNote; - await alice.client.request('i/pin', { noteId: note.id }); - await sleep(); - - const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id }); - strictEqual(_aliceInB.pinnedNoteIds.length, 0); - }); - - let pinnedNote: Misskey.entities.Note; - - test('Pinning normal Note is delivered', async () => { - pinnedNote = (await alice.client.request('notes/create', { text: 'a' })).createdNote; - await alice.client.request('i/pin', { noteId: pinnedNote.id }); - await sleep(); - - const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id }); - strictEqual(_aliceInB.pinnedNoteIds.length, 1); - const pinnedNoteInB = await resolveRemoteNote('a.test', pinnedNote.id, bob); - strictEqual(_aliceInB.pinnedNotes[0].id, pinnedNoteInB.id); - }); - - test('Unpinning normal Note is delivered', async () => { - await alice.client.request('i/unpin', { noteId: pinnedNote.id }); - await sleep(); - - const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id }); - strictEqual(_aliceInB.pinnedNoteIds.length, 0); - }); - }); - }); - - describe('Follow / Unfollow', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - }); - - describe('Follow a.test ==> b.test', () => { - beforeAll(async () => { - await alice.client.request('following/create', { userId: bobInA.id }); - - await sleep(); - }); - - test('Check consistency with `users/following` and `users/followers` endpoints', async () => { - await Promise.all([ - strictEqual( - (await alice.client.request('users/following', { userId: alice.id })) - .some(v => v.followeeId === bobInA.id), - true, - ), - strictEqual( - (await bob.client.request('users/followers', { userId: bob.id })) - .some(v => v.followerId === aliceInB.id), - true, - ), - ]); - }); - }); - - describe('Unfollow a.test ==> b.test', () => { - beforeAll(async () => { - await alice.client.request('following/delete', { userId: bobInA.id }); - - await sleep(); - }); - - test('Check consistency with `users/following` and `users/followers` endpoints', async () => { - await Promise.all([ - strictEqual( - (await alice.client.request('users/following', { userId: alice.id })) - .some(v => v.followeeId === bobInA.id), - false, - ), - strictEqual( - (await bob.client.request('users/followers', { userId: bob.id })) - .some(v => v.followerId === aliceInB.id), - false, - ), - ]); - }); - }); - }); - - describe('Follow requests', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - - await alice.client.request('i/update', { isLocked: true }); - }); - - describe('Send follow request from Bob to Alice and cancel', () => { - describe('Bob sends follow request to Alice', () => { - beforeAll(async () => { - await bob.client.request('following/create', { userId: aliceInB.id }); - await sleep(); - }); - - test('Alice should have a request', async () => { - const requests = await alice.client.request('following/requests/list', {}); - strictEqual(requests.length, 1); - strictEqual(requests[0].followee.id, alice.id); - strictEqual(requests[0].follower.id, bobInA.id); - }); - }); - - describe('Alice cancels it', () => { - beforeAll(async () => { - await bob.client.request('following/requests/cancel', { userId: aliceInB.id }); - await sleep(); - }); - - test('Alice should have no requests', async () => { - const requests = await alice.client.request('following/requests/list', {}); - strictEqual(requests.length, 0); - }); - }); - }); - - describe('Send follow request from Bob to Alice and reject', () => { - beforeAll(async () => { - await bob.client.request('following/create', { userId: aliceInB.id }); - await sleep(); - - await alice.client.request('following/requests/reject', { userId: bobInA.id }); - await sleep(); - }); - - test('Bob should have no requests', async () => { - await rejects( - async () => await bob.client.request('following/requests/cancel', { userId: aliceInB.id }), - (err: any) => { - strictEqual(err.code, 'FOLLOW_REQUEST_NOT_FOUND'); - return true; - }, - ); - }); - - test('Bob doesn\'t follow Alice', async () => { - const following = await bob.client.request('users/following', { userId: bob.id }); - strictEqual(following.length, 0); - }); - }); - - describe('Send follow request from Bob to Alice and accept', () => { - beforeAll(async () => { - await bob.client.request('following/create', { userId: aliceInB.id }); - await sleep(); - - await alice.client.request('following/requests/accept', { userId: bobInA.id }); - await sleep(); - }); - - test('Bob follows Alice', async () => { - const following = await bob.client.request('users/following', { userId: bob.id }); - strictEqual(following.length, 1); - strictEqual(following[0].followeeId, aliceInB.id); - strictEqual(following[0].followerId, bob.id); - }); - }); - }); - - describe('Deletion', () => { - describe('Check Delete consistency', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - }); - - test('Bob follows Alice, and Alice deleted themself', async () => { - await bob.client.request('following/create', { userId: aliceInB.id }); - await sleep(); - - const followers = await alice.client.request('users/followers', { userId: alice.id }); - strictEqual(followers.length, 1); // followed by Bob - - await alice.client.request('i/delete-account', { password: alice.password }); - // NOTE: user deletion query is slow - // FIXME: ensure user is removed successfully - await sleep(10000); - - const following = await bob.client.request('users/following', { userId: bob.id }); - strictEqual(following.length, 0); // no following relation - - await rejects( - async () => await bob.client.request('following/create', { userId: aliceInB.id }), - (err: any) => { - strictEqual(err.code, 'NO_SUCH_USER'); - return true; - }, - ); - }); - }); - - describe('Deletion of remote user for moderation', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - }); - - test('Bob follows Alice, then Alice gets deleted in B server', async () => { - await bob.client.request('following/create', { userId: aliceInB.id }); - await sleep(); - - const followers = await alice.client.request('users/followers', { userId: alice.id }); - strictEqual(followers.length, 1); // followed by Bob - - await bAdmin.client.request('admin/delete-account', { userId: aliceInB.id }); - await sleep(); - - /** - * FIXME: remote account is not deleted! - * @see https://github.com/misskey-dev/misskey/issues/14728 - */ - const deletedAlice = await bob.client.request('users/show', { userId: aliceInB.id }); - assert(deletedAlice.id, aliceInB.id); - - // TODO: why still following relation? - const following = await bob.client.request('users/following', { userId: bob.id }); - strictEqual(following.length, 1); - await rejects( - async () => await bob.client.request('following/create', { userId: aliceInB.id }), - (err: any) => { - strictEqual(err.code, 'ALREADY_FOLLOWING'); - return true; - }, - ); - }); - - test('Alice tries to follow Bob, but it is not processed', async () => { - await alice.client.request('following/create', { userId: bobInA.id }); - await sleep(); - - const following = await alice.client.request('users/following', { userId: alice.id }); - strictEqual(following.length, 0); // Not following Bob because B server doesn't return Accept - - const followers = await bob.client.request('users/followers', { userId: bob.id }); - strictEqual(followers.length, 0); // Alice's Follow is not processed - }); - }); - }); - - describe('Suspension', () => { - describe('Check suspend/unsuspend consistency', () => { - let alice: LoginUser, bob: LoginUser; - let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; - - beforeAll(async () => { - [alice, bob] = await Promise.all([ - createAccount('a.test'), - createAccount('b.test'), - ]); - - [bobInA, aliceInB] = await Promise.all([ - resolveRemoteUser('b.test', bob.id, alice), - resolveRemoteUser('a.test', alice.id, bob), - ]); - }); - - test('Bob follows Alice, and Alice gets suspended, there is no following relation, and Bob fails to follow again', async () => { - await bob.client.request('following/create', { userId: aliceInB.id }); - await sleep(); - - const followers = await alice.client.request('users/followers', { userId: alice.id }); - strictEqual(followers.length, 1); // followed by Bob - - await aAdmin.client.request('admin/suspend-user', { userId: alice.id }); - // NOTE: user deletion query is slow - // FIXME: ensure user is removed successfully - await sleep(10000); - - const following = await bob.client.request('users/following', { userId: bob.id }); - strictEqual(following.length, 0); // no following relation - - await rejects( - async () => await bob.client.request('following/create', { userId: aliceInB.id }), - (err: any) => { - strictEqual(err.code, 'NO_SUCH_USER'); - return true; - }, - ); - }); - - test('Alice gets unsuspended, Bob succeeds in following Alice', async () => { - await aAdmin.client.request('admin/unsuspend-user', { userId: alice.id }); - await sleep(); - - const followers = await alice.client.request('users/followers', { userId: alice.id }); - strictEqual(followers.length, 1); // FIXME: followers are not deleted?? - - /** - * FIXME: still rejected! - * seems to can't process Undo Delete activity because it is not implemented - * related @see https://github.com/misskey-dev/misskey/issues/13273 - */ - await rejects( - async () => await bob.client.request('following/create', { userId: aliceInB.id }), - (err: any) => { - strictEqual(err.code, 'NO_SUCH_USER'); - return true; - }, - ); - - // FIXME: resolving also fails - await rejects( - async () => await resolveRemoteUser('a.test', alice.id, bob), - (err: any) => { - strictEqual(err.code, 'INTERNAL_ERROR'); - return true; - }, - ); - }); - - /** - * instead of simple unsuspension, let's tell existence by following from Alice - */ - test('Alice can follow Bob', async () => { - await alice.client.request('following/create', { userId: bobInA.id }); - await sleep(); - - const bobFollowers = await bob.client.request('users/followers', { userId: bob.id }); - strictEqual(bobFollowers.length, 1); // followed by Alice - assert(bobFollowers[0].follower != null); - const renewedaliceInB = bobFollowers[0].follower; - assert(aliceInB.username === renewedaliceInB.username); - assert(aliceInB.host === renewedaliceInB.host); - assert(aliceInB.id !== renewedaliceInB.id); // TODO: Same username and host, but their ids are different! Is it OK? - - const following = await bob.client.request('users/following', { userId: bob.id }); - strictEqual(following.length, 0); // following are deleted - - // Bob tries to follow Alice - await bob.client.request('following/create', { userId: renewedaliceInB.id }); - await sleep(); - - const aliceFollowers = await alice.client.request('users/followers', { userId: alice.id }); - strictEqual(aliceFollowers.length, 1); - - // FIXME: but resolving still fails ... - await rejects( - async () => await resolveRemoteUser('a.test', alice.id, bob), - (err: any) => { - strictEqual(err.code, 'INTERNAL_ERROR'); - return true; - }, - ); - }); - }); - }); -}); diff --git a/packages/backend/test-federation/test/utils.ts b/packages/backend/test-federation/test/utils.ts deleted file mode 100644 index 2779eb7e81..0000000000 --- a/packages/backend/test-federation/test/utils.ts +++ /dev/null @@ -1,307 +0,0 @@ -import { deepStrictEqual, strictEqual } from 'assert'; -import { readFile } from 'fs/promises'; -import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; -import * as Misskey from 'misskey-js'; -import { WebSocket } from 'ws'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -export const ADMIN_PARAMS = { username: 'admin', password: 'admin' }; -const ADMIN_CACHE = new Map(); - -await Promise.all([ - fetchAdmin('a.test'), - fetchAdmin('b.test'), -]); - -type SigninResponse = Omit; - -export type LoginUser = SigninResponse & { - client: Misskey.api.APIClient; - username: string; - password: string; -}; - -/** used for avoiding overload and some endpoints */ -export type Request = < - E extends keyof Misskey.Endpoints, - P extends Misskey.Endpoints[E]['req'], ->( - endpoint: E, - params: P, - credential?: string | null, -) => Promise>; - -type Host = 'a.test' | 'b.test'; - -export async function sleep(ms = 250): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -async function signin( - host: Host, - params: Misskey.entities.SigninFlowRequest, -): Promise { - // wait for a second to prevent hit rate limit - await sleep(1000); - - return await (new Misskey.api.APIClient({ origin: `https://${host}` }).request as Request)('signin-flow', params) - .then(res => { - strictEqual(res.finished, true); - if (params.username === ADMIN_PARAMS.username) ADMIN_CACHE.set(host, res); - return res; - }) - .then(({ id, i }) => ({ id, i })) - .catch(async err => { - if (err.code === 'TOO_MANY_AUTHENTICATION_FAILURES') { - await sleep(Math.random() * 2000); - return await signin(host, params); - } - throw err; - }); -} - -async function createAdmin(host: Host): Promise { - const client = new Misskey.api.APIClient({ origin: `https://${host}` }); - return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => { - ADMIN_CACHE.set(host, { - id: res.id, - // @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this - i: res.token, - }); - return res as Misskey.entities.SignupResponse; - }).then(async res => { - await client.request('admin/roles/update-default-policies', { - policies: { - /** TODO: @see https://github.com/misskey-dev/misskey/issues/14169 */ - rateLimitFactor: 0 as never, - }, - }, res.token); - return res; - }).catch(err => { - if (err.info.e.message === 'access denied') return undefined; - throw err; - }); -} - -export async function fetchAdmin(host: Host): Promise { - const admin = ADMIN_CACHE.get(host) ?? await signin(host, ADMIN_PARAMS) - .catch(async err => { - if (err.id === '6cc579cc-885d-43d8-95c2-b8c7fc963280') { - await createAdmin(host); - return await signin(host, ADMIN_PARAMS); - } - throw err; - }); - - return { - ...admin, - client: new Misskey.api.APIClient({ origin: `https://${host}`, credential: admin.i }), - ...ADMIN_PARAMS, - }; -} - -export async function createAccount(host: Host): Promise { - const username = crypto.randomUUID().replaceAll('-', '').substring(0, 20); - const password = crypto.randomUUID().replaceAll('-', ''); - const admin = await fetchAdmin(host); - await admin.client.request('admin/accounts/create', { username, password }); - const signinRes = await signin(host, { username, password }); - - return { - ...signinRes, - client: new Misskey.api.APIClient({ origin: `https://${host}`, credential: signinRes.i }), - username, - password, - }; -} - -export async function createModerator(host: Host): Promise { - const user = await createAccount(host); - const role = await createRole(host, { - name: 'Moderator', - isModerator: true, - }); - const admin = await fetchAdmin(host); - await admin.client.request('admin/roles/assign', { roleId: role.id, userId: user.id }); - return user; -} - -export async function createRole( - host: Host, - params: Partial = {}, -): Promise { - const admin = await fetchAdmin(host); - return await admin.client.request('admin/roles/create', { - name: 'Some role', - description: 'Role for testing', - color: null, - iconUrl: null, - target: 'conditional', - condFormula: {}, - isPublic: true, - isModerator: false, - isAdministrator: false, - isExplorable: true, - asBadge: false, - canEditMembersByModerator: false, - displayOrder: 0, - policies: {}, - ...params, - }); -} - -export async function resolveRemoteUser( - host: Host, - id: string, - from: LoginUser, -): Promise { - const uri = `https://${host}/users/${id}`; - return await from.client.request('ap/show', { uri }) - .then(res => { - strictEqual(res.type, 'User'); - strictEqual(res.object.uri, uri); - return res.object; - }); -} - -export async function resolveRemoteNote( - host: Host, - id: string, - from: LoginUser, -): Promise { - const uri = `https://${host}/notes/${id}`; - return await from.client.request('ap/show', { uri }) - .then(res => { - strictEqual(res.type, 'Note'); - strictEqual(res.object.uri, uri); - return res.object; - }); -} - -export async function uploadFile( - host: Host, - user: { i: string }, - path = '../../test/resources/192.jpg', -): Promise { - const filename = path.split('/').pop() ?? 'untitled'; - const blob = new Blob([await readFile(join(__dirname, path))]); - - const body = new FormData(); - body.append('i', user.i); - body.append('force', 'true'); - body.append('file', blob); - body.append('name', filename); - - return await fetch(`https://${host}/api/drive/files/create`, { method: 'POST', body }) - .then(async res => await res.json()); -} - -export async function addCustomEmoji( - host: Host, - param?: Partial, - path?: string, -): Promise { - const admin = await fetchAdmin(host); - const name = crypto.randomUUID().replaceAll('-', ''); - const file = await uploadFile(host, admin, path); - return await admin.client.request('admin/emoji/add', { name, fileId: file.id, ...param }); -} - -export function deepStrictEqualWithExcludedFields(actual: T, expected: T, excludedFields: (keyof T)[]) { - const _actual = structuredClone(actual); - const _expected = structuredClone(expected); - for (const obj of [_actual, _expected]) { - for (const field of excludedFields) { - delete obj[field]; - } - } - deepStrictEqual(_actual, _expected); -} - -export async function isFired( - host: Host, - user: { i: string }, - channel: C, - trigger: () => Promise, - type: T, - // @ts-expect-error TODO: why getting error here? - cond: (msg: Parameters[0]) => boolean, - params?: Misskey.Channels[C]['params'], -): Promise { - return new Promise(async (resolve, reject) => { - const stream = new Misskey.Stream(`wss://${host}`, { token: user.i }, { WebSocket }); - const connection = stream.useChannel(channel, params); - connection.on(type as any, ((msg: any) => { - if (cond(msg)) { - stream.close(); - clearTimeout(timer); - resolve(true); - } - }) as any); - - let timer: NodeJS.Timeout | undefined; - - await trigger().then(() => { - timer = setTimeout(() => { - stream.close(); - resolve(false); - }, 500); - }).catch(err => { - stream.close(); - clearTimeout(timer); - reject(err); - }); - }); -}; - -export async function isNoteUpdatedEventFired( - host: Host, - user: { i: string }, - noteId: string, - trigger: () => Promise, - cond: (msg: Parameters[0]) => boolean, -): Promise { - return new Promise(async (resolve, reject) => { - const stream = new Misskey.Stream(`wss://${host}`, { token: user.i }, { WebSocket }); - stream.send('s', { id: noteId }); - stream.on('noteUpdated', msg => { - if (cond(msg)) { - stream.close(); - clearTimeout(timer); - resolve(true); - } - }); - - let timer: NodeJS.Timeout | undefined; - - await trigger().then(() => { - timer = setTimeout(() => { - stream.close(); - resolve(false); - }, 500); - }).catch(err => { - stream.close(); - clearTimeout(timer); - reject(err); - }); - }); -}; - -export async function assertNotificationReceived( - receiverHost: Host, - receiver: LoginUser, - trigger: () => Promise, - cond: (notification: Misskey.entities.Notification) => boolean, - expect: boolean, -) { - const streamingFired = await isFired(receiverHost, receiver, 'main', trigger, 'notification', cond); - strictEqual(streamingFired, expect); - - const endpointFired = await receiver.client.request('i/notifications', {}) - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - .then(([notification]) => notification != null ? cond(notification) : false); - strictEqual(endpointFired, expect); -} diff --git a/packages/backend/test-federation/tsconfig.json b/packages/backend/test-federation/tsconfig.json deleted file mode 100644 index 3a1cb3b9f3..0000000000 --- a/packages/backend/test-federation/tsconfig.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "NodeNext", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./built", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - "include": [ - "daemon.ts", - "./test/**/*.ts" - ] -} diff --git a/packages/backend/test-server/.swcrc b/packages/backend/test-server/.swcrc deleted file mode 100644 index eeac7eabc6..0000000000 --- a/packages/backend/test-server/.swcrc +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://swc.rs/schema.json", - "jsc": { - "parser": { - "syntax": "typescript", - "dynamicImport": true, - "decorators": true - }, - "transform": { - "legacyDecorator": true, - "decoratorMetadata": true - }, - "experimental": { - "keepImportAssertions": true - }, - "baseUrl": "../built", - "paths": { - "@/*": ["*"] - }, - "target": "es2022" - }, - "minify": false -} diff --git a/packages/backend/test-server/entry.ts b/packages/backend/test-server/entry.ts deleted file mode 100644 index 04bf62d209..0000000000 --- a/packages/backend/test-server/entry.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { portToPid } from 'pid-port'; -import fkill from 'fkill'; -import Fastify from 'fastify'; -import { NestFactory } from '@nestjs/core'; -import { MainModule } from '@/MainModule.js'; -import { ServerService } from '@/server/ServerService.js'; -import { loadConfig } from '@/config.js'; -import { NestLogger } from '@/NestLogger.js'; -import { INestApplicationContext } from '@nestjs/common'; - -const config = loadConfig(); -const originEnv = JSON.stringify(process.env); - -process.env.NODE_ENV = 'test'; - -let app: INestApplicationContext; -let serverService: ServerService; - -/** - * テスト用のサーバインスタンスを起動する - */ -async function launch() { - await killTestServer(); - - console.log('starting application...'); - - app = await NestFactory.createApplicationContext(MainModule, { - logger: new NestLogger(), - }); - serverService = app.get(ServerService); - await serverService.launch(); - - await startControllerEndpoints(); - - // ジョブキューは必要な時にテストコード側で起動する - // ジョブキューが動くとテスト結果の確認に支障が出ることがあるので意図的に動かさないでいる - - console.log('application initialized.'); -} - -/** - * 既に重複したポートで待ち受けしているサーバがある場合はkillする - */ -async function killTestServer() { - // - try { - const pid = await portToPid(config.port); - if (pid) { - await fkill(pid, { force: true }); - } - } catch { - // NOP; - } -} - -/** - * 別プロセスに切り離してしまったが故に出来なくなった環境変数の書き換え等を実現するためのエンドポイントを作る - * @param port - */ -async function startControllerEndpoints(port = config.port + 1000) { - const fastify = Fastify(); - - fastify.post<{ Body: { key?: string, value?: string } }>('/env', async (req, res) => { - console.log(req.body); - const key = req.body['key']; - if (!key) { - res.code(400).send({ success: false }); - return; - } - - process.env[key] = req.body['value']; - - res.code(200).send({ success: true }); - }); - - fastify.post<{ Body: { key?: string, value?: string } }>('/env-reset', async (req, res) => { - process.env = JSON.parse(originEnv); - - await serverService.dispose(); - await app.close(); - - await killTestServer(); - - console.log('starting application...'); - - app = await NestFactory.createApplicationContext(MainModule, { - logger: new NestLogger(), - }); - serverService = app.get(ServerService); - await serverService.launch(); - - res.code(200).send({ success: true }); - }); - - await fastify.listen({ port: port, host: 'localhost' }); -} - -export default launch; diff --git a/packages/backend/test-server/eslint.config.js b/packages/backend/test-server/eslint.config.js deleted file mode 100644 index b9c16d469f..0000000000 --- a/packages/backend/test-server/eslint.config.js +++ /dev/null @@ -1,43 +0,0 @@ -import tsParser from '@typescript-eslint/parser'; -import sharedConfig from '../../shared/eslint.config.js'; - -export default [ - ...sharedConfig, - { - files: ['**/*.ts', '**/*.tsx'], - languageOptions: { - parserOptions: { - parser: tsParser, - project: ['./tsconfig.json'], - sourceType: 'module', - tsconfigRootDir: import.meta.dirname, - }, - }, - rules: { - 'import/order': ['warn', { - groups: [ - 'builtin', - 'external', - 'internal', - 'parent', - 'sibling', - 'index', - 'object', - 'type', - ], - pathGroups: [{ - pattern: '@/**', - group: 'external', - position: 'after', - }], - }], - 'no-restricted-globals': ['error', { - name: '__dirname', - message: 'Not in ESModule. Use `import.meta.url` instead.', - }, { - name: '__filename', - message: 'Not in ESModule. Use `import.meta.url` instead.', - }], - }, - }, -]; diff --git a/packages/backend/test-server/tsconfig.json b/packages/backend/test-server/tsconfig.json deleted file mode 100644 index 10313699c2..0000000000 --- a/packages/backend/test-server/tsconfig.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "noEmitOnError": true, - "noImplicitAny": true, - "noImplicitReturns": true, - "noUnusedParameters": false, - "noUnusedLocals": false, - "noFallthroughCasesInSwitch": true, - "declaration": false, - "sourceMap": true, - "target": "ES2022", - "module": "nodenext", - "moduleResolution": "nodenext", - "allowSyntheticDefaultImports": true, - "removeComments": false, - "noLib": false, - "strict": true, - "strictNullChecks": true, - "strictPropertyInitialization": false, - "skipLibCheck": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "resolveJsonModule": true, - "isolatedModules": true, - "rootDir": "../src", - "baseUrl": "./", - "paths": { - "@/*": ["../src/*"] - }, - "outDir": "../built-test", - "types": [ - "node" - ], - "typeRoots": [ - "../src/@types", - "../node_modules/@types", - "../node_modules" - ], - "lib": [ - "esnext" - ] - }, - "compileOnSave": false, - "include": [ - "./**/*.ts", - "../src/**/*.ts" - ], - "exclude": [ - "../src/**/*.test.ts" - ] -} diff --git a/packages/backend/test/.eslintrc.cjs b/packages/backend/test/.eslintrc.cjs new file mode 100644 index 0000000000..41ecea0c3f --- /dev/null +++ b/packages/backend/test/.eslintrc.cjs @@ -0,0 +1,11 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + extends: ['../.eslintrc.cjs'], + env: { + node: true, + jest: true, + }, +}; diff --git a/packages/backend/test/compose.yml b/packages/backend/test/docker-compose.yml similarity index 85% rename from packages/backend/test/compose.yml rename to packages/backend/test/docker-compose.yml index 6593fc33dd..da6c01dda1 100644 --- a/packages/backend/test/compose.yml +++ b/packages/backend/test/docker-compose.yml @@ -1,3 +1,5 @@ +version: "3" + services: redistest: image: redis:7 @@ -5,7 +7,7 @@ services: - "127.0.0.1:56312:6379" dbtest: - image: postgres:15 + image: postgres:13 ports: - "127.0.0.1:54312:5432" environment: diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index 48e1bababb..04be97ad9d 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -1,28 +1,17 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as crypto from 'node:crypto'; import cbor from 'cbor'; import * as OTPAuth from 'otpauth'; -import { loadConfig } from '@/config.js'; -import { api, signup } from '../utils.js'; -import type { - AuthenticationResponseJSON, - AuthenticatorAssertionResponseJSON, - AuthenticatorAttestationResponseJSON, - PublicKeyCredentialCreationOptionsJSON, - PublicKeyCredentialRequestOptionsJSON, - RegistrationResponseJSON, -} from '@simplewebauthn/types'; +import { loadConfig } from '../../src/config.js'; +import { signup, api, post, react, startServer, waitFire } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; describe('2要素認証', () => { - let alice: misskey.entities.SignupResponse; + let app: INestApplicationContext; + let alice: misskey.entities.MeSignup; const config = loadConfig(); const password = 'test'; @@ -53,20 +42,21 @@ describe('2要素認証', () => { const rpIdHash = (): Buffer => { return crypto.createHash('sha256') - .update(Buffer.from(config.host, 'utf-8')) + .update(Buffer.from(config.hostname, 'utf-8')) .digest(); }; const keyDoneParam = (param: { - token: string, keyName: string, + challengeId: string, + challenge: string, credentialId: Buffer, - creationOptions: PublicKeyCredentialCreationOptionsJSON, }): { - token: string, + attestationObject: string, + challengeId: string, + clientDataJSON: string, password: string, name: string, - credential: RegistrationResponseJSON, } => { // A COSE encoded public key const credentialPublicKey = cbor.encode(new Map([ @@ -81,7 +71,7 @@ describe('2要素認証', () => { // AuthenticatorAssertionResponse.authenticatorData // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData const credentialIdLength = Buffer.allocUnsafe(2); - credentialIdLength.writeUInt16BE(param.credentialId.length, 0); + credentialIdLength.writeUInt16BE(param.credentialId.length); const authData = Buffer.concat([ rpIdHash(), // rpIdHash(32) Buffer.from([0x45]), // flags(1) @@ -93,28 +83,20 @@ describe('2要素認証', () => { ]); return { + attestationObject: cbor.encode({ + fmt: 'none', + attStmt: {}, + authData, + }).toString('hex'), + challengeId: param.challengeId, + clientDataJSON: JSON.stringify({ + type: 'webauthn.create', + challenge: param.challenge, + origin: config.scheme + '://' + config.host, + androidPackageName: 'org.mozilla.firefox', + }), password, - token: param.token, name: param.keyName, - credential: { - id: param.credentialId.toString('base64url'), - rawId: param.credentialId.toString('base64url'), - response: { - clientDataJSON: Buffer.from(JSON.stringify({ - type: 'webauthn.create', - challenge: param.creationOptions.challenge, - origin: config.scheme + '://' + config.host, - androidPackageName: 'org.mozilla.firefox', - }), 'utf-8').toString('base64url'), - attestationObject: cbor.encode({ - fmt: 'none', - attStmt: {}, - authData, - }).toString('base64url'), - }, - clientExtensionResults: {}, - type: 'public-key', - }, }; }; @@ -134,9 +116,20 @@ describe('2要素認証', () => { const signinWithSecurityKeyParam = (param: { keyName: string, + challengeId: string, + challenge: string, credentialId: Buffer, - requestOptions: PublicKeyCredentialRequestOptionsJSON, - }): misskey.entities.SigninFlowRequest => { + }): { + authenticatorData: string, + credentialId: string, + challengeId: string, + clientDataJSON: string, + username: string, + password: string, + signature: string, + 'g-recaptcha-response'?: string | null, + 'hcaptcha-response'?: string | null, + } => { // AuthenticatorAssertionResponse.authenticatorData // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData const authenticatorData = Buffer.concat([ @@ -146,10 +139,10 @@ describe('2要素認証', () => { ]); const clientDataJSONBuffer = Buffer.from(JSON.stringify({ type: 'webauthn.get', - challenge: param.requestOptions.challenge, + challenge: param.challenge, origin: config.scheme + '://' + config.host, androidPackageName: 'org.mozilla.firefox', - }), 'utf-8'); + })); const hashedclientDataJSON = crypto.createHash('sha256') .update(clientDataJSONBuffer) .digest(); @@ -158,30 +151,29 @@ describe('2要素認証', () => { .update(Buffer.concat([authenticatorData, hashedclientDataJSON])) .sign(privateKey); return { + authenticatorData: authenticatorData.toString('hex'), + credentialId: param.credentialId.toString('base64'), + challengeId: param.challengeId, + clientDataJSON: clientDataJSONBuffer.toString('hex'), username, password, - credential: { - id: param.credentialId.toString('base64url'), - rawId: param.credentialId.toString('base64url'), - response: { - clientDataJSON: clientDataJSONBuffer.toString('base64url'), - authenticatorData: authenticatorData.toString('base64url'), - signature: signature.toString('base64url'), - }, - clientExtensionResults: {}, - type: 'public-key', - }, + signature: signature.toString('hex'), 'g-recaptcha-response': null, 'hcaptcha-response': null, }; }; beforeAll(async () => { + app = await startServer(); alice = await signup({ username, password }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + test('が設定でき、OTPでログインできる。', async () => { - const registerResponse = await api('i/2fa/register', { + const registerResponse = await api('/i/2fa/register', { password, }, alice); assert.strictEqual(registerResponse.status, 200); @@ -191,299 +183,258 @@ describe('2要素認証', () => { assert.strictEqual(registerResponse.body.label, username); assert.strictEqual(registerResponse.body.issuer, config.host); - const doneResponse = await api('i/2fa/done', { + const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); - assert.strictEqual(doneResponse.status, 200); + assert.strictEqual(doneResponse.status, 204); - const signinWithoutTokenResponse = await api('signin-flow', { - ...signinParam(), - }); - assert.strictEqual(signinWithoutTokenResponse.status, 200); - assert.deepStrictEqual(signinWithoutTokenResponse.body, { - finished: false, - next: 'totp', - }); + const usersShowResponse = await api('/users/show', { + username, + }, alice); + assert.strictEqual(usersShowResponse.status, 200); + assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); - const signinResponse = await api('signin-flow', { + const signinResponse = await api('/signin', { ...signinParam(), token: otpToken(registerResponse.body.secret), }); assert.strictEqual(signinResponse.status, 200); - assert.strictEqual(signinResponse.body.finished, true); assert.notEqual(signinResponse.body.i, undefined); - - // 後片付け - await api('i/2fa/unregister', { - password, - token: otpToken(registerResponse.body.secret), - }, alice); }); test('が設定でき、セキュリティキーでログインできる。', async () => { - const registerResponse = await api('i/2fa/register', { + const registerResponse = await api('/i/2fa/register', { password, }, alice); assert.strictEqual(registerResponse.status, 200); - const doneResponse = await api('i/2fa/done', { + const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); - assert.strictEqual(doneResponse.status, 200); + assert.strictEqual(doneResponse.status, 204); - const registerKeyResponse = await api('i/2fa/register-key', { + const registerKeyResponse = await api('/i/2fa/register-key', { password, - token: otpToken(registerResponse.body.secret), }, alice); assert.strictEqual(registerKeyResponse.status, 200); - assert.notEqual(registerKeyResponse.body.rp, undefined); + assert.notEqual(registerKeyResponse.body.challengeId, undefined); assert.notEqual(registerKeyResponse.body.challenge, undefined); const keyName = 'example-key'; const credentialId = crypto.randomBytes(0x41); - const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({ - token: otpToken(registerResponse.body.secret), + const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ keyName, + challengeId: registerKeyResponse.body.challengeId, + challenge: registerKeyResponse.body.challenge, credentialId, - creationOptions: registerKeyResponse.body, - } as any) as any, alice); + }), alice); assert.strictEqual(keyDoneResponse.status, 200); - assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url')); + assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('hex')); assert.strictEqual(keyDoneResponse.body.name, keyName); - const signinResponse = await api('signin-flow', { + const usersShowResponse = await api('/users/show', { + username, + }); + assert.strictEqual(usersShowResponse.status, 200); + assert.strictEqual(usersShowResponse.body.securityKeys, true); + + const signinResponse = await api('/signin', { ...signinParam(), }); assert.strictEqual(signinResponse.status, 200); - assert.strictEqual(signinResponse.body.finished, false); - assert.strictEqual(signinResponse.body.next, 'passkey'); - assert.notEqual(signinResponse.body.authRequest.challenge, undefined); - assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined); - assert.strictEqual(signinResponse.body.authRequest.allowCredentials && signinResponse.body.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url')); + assert.strictEqual(signinResponse.body.i, undefined); + assert.notEqual(signinResponse.body.challengeId, undefined); + assert.notEqual(signinResponse.body.challenge, undefined); + assert.notEqual(signinResponse.body.securityKeys, undefined); + assert.strictEqual(signinResponse.body.securityKeys[0].id, credentialId.toString('hex')); - const signinResponse2 = await api('signin-flow', signinWithSecurityKeyParam({ + const signinResponse2 = await api('/signin', signinWithSecurityKeyParam({ keyName, + challengeId: signinResponse.body.challengeId, + challenge: signinResponse.body.challenge, credentialId, - requestOptions: signinResponse.body.authRequest, })); assert.strictEqual(signinResponse2.status, 200); - assert.strictEqual(signinResponse2.body.finished, true); assert.notEqual(signinResponse2.body.i, undefined); - - // 後片付け - await api('i/2fa/unregister', { - password, - token: otpToken(registerResponse.body.secret), - }, alice); }); test('が設定でき、セキュリティキーでパスワードレスログインできる。', async () => { - const registerResponse = await api('i/2fa/register', { + const registerResponse = await api('/i/2fa/register', { password, }, alice); assert.strictEqual(registerResponse.status, 200); - const doneResponse = await api('i/2fa/done', { + const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); - assert.strictEqual(doneResponse.status, 200); + assert.strictEqual(doneResponse.status, 204); - const registerKeyResponse = await api('i/2fa/register-key', { - token: otpToken(registerResponse.body.secret), + const registerKeyResponse = await api('/i/2fa/register-key', { password, }, alice); assert.strictEqual(registerKeyResponse.status, 200); const keyName = 'example-key'; const credentialId = crypto.randomBytes(0x41); - const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({ - token: otpToken(registerResponse.body.secret), + const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ keyName, + challengeId: registerKeyResponse.body.challengeId, + challenge: registerKeyResponse.body.challenge, credentialId, - creationOptions: registerKeyResponse.body, - } as any) as any, alice); + }), alice); assert.strictEqual(keyDoneResponse.status, 200); - const passwordLessResponse = await api('i/2fa/password-less', { + const passwordLessResponse = await api('/i/2fa/password-less', { value: true, }, alice); assert.strictEqual(passwordLessResponse.status, 204); - const iResponse = await api('i', {}, alice); - assert.strictEqual(iResponse.status, 200); - assert.strictEqual(iResponse.body.usePasswordLessLogin, true); + const usersShowResponse = await api('/users/show', { + username, + }); + assert.strictEqual(usersShowResponse.status, 200); + assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true); - const signinResponse = await api('signin-flow', { + const signinResponse = await api('/signin', { ...signinParam(), password: '', }); assert.strictEqual(signinResponse.status, 200); - assert.strictEqual(signinResponse.body.finished, false); - assert.strictEqual(signinResponse.body.next, 'passkey'); - assert.notEqual(signinResponse.body.authRequest.challenge, undefined); - assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined); + assert.strictEqual(signinResponse.body.i, undefined); - const signinResponse2 = await api('signin-flow', { + const signinResponse2 = await api('/signin', { ...signinWithSecurityKeyParam({ keyName, + challengeId: signinResponse.body.challengeId, + challenge: signinResponse.body.challenge, credentialId, - requestOptions: signinResponse.body.authRequest, - } as any), + }), password: '', }); assert.strictEqual(signinResponse2.status, 200); - assert.strictEqual(signinResponse2.body.finished, true); assert.notEqual(signinResponse2.body.i, undefined); - - // 後片付け - await api('i/2fa/unregister', { - password, - token: otpToken(registerResponse.body.secret), - }, alice); }); test('が設定でき、設定したセキュリティキーの名前を変更できる。', async () => { - const registerResponse = await api('i/2fa/register', { + const registerResponse = await api('/i/2fa/register', { password, }, alice); assert.strictEqual(registerResponse.status, 200); - const doneResponse = await api('i/2fa/done', { + const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); - assert.strictEqual(doneResponse.status, 200); + assert.strictEqual(doneResponse.status, 204); - const registerKeyResponse = await api('i/2fa/register-key', { - token: otpToken(registerResponse.body.secret), + const registerKeyResponse = await api('/i/2fa/register-key', { password, }, alice); assert.strictEqual(registerKeyResponse.status, 200); const keyName = 'example-key'; const credentialId = crypto.randomBytes(0x41); - const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({ - token: otpToken(registerResponse.body.secret), + const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ keyName, + challengeId: registerKeyResponse.body.challengeId, + challenge: registerKeyResponse.body.challenge, credentialId, - creationOptions: registerKeyResponse.body, - } as any) as any, alice); + }), alice); assert.strictEqual(keyDoneResponse.status, 200); const renamedKey = 'other-key'; - const updateKeyResponse = await api('i/2fa/update-key', { + const updateKeyResponse = await api('/i/2fa/update-key', { name: renamedKey, - credentialId: credentialId.toString('base64url'), + credentialId: credentialId.toString('hex'), }, alice); assert.strictEqual(updateKeyResponse.status, 200); - const iResponse = await api('i', { + const iResponse = await api('/i', { }, alice); assert.strictEqual(iResponse.status, 200); - assert.ok(iResponse.body.securityKeysList); - const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url')); + const securityKeys = iResponse.body.securityKeysList.filter(s => s.id === credentialId.toString('hex')); assert.strictEqual(securityKeys.length, 1); assert.strictEqual(securityKeys[0].name, renamedKey); assert.notEqual(securityKeys[0].lastUsed, undefined); - - // 後片付け - await api('i/2fa/unregister', { - password, - token: otpToken(registerResponse.body.secret), - }, alice); }); test('が設定でき、設定したセキュリティキーを削除できる。', async () => { - const registerResponse = await api('i/2fa/register', { + const registerResponse = await api('/i/2fa/register', { password, }, alice); assert.strictEqual(registerResponse.status, 200); - const doneResponse = await api('i/2fa/done', { + const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); - assert.strictEqual(doneResponse.status, 200); + assert.strictEqual(doneResponse.status, 204); - const registerKeyResponse = await api('i/2fa/register-key', { - token: otpToken(registerResponse.body.secret), + const registerKeyResponse = await api('/i/2fa/register-key', { password, }, alice); assert.strictEqual(registerKeyResponse.status, 200); const keyName = 'example-key'; const credentialId = crypto.randomBytes(0x41); - const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({ - token: otpToken(registerResponse.body.secret), + const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ keyName, + challengeId: registerKeyResponse.body.challengeId, + challenge: registerKeyResponse.body.challenge, credentialId, - creationOptions: registerKeyResponse.body, - } as any) as any, alice); + }), alice); assert.strictEqual(keyDoneResponse.status, 200); // テストの実行順によっては複数残ってるので全部消す - const beforeIResponse = await api('i', { + const iResponse = await api('/i', { }, alice); - assert.strictEqual(beforeIResponse.status, 200); - assert.ok(beforeIResponse.body.securityKeysList); - for (const key of beforeIResponse.body.securityKeysList) { - const removeKeyResponse = await api('i/2fa/remove-key', { - token: otpToken(registerResponse.body.secret), + assert.strictEqual(iResponse.status, 200); + for (const key of iResponse.body.securityKeysList) { + const removeKeyResponse = await api('/i/2fa/remove-key', { password, credentialId: key.id, }, alice); assert.strictEqual(removeKeyResponse.status, 200); } - const afterIResponse = await api('i', {}, alice); - assert.strictEqual(afterIResponse.status, 200); - assert.strictEqual(afterIResponse.body.securityKeys, false); + const usersShowResponse = await api('/users/show', { + username, + }); + assert.strictEqual(usersShowResponse.status, 200); + assert.strictEqual(usersShowResponse.body.securityKeys, false); - const signinResponse = await api('signin-flow', { + const signinResponse = await api('/signin', { ...signinParam(), token: otpToken(registerResponse.body.secret), }); assert.strictEqual(signinResponse.status, 200); - assert.strictEqual(signinResponse.body.finished, true); assert.notEqual(signinResponse.body.i, undefined); - - // 後片付け - await api('i/2fa/unregister', { - password, - token: otpToken(registerResponse.body.secret), - }, alice); }); test('が設定でき、設定解除できる。(パスワードのみでログインできる。)', async () => { - const registerResponse = await api('i/2fa/register', { + const registerResponse = await api('/i/2fa/register', { password, }, alice); assert.strictEqual(registerResponse.status, 200); - const doneResponse = await api('i/2fa/done', { + const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); - assert.strictEqual(doneResponse.status, 200); + assert.strictEqual(doneResponse.status, 204); - const iResponse = await api('i', {}, alice); - assert.strictEqual(iResponse.status, 200); - assert.strictEqual(iResponse.body.twoFactorEnabled, true); + const usersShowResponse = await api('/users/show', { + username, + }); + assert.strictEqual(usersShowResponse.status, 200); + assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); - const unregisterResponse = await api('i/2fa/unregister', { - token: otpToken(registerResponse.body.secret), + const unregisterResponse = await api('/i/2fa/unregister', { password, }, alice); assert.strictEqual(unregisterResponse.status, 204); - const signinResponse = await api('signin-flow', { + const signinResponse = await api('/signin', { ...signinParam(), }); assert.strictEqual(signinResponse.status, 200); - assert.strictEqual(signinResponse.body.finished, true); assert.notEqual(signinResponse.body.i, undefined); - - // 後片付け - await api('i/2fa/unregister', { - password, - token: otpToken(registerResponse.body.secret), - }, alice); }); }); diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index 4dbeacf925..cb526669f5 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -1,24 +1,24 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { inspect } from 'node:util'; +import { DEFAULT_POLICIES } from '@/core/RoleService.js'; +import type { Packed } from '@/misc/json-schema.js'; import { - api, - failedApiCall, - post, - role, signup, - successfulApiCall, - testPaginationConsistency, - uploadFile, + post, userList, + page, + role, + startServer, + api, + successfulApiCall, + failedApiCall, + uploadFile, + testPaginationConsistency, } from '../utils.js'; import type * as misskey from 'misskey-js'; -import { DEFAULT_POLICIES } from '@/core/RoleService.js'; +import type { INestApplicationContext } from '@nestjs/common'; const compareBy = (selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => { return selector(a).localeCompare(selector(b)); @@ -28,8 +28,11 @@ describe('アンテナ', () => { // エンティティとしてのアンテナを主眼においたテストを記述する // (Antennaを返すエンドポイント、Antennaエンティティを書き換えるエンドポイント、Antennaからノートを取得するエンドポイントをテストする) - type Antenna = misskey.entities.Antenna; - type User = misskey.entities.SignupResponse; + // BUG misskey-jsとjson-schemaが一致していない。 + // - srcのenumにgroupが残っている + // - userGroupIdが残っている, isActiveがない + type Antenna = misskey.entities.Antenna | Packed<'Antenna'>; + type User = misskey.entities.MeSignup; type Note = misskey.entities.Note; // アンテナを作成できる最小のパラメタ @@ -38,14 +41,16 @@ describe('アンテナ', () => { excludeKeywords: [['']], keywords: [['keyword']], name: 'test', + notify: false, src: 'all' as const, userListId: null, users: [''], withFile: false, withReplies: false, - excludeBots: false, }; + let app: INestApplicationContext; + let root: User; let alice: User; let bob: User; @@ -69,6 +74,10 @@ describe('アンテナ', () => { let userMutingAlice: User; let userMutedByAlice: User; + beforeAll(async () => { + app = await startServer(); + }, 1000 * 60 * 2); + beforeAll(async () => { root = await signup({ username: 'root' }); alice = await signup({ username: 'alice' }); @@ -76,7 +85,7 @@ describe('アンテナ', () => { aliceList = await userList(alice, {}); bob = await signup({ username: 'bob' }); aliceList = await userList(alice, {}); - bobFile = (await uploadFile(bob)).body!; + bobFile = (await uploadFile(bob)).body; bobList = await userList(bob); carol = await signup({ username: 'carol' }); await api('users/lists/push', { listId: aliceList.id, userId: bob.id }, alice); @@ -122,12 +131,16 @@ describe('アンテナ', () => { await api('mute/create', { userId: userMutedByAlice.id }, alice); }, 1000 * 60 * 10); + afterAll(async () => { + await app.close(); + }); + beforeEach(async () => { // テスト間で影響し合わないように毎回全部消す。 for (const user of [alice, bob]) { - const list = await api('antennas/list', {}, user); + const list = await api('/antennas/list', {}, user); for (const antenna of list.body) { - await api('antennas/delete', { antennaId: antenna.id }, user); + await api('/antennas/delete', { antennaId: antenna.id }, user); } } }); @@ -137,34 +150,32 @@ describe('アンテナ', () => { test('が作成できること、キーが過不足なく入っていること。', async () => { const response = await successfulApiCall({ endpoint: 'antennas/create', - parameters: defaultParam, + parameters: { ...defaultParam }, user: alice, }); assert.match(response.id, /[0-9a-z]{10}/); - const expected: Antenna = { + const expected = { id: response.id, caseSensitive: false, createdAt: new Date(response.createdAt).toISOString(), excludeKeywords: [['']], - excludeNotesInSensitiveChannel: false, hasUnreadNote: false, isActive: true, keywords: [['keyword']], name: 'test', + notify: false, src: 'all', userListId: null, users: [''], withFile: false, withReplies: false, - excludeBots: false, - localOnly: false, - notify: false, - }; + } as Antenna; assert.deepStrictEqual(response, expected); }); test('が上限いっぱいまで作成できること', async () => { - const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit)].map(() => successfulApiCall({ + // antennaLimit + 1まで作れるのがキモ + const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit + 1)].map(() => successfulApiCall({ endpoint: 'antennas/create', parameters: { ...defaultParam }, user: alice, @@ -199,27 +210,27 @@ describe('アンテナ', () => { }); const antennaParamPattern = [ - { parameters: () => ({ name: 'x'.repeat(100) }) }, - { parameters: () => ({ name: 'x' }) }, - { parameters: () => ({ src: 'home' as const }) }, - { parameters: () => ({ src: 'all' as const }) }, - { parameters: () => ({ src: 'users' as const }) }, - { parameters: () => ({ src: 'list' as const }) }, - { parameters: () => ({ userListId: null }) }, - { parameters: () => ({ src: 'list' as const, userListId: aliceList.id }) }, - { parameters: () => ({ keywords: [['x']] }) }, - { parameters: () => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) }, - { parameters: () => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) }, - { parameters: () => ({ users: [alice.username] }) }, - { parameters: () => ({ users: [alice.username, bob.username, carol.username] }) }, - { parameters: () => ({ caseSensitive: false }) }, - { parameters: () => ({ caseSensitive: true }) }, - { parameters: () => ({ withReplies: false }) }, - { parameters: () => ({ withReplies: true }) }, - { parameters: () => ({ withFile: false }) }, - { parameters: () => ({ withFile: true }) }, - { parameters: () => ({ excludeNotesInSensitiveChannel: false }) }, - { parameters: () => ({ excludeNotesInSensitiveChannel: true }) }, + { parameters: (): object => ({ name: 'x'.repeat(100) }) }, + { parameters: (): object => ({ name: 'x' }) }, + { parameters: (): object => ({ src: 'home' }) }, + { parameters: (): object => ({ src: 'all' }) }, + { parameters: (): object => ({ src: 'users' }) }, + { parameters: (): object => ({ src: 'list' }) }, + { parameters: (): object => ({ userListId: null }) }, + { parameters: (): object => ({ src: 'list', userListId: aliceList.id }) }, + { parameters: (): object => ({ keywords: [['x']] }) }, + { parameters: (): object => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) }, + { parameters: (): object => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) }, + { parameters: (): object => ({ users: [alice.username] }) }, + { parameters: (): object => ({ users: [alice.username, bob.username, carol.username] }) }, + { parameters: (): object => ({ caseSensitive: false }) }, + { parameters: (): object => ({ caseSensitive: true }) }, + { parameters: (): object => ({ withReplies: false }) }, + { parameters: (): object => ({ withReplies: true }) }, + { parameters: (): object => ({ withFile: false }) }, + { parameters: (): object => ({ withFile: true }) }, + { parameters: (): object => ({ notify: false }) }, + { parameters: (): object => ({ notify: true }) }, ]; test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => { const response = await successfulApiCall({ @@ -231,17 +242,6 @@ describe('アンテナ', () => { assert.deepStrictEqual(response, expected); }); - test('を作成する時キーワードが指定されていないとエラーになる', async () => { - await failedApiCall({ - endpoint: 'antennas/create', - parameters: { ...defaultParam, keywords: [[]], excludeKeywords: [[]] }, - user: alice, - }, { - status: 400, - code: 'EMPTY_KEYWORD', - id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a', - }); - }); //#endregion //#region 更新(antennas/update) @@ -269,18 +269,6 @@ describe('アンテナ', () => { id: '1c6b35c9-943e-48c2-81e4-2844989407f7', }); }); - test('を変更する時キーワードが指定されていないとエラーになる', async () => { - const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice }); - await failedApiCall({ - endpoint: 'antennas/update', - parameters: { ...defaultParam, antennaId: antenna.id, keywords: [[]], excludeKeywords: [[]] }, - user: alice, - }, { - status: 400, - code: 'EMPTY_KEYWORD', - id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4', - }); - }); //#endregion //#region 表示(antennas/show) @@ -355,7 +343,7 @@ describe('アンテナ', () => { test.each([ { label: '全体から', - parameters: () => ({ src: 'all' }), + parameters: (): object => ({ src: 'all' }), posts: [ { note: (): Promise => post(alice, { text: `${keyword}` }), included: true }, { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}` }), included: true }, @@ -366,7 +354,7 @@ describe('アンテナ', () => { { // BUG e4144a1 以降home指定は壊れている(allと同じ) label: 'ホーム指定はallと同じ', - parameters: () => ({ src: 'home' }), + parameters: (): object => ({ src: 'home' }), posts: [ { note: (): Promise => post(alice, { text: `${keyword}` }), included: true }, { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}` }), included: true }, @@ -375,77 +363,68 @@ describe('アンテナ', () => { ], }, { - label: 'フォロワー限定投稿とDM投稿を含む', - parameters: () => ({}), + // https://github.com/misskey-dev/misskey/issues/9025 + label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。', + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true }, { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true }, - { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'followers' }), included: true }, - { note: (): Promise => post(bob, { text: `${keyword}`, visibility: 'specified', visibleUserIds: [alice.id] }), included: true }, - ], - }, - { - label: 'フォロワー限定投稿とDM投稿を含まない', - parameters: () => ({}), - posts: [ - { note: (): Promise => post(bob, { text: `${keyword}`, visibility: 'public' }), included: true }, - { note: (): Promise => post(bob, { text: `${keyword}`, visibility: 'home' }), included: true }, - { note: (): Promise => post(bob, { text: `${keyword}`, visibility: 'followers' }) }, - { note: (): Promise => post(bob, { text: `${keyword}`, visibility: 'specified', visibleUserIds: [carol.id] }) }, + { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'followers' }) }, + { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'specified', visibleUserIds: [alice.id] }) }, ], }, { label: 'ブロックしているユーザーのノートは含む', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userBlockedByAlice, { text: `${keyword}` }), included: true }, ], }, { label: 'ブロックされているユーザーのノートは含まない', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userBlockingAlice, { text: `${keyword}` }) }, ], }, { label: 'ミュートしているユーザーのノートは含まない', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userMutedByAlice, { text: `${keyword}` }) }, ], }, { label: 'ミュートされているユーザーのノートは含む', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userMutingAlice, { text: `${keyword}` }), included: true }, ], }, { label: '「見つけやすくする」がOFFのユーザーのノートも含まれる', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userNotExplorable, { text: `${keyword}` }), included: true }, ], }, { label: '鍵付きユーザーのノートも含まれる', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userLocking, { text: `${keyword}` }), included: true }, ], }, { label: 'サイレンスのノートも含まれる', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userSilenced, { text: `${keyword}` }), included: true }, ], }, { label: '削除ユーザーのノートも含まれる', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userDeletedBySelf, { text: `${keyword}` }), included: true }, { note: (): Promise => post(userDeletedByAdmin, { text: `${keyword}` }), included: true }, @@ -453,7 +432,7 @@ describe('アンテナ', () => { }, { label: 'ユーザー指定で', - parameters: () => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }), + parameters: (): object => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }), posts: [ { note: (): Promise => post(alice, { text: `test ${keyword}` }) }, { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true }, @@ -462,7 +441,7 @@ describe('アンテナ', () => { }, { label: 'リスト指定で', - parameters: () => ({ src: 'list', userListId: aliceList.id }), + parameters: (): object => ({ src: 'list', userListId: aliceList.id }), posts: [ { note: (): Promise => post(alice, { text: `test ${keyword}` }) }, { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true }, @@ -471,14 +450,14 @@ describe('アンテナ', () => { }, { label: 'CWにもマッチする', - parameters: () => ({ keywords: [[keyword]] }), + parameters: (): object => ({ keywords: [[keyword]] }), posts: [ { note: (): Promise => post(bob, { text: 'test', cw: `cw ${keyword}` }), included: true }, ], }, { label: 'キーワード1つ', - parameters: () => ({ keywords: [[keyword]] }), + parameters: (): object => ({ keywords: [[keyword]] }), posts: [ { note: (): Promise => post(alice, { text: 'test' }) }, { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true }, @@ -487,7 +466,7 @@ describe('アンテナ', () => { }, { label: 'キーワード3つ(AND)', - parameters: () => ({ keywords: [['A', 'B', 'C']] }), + parameters: (): object => ({ keywords: [['A', 'B', 'C']] }), posts: [ { note: (): Promise => post(bob, { text: 'test A' }) }, { note: (): Promise => post(bob, { text: 'test A B' }) }, @@ -498,7 +477,7 @@ describe('アンテナ', () => { }, { label: 'キーワード3つ(OR)', - parameters: () => ({ keywords: [['A'], ['B'], ['C']] }), + parameters: (): object => ({ keywords: [['A'], ['B'], ['C']] }), posts: [ { note: (): Promise => post(bob, { text: 'test' }) }, { note: (): Promise => post(bob, { text: 'test A' }), included: true }, @@ -511,7 +490,7 @@ describe('アンテナ', () => { }, { label: '除外ワード3つ(AND)', - parameters: () => ({ excludeKeywords: [['A', 'B', 'C']] }), + parameters: (): object => ({ excludeKeywords: [['A', 'B', 'C']] }), posts: [ { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true }, { note: (): Promise => post(bob, { text: `test ${keyword} A` }), included: true }, @@ -524,7 +503,7 @@ describe('アンテナ', () => { }, { label: '除外ワード3つ(OR)', - parameters: () => ({ excludeKeywords: [['A'], ['B'], ['C']] }), + parameters: (): object => ({ excludeKeywords: [['A'], ['B'], ['C']] }), posts: [ { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true }, { note: (): Promise => post(bob, { text: `test ${keyword} A` }) }, @@ -537,7 +516,7 @@ describe('アンテナ', () => { }, { label: 'キーワード1つ(大文字小文字区別する)', - parameters: () => ({ keywords: [['KEYWORD']], caseSensitive: true }), + parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: true }), posts: [ { note: (): Promise => post(bob, { text: 'keyword' }) }, { note: (): Promise => post(bob, { text: 'kEyWoRd' }) }, @@ -546,7 +525,7 @@ describe('アンテナ', () => { }, { label: 'キーワード1つ(大文字小文字区別しない)', - parameters: () => ({ keywords: [['KEYWORD']], caseSensitive: false }), + parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: false }), posts: [ { note: (): Promise => post(bob, { text: 'keyword' }), included: true }, { note: (): Promise => post(bob, { text: 'kEyWoRd' }), included: true }, @@ -555,7 +534,7 @@ describe('アンテナ', () => { }, { label: '除外ワード1つ(大文字小文字区別する)', - parameters: () => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }), + parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }), posts: [ { note: (): Promise => post(bob, { text: `${keyword}` }), included: true }, { note: (): Promise => post(bob, { text: `${keyword} keyword` }), included: true }, @@ -565,7 +544,7 @@ describe('アンテナ', () => { }, { label: '除外ワード1つ(大文字小文字区別しない)', - parameters: () => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }), + parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }), posts: [ { note: (): Promise => post(bob, { text: `${keyword}` }), included: true }, { note: (): Promise => post(bob, { text: `${keyword} keyword` }) }, @@ -575,7 +554,7 @@ describe('アンテナ', () => { }, { label: '添付ファイルを問わない', - parameters: () => ({ withFile: false }), + parameters: (): object => ({ withFile: false }), posts: [ { note: (): Promise => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true }, { note: (): Promise => post(bob, { text: `${keyword}` }), included: true }, @@ -583,7 +562,7 @@ describe('アンテナ', () => { }, { label: '添付ファイル付きのみ', - parameters: () => ({ withFile: true }), + parameters: (): object => ({ withFile: true }), posts: [ { note: (): Promise => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true }, { note: (): Promise => post(bob, { text: `${keyword}` }) }, @@ -591,7 +570,7 @@ describe('アンテナ', () => { }, { label: 'リプライ以外', - parameters: () => ({ withReplies: false }), + parameters: (): object => ({ withReplies: false }), posts: [ { note: (): Promise => post(bob, { text: `${keyword}`, replyId: alicePost.id }) }, { note: (): Promise => post(bob, { text: `${keyword}` }), included: true }, @@ -599,7 +578,7 @@ describe('アンテナ', () => { }, { label: 'リプライも含む', - parameters: () => ({ withReplies: true }), + parameters: (): object => ({ withReplies: true }), posts: [ { note: (): Promise => post(bob, { text: `${keyword}`, replyId: alicePost.id }), included: true }, { note: (): Promise => post(bob, { text: `${keyword}` }), included: true }, @@ -638,42 +617,6 @@ describe('アンテナ', () => { assert.deepStrictEqual(response, expected); }); - test('が取得できること(センシティブチャンネルのノートを除く)', async () => { - const keyword = 'キーワード'; - const antenna = await successfulApiCall({ - endpoint: 'antennas/create', - parameters: { ...defaultParam, keywords: [[keyword]], excludeNotesInSensitiveChannel: true }, - user: alice, - }); - const nonSensitiveChannel = await successfulApiCall({ - endpoint: 'channels/create', - parameters: { name: 'test', isSensitive: false }, - user: alice, - }); - const sensitiveChannel = await successfulApiCall({ - endpoint: 'channels/create', - parameters: { name: 'test', isSensitive: true }, - user: alice, - }); - - const noteInLocal = await post(bob, { text: `test ${keyword}` }); - const noteInNonSensitiveChannel = await post(bob, { text: `test ${keyword}`, channelId: nonSensitiveChannel.id }); - await post(bob, { text: `test ${keyword}`, channelId: sensitiveChannel.id }); - - const response = await successfulApiCall({ - endpoint: 'antennas/notes', - parameters: { antennaId: antenna.id }, - user: alice, - }); - // 最後に投稿したものが先頭に来る。 - const expected = [ - noteInNonSensitiveChannel, - noteInLocal, - ]; - assert.deepStrictEqual(response, expected); - }); - - test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { }); test.each([ { label: 'ID指定', offsetBy: 'id' }, @@ -698,7 +641,7 @@ describe('アンテナ', () => { endpoint: 'antennas/notes', parameters: { antennaId: antenna.id, ...paginationParam }, user: alice, - }); + }) as any as Note[]; }, offsetBy, 'desc'); }); diff --git a/packages/backend/test/e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts index 2dd645d97a..f781559d50 100644 --- a/packages/backend/test/e2e/api-visibility.ts +++ b/packages/backend/test/e2e/api-visibility.ts @@ -1,61 +1,67 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { UserToken, api, post, signup } from '../utils.js'; +import { signup, api, post, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; describe('API visibility', () => { + let app: INestApplicationContext; + + beforeAll(async () => { + app = await startServer(); + }, 1000 * 60 * 2); + + afterAll(async () => { + await app.close(); + }); + describe('Note visibility', () => { //#region vars /** ヒロイン */ - let alice: misskey.entities.SignupResponse; + let alice: misskey.entities.MeSignup; /** フォロワー */ - let follower: misskey.entities.SignupResponse; + let follower: misskey.entities.MeSignup; /** 非フォロワー */ - let other: misskey.entities.SignupResponse; + let other: misskey.entities.MeSignup; /** 非フォロワーでもリプライやメンションをされた人 */ - let target: misskey.entities.SignupResponse; + let target: misskey.entities.MeSignup; /** specified mentionでmentionを飛ばされる人 */ - let target2: misskey.entities.SignupResponse; + let target2: misskey.entities.MeSignup; /** public-post */ - let pub: misskey.entities.Note; + let pub: any; /** home-post */ - let home: misskey.entities.Note; + let home: any; /** followers-post */ - let fol: misskey.entities.Note; + let fol: any; /** specified-post */ - let spe: misskey.entities.Note; + let spe: any; /** public-reply to target's post */ - let pubR: misskey.entities.Note; + let pubR: any; /** home-reply to target's post */ - let homeR: misskey.entities.Note; + let homeR: any; /** followers-reply to target's post */ - let folR: misskey.entities.Note; + let folR: any; /** specified-reply to target's post */ - let speR: misskey.entities.Note; + let speR: any; /** public-mention to target */ - let pubM: misskey.entities.Note; + let pubM: any; /** home-mention to target */ - let homeM: misskey.entities.Note; + let homeM: any; /** followers-mention to target */ - let folM: misskey.entities.Note; + let folM: any; /** specified-mention to target */ - let speM: misskey.entities.Note; + let speM: any; /** reply target post */ - let tgt: misskey.entities.Note; + let tgt: any; //#endregion - const show = async (noteId: misskey.entities.Note['id'], by?: UserToken) => { - return await api('notes/show', { + const show = async (noteId: any, by: any) => { + return await api('/notes/show', { noteId, }, by); }; @@ -70,7 +76,7 @@ describe('API visibility', () => { target2 = await signup({ username: 'target2' }); // follow alice <= follower - await api('following/create', { userId: alice.id }, follower); + await api('/following/create', { userId: alice.id }, follower); // normal posts pub = await post(alice, { text: 'x', visibility: 'public' }); @@ -111,7 +117,7 @@ describe('API visibility', () => { }); test('[show] public-postを未認証が見れる', async () => { - const res = await show(pub.id); + const res = await show(pub.id, null); assert.strictEqual(res.body.text, 'x'); }); @@ -132,7 +138,7 @@ describe('API visibility', () => { }); test('[show] home-postを未認証が見れる', async () => { - const res = await show(home.id); + const res = await show(home.id, null); assert.strictEqual(res.body.text, 'x'); }); @@ -153,7 +159,7 @@ describe('API visibility', () => { }); test('[show] followers-postを未認証が見れない', async () => { - const res = await show(fol.id); + const res = await show(fol.id, null); assert.strictEqual(res.body.isHidden, true); }); @@ -179,7 +185,7 @@ describe('API visibility', () => { }); test('[show] specified-postを未認証が見れない', async () => { - const res = await show(spe.id); + const res = await show(spe.id, null); assert.strictEqual(res.body.isHidden, true); }); //#endregion @@ -207,7 +213,7 @@ describe('API visibility', () => { }); test('[show] public-replyを未認証が見れる', async () => { - const res = await show(pubR.id); + const res = await show(pubR.id, null); assert.strictEqual(res.body.text, 'x'); }); @@ -233,7 +239,7 @@ describe('API visibility', () => { }); test('[show] home-replyを未認証が見れる', async () => { - const res = await show(homeR.id); + const res = await show(homeR.id, null); assert.strictEqual(res.body.text, 'x'); }); @@ -259,7 +265,7 @@ describe('API visibility', () => { }); test('[show] followers-replyを未認証が見れない', async () => { - const res = await show(folR.id); + const res = await show(folR.id, null); assert.strictEqual(res.body.isHidden, true); }); @@ -290,7 +296,7 @@ describe('API visibility', () => { }); test('[show] specified-replyを未認証が見れない', async () => { - const res = await show(speR.id); + const res = await show(speR.id, null); assert.strictEqual(res.body.isHidden, true); }); //#endregion @@ -318,7 +324,7 @@ describe('API visibility', () => { }); test('[show] public-mentionを未認証が見れる', async () => { - const res = await show(pubM.id); + const res = await show(pubM.id, null); assert.strictEqual(res.body.text, '@target x'); }); @@ -344,7 +350,7 @@ describe('API visibility', () => { }); test('[show] home-mentionを未認証が見れる', async () => { - const res = await show(homeM.id); + const res = await show(homeM.id, null); assert.strictEqual(res.body.text, '@target x'); }); @@ -370,7 +376,7 @@ describe('API visibility', () => { }); test('[show] followers-mentionを未認証が見れない', async () => { - const res = await show(folM.id); + const res = await show(folM.id, null); assert.strictEqual(res.body.isHidden, true); }); @@ -401,69 +407,69 @@ describe('API visibility', () => { }); test('[show] specified-mentionを未認証が見れない', async () => { - const res = await show(speM.id); + const res = await show(speM.id, null); assert.strictEqual(res.body.isHidden, true); }); //#endregion //#region HTL test('[HTL] public-post が 自分が見れる', async () => { - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === pub.id); + const notes = res.body.filter((n: any) => n.id === pub.id); assert.strictEqual(notes[0].text, 'x'); }); test('[HTL] public-post が 非フォロワーから見れない', async () => { - const res = await api('notes/timeline', { limit: 100 }, other); + const res = await api('/notes/timeline', { limit: 100 }, other); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === pub.id); + const notes = res.body.filter((n: any) => n.id === pub.id); assert.strictEqual(notes.length, 0); }); test('[HTL] followers-post が フォロワーから見れる', async () => { - const res = await api('notes/timeline', { limit: 100 }, follower); + const res = await api('/notes/timeline', { limit: 100 }, follower); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === fol.id); + const notes = res.body.filter((n: any) => n.id === fol.id); assert.strictEqual(notes[0].text, 'x'); }); //#endregion //#region RTL test('[replies] followers-reply が フォロワーから見れる', async () => { - const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, follower); + const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === folR.id); + const notes = res.body.filter((n: any) => n.id === folR.id); assert.strictEqual(notes[0].text, 'x'); }); test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => { - const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other); + const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, other); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === folR.id); + const notes = res.body.filter((n: any) => n.id === folR.id); assert.strictEqual(notes.length, 0); }); test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { - const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, target); + const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, target); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === folR.id); + const notes = res.body.filter((n: any) => n.id === folR.id); assert.strictEqual(notes[0].text, 'x'); }); //#endregion //#region MTL test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { - const res = await api('notes/mentions', { limit: 100 }, target); + const res = await api('/notes/mentions', { limit: 100 }, target); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === folR.id); + const notes = res.body.filter((n: any) => n.id === folR.id); assert.strictEqual(notes[0].text, 'x'); }); test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => { - const res = await api('notes/mentions', { limit: 100 }, target); + const res = await api('/notes/mentions', { limit: 100 }, target); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === folM.id); + const notes = res.body.filter((n: any) => n.id === folM.id); assert.strictEqual(notes[0].text, '@target x'); }); //#endregion diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts index 49c6a0636b..c9182940f1 100644 --- a/packages/backend/test/e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -1,54 +1,46 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { IncomingMessage } from 'http'; -import { - api, - connectStream, - createAppToken, - failedApiCall, - relativeFetch, - signup, - successfulApiCall, - uploadFile, - waitFire, -} from '../utils.js'; +import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; +import { IncomingMessage } from 'http'; describe('API', () => { - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; + let app: INestApplicationContext; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + describe('General validation', () => { test('wrong type', async () => { - const res = await api('test', { + const res = await api('/test', { required: true, - // @ts-expect-error string must be string string: 42, }); assert.strictEqual(res.status, 400); }); test('missing require param', async () => { - // @ts-expect-error required is required - const res = await api('test', { + const res = await api('/test', { string: 'a', }); assert.strictEqual(res.status, 400); }); test('invalid misskey:id (empty string)', async () => { - const res = await api('test', { + const res = await api('/test', { required: true, id: '', }); @@ -56,7 +48,7 @@ describe('API', () => { }); test('valid misskey:id', async () => { - const res = await api('test', { + const res = await api('/test', { required: true, id: '8wvhjghbxu', }); @@ -64,7 +56,7 @@ describe('API', () => { }); test('default value', async () => { - const res = await api('test', { + const res = await api('/test', { required: true, string: 'a', }); @@ -73,7 +65,7 @@ describe('API', () => { }); test('can set null even if it has default value', async () => { - const res = await api('test', { + const res = await api('/test', { required: true, nullableDefault: null, }); @@ -82,7 +74,7 @@ describe('API', () => { }); test('cannot set undefined if it has default value', async () => { - const res = await api('test', { + const res = await api('/test', { required: true, nullableDefault: undefined, }); @@ -92,21 +84,16 @@ describe('API', () => { }); test('管理者専用のAPIのアクセス制限', async () => { - const application = await createAppToken(alice, ['read:account']); - const application2 = await createAppToken(alice, ['read:admin:index-stats']); - const application3 = await createAppToken(bob, []); - const application4 = await createAppToken(bob, ['read:admin:index-stats']); - // aliceは管理者、APIを使える await successfulApiCall({ - endpoint: 'admin/get-index-stats', + endpoint: '/admin/get-index-stats', parameters: {}, user: alice, }); // bobは一般ユーザーだからダメ await failedApiCall({ - endpoint: 'admin/get-index-stats', + endpoint: '/admin/get-index-stats', parameters: {}, user: bob, }, { @@ -117,7 +104,7 @@ describe('API', () => { // publicアクセスももちろんダメ await failedApiCall({ - endpoint: 'admin/get-index-stats', + endpoint: '/admin/get-index-stats', parameters: {}, user: undefined, }, { @@ -128,7 +115,7 @@ describe('API', () => { // ごまがしもダメ await failedApiCall({ - endpoint: 'admin/get-index-stats', + endpoint: '/admin/get-index-stats', parameters: {}, user: { token: 'tsukawasete' }, }, { @@ -136,48 +123,12 @@ describe('API', () => { code: 'AUTHENTICATION_FAILED', id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', }); - - await successfulApiCall({ - endpoint: 'admin/get-index-stats', - parameters: {}, - user: { token: application2 }, - }); - - await failedApiCall({ - endpoint: 'admin/get-index-stats', - parameters: {}, - user: { token: application }, - }, { - status: 403, - code: 'PERMISSION_DENIED', - id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838', - }); - - await failedApiCall({ - endpoint: 'admin/get-index-stats', - parameters: {}, - user: { token: application3 }, - }, { - status: 403, - code: 'ROLE_PERMISSION_DENIED', - id: 'c3d38592-54c0-429d-be96-5636b0431a61', - }); - - await failedApiCall({ - endpoint: 'admin/get-index-stats', - parameters: {}, - user: { token: application4 }, - }, { - status: 403, - code: 'ROLE_PERMISSION_DENIED', - id: 'c3d38592-54c0-429d-be96-5636b0431a61', - }); }); describe('Authentication header', () => { test('一般リクエスト', async () => { await successfulApiCall({ - endpoint: 'admin/get-index-stats', + endpoint: '/admin/get-index-stats', parameters: {}, user: { token: alice.token, @@ -211,7 +162,7 @@ describe('API', () => { describe('tokenエラー応答でWWW-Authenticate headerを送る', () => { describe('invalid_token', () => { test('一般リクエスト', async () => { - const result = await api('admin/get-index-stats', {}, { + const result = await api('/admin/get-index-stats', {}, { token: 'syuilo', bearer: true, }); @@ -246,7 +197,7 @@ describe('API', () => { describe('tokenがないとrealmだけおくる', () => { test('一般リクエスト', async () => { - const result = await api('admin/get-index-stats', {}); + const result = await api('/admin/get-index-stats', {}); assert.strictEqual(result.status, 401); assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="Misskey"'); }); @@ -259,8 +210,7 @@ describe('API', () => { }); test('invalid_request', async () => { - // @ts-expect-error text must be string - const result = await api('notes/create', { text: true }, { + const result = await api('/notes/create', { text: true }, { token: alice.token, bearer: true, }); diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts index 35b0e59383..8357884092 100644 --- a/packages/backend/test/e2e/block.ts +++ b/packages/backend/test/e2e/block.ts @@ -1,28 +1,31 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { api, castAsError, post, signup } from '../utils.js'; +import { signup, api, post, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; describe('Block', () => { + let app: INestApplicationContext; + // alice blocks bob - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - let carol: misskey.entities.SignupResponse; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + test('Block作成', async () => { - const res = await api('blocking/create', { + const res = await api('/blocking/create', { userId: bob.id, }, alice); @@ -30,39 +33,37 @@ describe('Block', () => { }); test('ブロックされているユーザーをフォローできない', async () => { - const res = await api('following/create', { userId: alice.id }, bob); + const res = await api('/following/create', { userId: alice.id }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); + assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); }); test('ブロックされているユーザーにリアクションできない', async () => { const note = await post(alice, { text: 'hello' }); - const res = await api('notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); + const res = await api('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); assert.strictEqual(res.status, 400); - assert.ok(res.body); - assert.strictEqual(castAsError(res.body).error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); + assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); }); test('ブロックされているユーザーに返信できない', async () => { const note = await post(alice, { text: 'hello' }); - const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob); + const res = await api('/notes/create', { replyId: note.id, text: 'yo' }, bob); assert.strictEqual(res.status, 400); - assert.ok(res.body); - assert.strictEqual(castAsError(res.body).error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); + assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); }); test('ブロックされているユーザーのノートをRenoteできない', async () => { const note = await post(alice, { text: 'hello' }); - const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob); + const res = await api('/notes/create', { renoteId: note.id, text: 'yo' }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); + assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); }); // TODO: ユーザーリストに入れられないテスト @@ -74,13 +75,12 @@ describe('Block', () => { const bobNote = await post(bob, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' }); - const res = await api('notes/local-timeline', {}, bob); - const body = res.body as misskey.entities.Note[]; + const res = await api('/notes/local-timeline', {}, bob); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(body.some(note => note.id === aliceNote.id), false); - assert.strictEqual(body.some(note => note.id === bobNote.id), true); - assert.strictEqual(body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); }); }); diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts index 570cc61c4b..175f2cac97 100644 --- a/packages/backend/test/e2e/clips.ts +++ b/packages/backend/test/e2e/clips.ts @@ -1,39 +1,59 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { JTDDataType } from 'ajv/dist/jtd'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; -import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js'; -import type * as Misskey from 'misskey-js'; - -type Optional = Pick, K> & Omit; +import type { Packed } from '@/misc/json-schema.js'; +import { paramDef as CreateParamDef } from '@/server/api/endpoints/clips/create.js'; +import { paramDef as UpdateParamDef } from '@/server/api/endpoints/clips/update.js'; +import { paramDef as DeleteParamDef } from '@/server/api/endpoints/clips/delete.js'; +import { paramDef as ShowParamDef } from '@/server/api/endpoints/clips/show.js'; +import { paramDef as FavoriteParamDef } from '@/server/api/endpoints/clips/favorite.js'; +import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unfavorite.js'; +import { paramDef as AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js'; +import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-note.js'; +import { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js'; +import { + signup, + post, + startServer, + api, + successfulApiCall, + failedApiCall, + ApiRequest, + hiddenNote, +} from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('クリップ', () => { - let alice: Misskey.entities.SignupResponse; - let bob: Misskey.entities.SignupResponse; - let aliceNote: Misskey.entities.Note; - let aliceHomeNote: Misskey.entities.Note; - let aliceFollowersNote: Misskey.entities.Note; - let aliceSpecifiedNote: Misskey.entities.Note; - let bobNote: Misskey.entities.Note; - let bobHomeNote: Misskey.entities.Note; - let bobFollowersNote: Misskey.entities.Note; - let bobSpecifiedNote: Misskey.entities.Note; + type User = Packed<'User'>; + type Note = Packed<'Note'>; + type Clip = Packed<'Clip'>; + + let app: INestApplicationContext; + + let alice: User; + let bob: User; + let aliceNote: Note; + let aliceHomeNote: Note; + let aliceFollowersNote: Note; + let aliceSpecifiedNote: Note; + let bobNote: Note; + let bobHomeNote: Note; + let bobFollowersNote: Note; + let bobSpecifiedNote: Note; const compareBy = (selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => { return selector(a).localeCompare(selector(b)); }; - const defaultCreate = (): Pick => ({ + type CreateParam = JTDDataType; + const defaultCreate = (): Partial => ({ name: 'test', }); - const create = async (parameters: Partial = {}, request: Partial> = {}): Promise => { - const clip = await successfulApiCall({ - endpoint: 'clips/create', + const create = async (parameters: Partial = {}, request: Partial = {}): Promise => { + const clip = await successfulApiCall({ + endpoint: '/clips/create', parameters: { ...defaultCreate(), ...parameters, @@ -51,16 +71,17 @@ describe('クリップ', () => { return clip; }; - const createMany = async (parameters: Partial, count = 10, user = alice): Promise => { + const createMany = async (parameters: Partial, count = 10, user = alice): Promise => { return await Promise.all([...Array(count)].map((_, i) => create({ name: `test${i}`, ...parameters, }, { user }))); }; - const update = async (parameters: Optional, request: Partial> = {}): Promise => { - const clip = await successfulApiCall({ - endpoint: 'clips/update', + type UpdateParam = JTDDataType; + const update = async (parameters: Partial, request: Partial = {}): Promise => { + const clip = await successfulApiCall({ + endpoint: '/clips/update', parameters: { name: 'updated', ...parameters, @@ -78,9 +99,10 @@ describe('クリップ', () => { return clip; }; - const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial> = {}): Promise => { - await successfulApiCall({ - endpoint: 'clips/delete', + type DeleteParam = JTDDataType; + const deleteClip = async (parameters: DeleteParam, request: Partial = {}): Promise => { + return await successfulApiCall({ + endpoint: '/clips/delete', parameters, user: alice, ...request, @@ -89,53 +111,60 @@ describe('クリップ', () => { }); }; - const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial> = {}): Promise => { - return await successfulApiCall({ - endpoint: 'clips/show', + type ShowParam = JTDDataType; + const show = async (parameters: ShowParam, request: Partial = {}): Promise => { + return await successfulApiCall({ + endpoint: '/clips/show', parameters, user: alice, ...request, }); }; - const list = async (request: Partial>): Promise => { - return successfulApiCall({ - endpoint: 'clips/list', + const list = async (request: Partial): Promise => { + return successfulApiCall({ + endpoint: '/clips/list', parameters: {}, user: alice, ...request, }); }; - const usersClips = async (parameters: Misskey.entities.UsersClipsRequest, request: Partial> = {}): Promise => { - return await successfulApiCall({ - endpoint: 'users/clips', - parameters, + const usersClips = async (request: Partial): Promise => { + return await successfulApiCall({ + endpoint: '/users/clips', + parameters: {}, user: alice, ...request, }); }; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); - aliceNote = await post(alice, { text: 'test' }); - aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }); - aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }); - aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }); - bobNote = await post(bob, { text: 'test' }); - bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }); - bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }); - bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }); + // FIXME: misskey-jsのNoteはoutdatedなので直接変換できない + aliceNote = await post(alice, { text: 'test' }) as any; + aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }) as any; + aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }) as any; + aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }) as any; + bobNote = await post(bob, { text: 'test' }) as any; + bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }) as any; + bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }) as any; + bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any; }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + afterEach(async () => { // テスト間で影響し合わないように毎回全部消す。 for (const user of [alice, bob]) { - const list = await api('clips/list', { limit: 11 }, user); + const list = await api('/clips/list', { limit: 11 }, user); for (const clip of list.body) { - await api('clips/delete', { clipId: clip.id }, user); + await api('/clips/delete', { clipId: clip.id }, user); } } }); @@ -153,13 +182,14 @@ describe('クリップ', () => { }); test('の作成はポリシーで定められた数以上はできない。', async () => { - const clipLimit = DEFAULT_POLICIES.clipLimit; + // ポリシー + 1まで作れるという所がミソ + const clipLimit = DEFAULT_POLICIES.clipLimit + 1; for (let i = 0; i < clipLimit; i++) { await create(); } await failedApiCall({ - endpoint: 'clips/create', + endpoint: '/clips/create', parameters: defaultCreate(), user: alice, }, { @@ -182,11 +212,11 @@ describe('クリップ', () => { { label: 'nameがnull', parameters: { name: null } }, { label: 'nameが最大長+1', parameters: { name: 'x'.repeat(101) } }, { label: 'isPublicがboolじゃない', parameters: { isPublic: 'true' } }, + { label: 'descriptionがゼロ長', parameters: { description: '' } }, { label: 'descriptionが最大長+1', parameters: { description: 'a'.repeat(2049) } }, ]; test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({ - endpoint: 'clips/create', - // @ts-expect-error invalid params + endpoint: '/clips/create', parameters: { ...defaultCreate(), ...parameters, @@ -198,23 +228,6 @@ describe('クリップ', () => { id: '3d81ceae-475f-4600-b2a8-2bc116157532', })); - test('の作成はdescriptionが空文字ならnullになる', async () => { - const clip = await successfulApiCall({ - endpoint: 'clips/create', - parameters: { - ...defaultCreate(), - description: '', - }, - user: alice, - }); - - assert.deepStrictEqual(clip, { - ...clip, - ...defaultCreate(), - description: null, - }); - }); - test('の更新ができる', async () => { const res = await update({ clipId: (await create()).id, @@ -245,15 +258,15 @@ describe('クリップ', () => { code: 'NO_SUCH_CLIP', id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', } }, - { label: '他人のクリップ', user: () => bob, assertion: { + { label: '他人のクリップ', user: (): User => bob, assertion: { code: 'NO_SUCH_CLIP', id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', } }, ...createClipDenyPattern as any, ])('の更新は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ - endpoint: 'clips/update', + endpoint: '/clips/update', parameters: { - clipId: (await create({}, { user: (user ?? (() => alice))() })).id, + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, name: 'updated', ...parameters, }, @@ -265,24 +278,6 @@ describe('クリップ', () => { ...assertion, })); - test('の更新はdescriptionが空文字ならnullになる', async () => { - const clip = await successfulApiCall({ - endpoint: 'clips/update', - parameters: { - clipId: (await create()).id, - name: 'updated', - description: '', - }, - user: alice, - }); - - assert.deepStrictEqual(clip, { - ...clip, - name: 'updated', - description: null, - }); - }); - test('の削除ができる', async () => { await deleteClip({ clipId: (await create()).id, @@ -296,15 +291,14 @@ describe('クリップ', () => { code: 'NO_SUCH_CLIP', id: '70ca08ba-6865-4630-b6fb-8494759aa754', } }, - { label: '他人のクリップ', user: () => bob, assertion: { + { label: '他人のクリップ', user: (): User => bob, assertion: { code: 'NO_SUCH_CLIP', id: '70ca08ba-6865-4630-b6fb-8494759aa754', } }, ])('の削除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ - endpoint: 'clips/delete', + endpoint: '/clips/delete', parameters: { - // @ts-expect-error clipId must not be null - clipId: (await create({}, { user: (user ?? (() => alice))() })).id, + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, ...parameters, }, user: alice, @@ -324,7 +318,7 @@ describe('クリップ', () => { test('のID指定取得は他人のPrivateなクリップは取得できない', async () => { const clip = await create({ isPublic: false }, { user: bob } ); failedApiCall({ - endpoint: 'clips/show', + endpoint: '/clips/show', parameters: { clipId: clip.id }, user: alice, }, { @@ -341,8 +335,7 @@ describe('クリップ', () => { id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20', } }, ])('のID指定取得は$labelならできない', async ({ parameters, assetion }) => failedApiCall({ - endpoint: 'clips/show', - // @ts-expect-error clipId must not be undefined + endpoint: '/clips/show', parameters: { ...parameters, }, @@ -360,7 +353,7 @@ describe('クリップ', () => { }); test('の一覧(clips/list)が取得できる(上限いっぱい)', async () => { - const clipLimit = DEFAULT_POLICIES.clipLimit; + const clipLimit = DEFAULT_POLICIES.clipLimit + 1; const clips = await createMany({}, clipLimit); const res = await list({ parameters: { limit: 1 }, // FIXME: 無視されて11全部返ってくる @@ -375,23 +368,27 @@ describe('クリップ', () => { test('の一覧が取得できる(空)', async () => { const res = await usersClips({ - userId: alice.id, + parameters: { + userId: alice.id, + }, }); assert.deepStrictEqual(res, []); }); test.each([ { label: '' }, - { label: '他人アカウントから', user: () => bob }, + { label: '他人アカウントから', user: (): User => bob }, ])('の一覧が$label取得できる', async () => { const clips = await createMany({ isPublic: true }); const res = await usersClips({ - userId: alice.id, + parameters: { + userId: alice.id, + }, }); // 返ってくる配列には順序保障がないのでidでソートして厳密比較 assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), + res.sort(compareBy(s => s.id)), clips.sort(compareBy(s => s.id))); // 認証状態で見たときだけisFavoritedが入っている @@ -401,16 +398,17 @@ describe('クリップ', () => { }); test.each([ - { label: '未認証', user: () => undefined }, + { label: '未認証', user: (): undefined => undefined }, { label: '存在しないユーザーのもの', parameters: { userId: 'xxxxxxx' } }, ])('の一覧は$labelでも取得できる', async ({ parameters, user }) => { const clips = await createMany({ isPublic: true }); const res = await usersClips({ - userId: alice.id, - limit: clips.length, - ...parameters, - }, { - user: (user ?? (() => alice))(), + parameters: { + userId: alice.id, + limit: clips.length, + ...parameters, + }, + user: (user ?? ((): User => alice))(), }); // 未認証で見たときはisFavoritedは入らない @@ -423,8 +421,10 @@ describe('クリップ', () => { await create({ isPublic: false }); const aliceClip = await create({ isPublic: true }); const res = await usersClips({ - userId: alice.id, - limit: 2, + parameters: { + userId: alice.id, + limit: 2, + }, }); assert.deepStrictEqual(res, [aliceClip]); }); @@ -433,15 +433,17 @@ describe('クリップ', () => { const clips = await createMany({ isPublic: true }, 7); clips.sort(compareBy(s => s.id)); const res = await usersClips({ - userId: alice.id, - sinceId: clips[1].id, - untilId: clips[5].id, - limit: 4, + parameters: { + userId: alice.id, + sinceId: clips[1].id, + untilId: clips[5].id, + limit: 4, + }, }); // Promise.allで返ってくる配列には順序保障がないのでidでソートして厳密比較 assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), + res.sort(compareBy(s => s.id)), [clips[2], clips[3], clips[4]], // sinceIdとuntilId自体は結果に含まれない clips[1].id + ' ... ' + clips[3].id + ' with ' + clips.map(s => s.id) + ' vs. ' + res.map(s => s.id)); }); @@ -451,9 +453,8 @@ describe('クリップ', () => { { label: 'limitゼロ', parameters: { limit: 0 } }, { label: 'limit最大+1', parameters: { limit: 101 } }, ])('の一覧は$labelだと取得できない', async ({ parameters }) => failedApiCall({ - endpoint: 'users/clips', + endpoint: '/users/clips', parameters: { - // @ts-expect-error userId must not be undefined userId: alice.id, ...parameters, }, @@ -465,15 +466,15 @@ describe('クリップ', () => { })); test.each([ - { label: '作成', endpoint: 'clips/create' as const }, - { label: '更新', endpoint: 'clips/update' as const }, - { label: '削除', endpoint: 'clips/delete' as const }, - { label: '取得', endpoint: 'clips/list' as const }, - { label: 'お気に入り設定', endpoint: 'clips/favorite' as const }, - { label: 'お気に入り解除', endpoint: 'clips/unfavorite' as const }, - { label: 'お気に入り取得', endpoint: 'clips/my-favorites' as const }, - { label: 'ノート追加', endpoint: 'clips/add-note' as const }, - { label: 'ノート削除', endpoint: 'clips/remove-note' as const }, + { label: '作成', endpoint: '/clips/create' }, + { label: '更新', endpoint: '/clips/update' }, + { label: '削除', endpoint: '/clips/delete' }, + { label: '取得', endpoint: '/clips/list' }, + { label: 'お気に入り設定', endpoint: '/clips/favorite' }, + { label: 'お気に入り解除', endpoint: '/clips/unfavorite' }, + { label: 'お気に入り取得', endpoint: '/clips/my-favorites' }, + { label: 'ノート追加', endpoint: '/clips/add-note' }, + { label: 'ノート削除', endpoint: '/clips/remove-note' }, ])('の$labelは未認証ではできない', async ({ endpoint }) => await failedApiCall({ endpoint: endpoint, parameters: {}, @@ -485,11 +486,12 @@ describe('クリップ', () => { })); describe('のお気に入り', () => { - let aliceClip: Misskey.entities.Clip; + let aliceClip: Clip; - const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial> = {}): Promise => { - await successfulApiCall({ - endpoint: 'clips/favorite', + type FavoriteParam = JTDDataType; + const favorite = async (parameters: FavoriteParam, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/favorite', parameters, user: alice, ...request, @@ -498,9 +500,10 @@ describe('クリップ', () => { }); }; - const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial> = {}): Promise => { - await successfulApiCall({ - endpoint: 'clips/unfavorite', + type UnfavoriteParam = JTDDataType; + const unfavorite = async (parameters: UnfavoriteParam, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/unfavorite', parameters, user: alice, ...request, @@ -509,9 +512,9 @@ describe('クリップ', () => { }); }; - const myFavorites = async (request: Partial> = {}): Promise => { - return successfulApiCall({ - endpoint: 'clips/my-favorites', + const myFavorites = async (request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/my-favorites', parameters: {}, user: alice, ...request, @@ -577,7 +580,7 @@ describe('クリップ', () => { test('は同じクリップに対して二回設定できない。', async () => { await favorite({ clipId: aliceClip.id }); await failedApiCall({ - endpoint: 'clips/favorite', + endpoint: '/clips/favorite', parameters: { clipId: aliceClip.id, }, @@ -595,15 +598,14 @@ describe('クリップ', () => { code: 'NO_SUCH_CLIP', id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', } }, - { label: '他人のクリップ', user: () => bob, assertion: { + { label: '他人のクリップ', user: (): User => bob, assertion: { code: 'NO_SUCH_CLIP', id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', } }, ])('の設定は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ - endpoint: 'clips/favorite', + endpoint: '/clips/favorite', parameters: { - // @ts-expect-error clipId must not be null - clipId: (await create({}, { user: (user ?? (() => alice))() })).id, + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, ...parameters, }, user: alice, @@ -629,7 +631,7 @@ describe('クリップ', () => { code: 'NO_SUCH_CLIP', id: '2603966e-b865-426c-94a7-af4a01241dc1', } }, - { label: '他人のクリップ', user: () => bob, assertion: { + { label: '他人のクリップ', user: (): User => bob, assertion: { code: 'NOT_FAVORITED', id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', } }, @@ -638,10 +640,9 @@ describe('クリップ', () => { id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', } }, ])('の設定解除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ - endpoint: 'clips/unfavorite', + endpoint: '/clips/unfavorite', parameters: { - // @ts-expect-error clipId must not be null - clipId: (await create({}, { user: (user ?? (() => alice))() })).id, + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, ...parameters, }, user: alice, @@ -666,38 +667,41 @@ describe('クリップ', () => { }); describe('に紐づくノート', () => { - let aliceClip: Misskey.entities.Clip; + let aliceClip: Clip; - const sampleNotes = (): Misskey.entities.Note[] => [ + const sampleNotes = (): Note[] => [ aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote, bobNote, bobHomeNote, bobFollowersNote, bobSpecifiedNote, ]; - const addNote = async (parameters: Misskey.entities.ClipsAddNoteRequest, request: Partial> = {}): Promise => { - return successfulApiCall({ - endpoint: 'clips/add-note', + type AddNoteParam = JTDDataType; + const addNote = async (parameters: AddNoteParam, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/add-note', parameters, user: alice, ...request, }, { status: 204, - }) as any as void; + }); }; - const removeNote = async (parameters: Misskey.entities.ClipsRemoveNoteRequest, request: Partial> = {}): Promise => { - return successfulApiCall({ - endpoint: 'clips/remove-note', + type RemoveNoteParam = JTDDataType; + const removeNote = async (parameters: RemoveNoteParam, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/remove-note', parameters, user: alice, ...request, }, { status: 204, - }) as any as void; + }); }; - const notes = async (parameters: Misskey.entities.ClipsNotesRequest, request: Partial> = {}): Promise => { - return successfulApiCall({ - endpoint: 'clips/notes', + type NotesParam = JTDDataType; + const notes = async (parameters: Partial, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/notes', parameters, user: alice, ...request, @@ -711,8 +715,8 @@ describe('クリップ', () => { test('を追加できる。', async () => { await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); const res = await show({ clipId: aliceClip.id }); - assert.strictEqual(res.lastClippedAt, res.lastClippedAt ? new Date(res.lastClippedAt).toISOString() : null); - assert.deepStrictEqual((await notes({ clipId: aliceClip.id })).map(x => x.id), [aliceNote.id]); + assert.strictEqual(res.lastClippedAt, new Date(res.lastClippedAt ?? '').toISOString()); + assert.deepStrictEqual(await notes({ clipId: aliceClip.id }), [aliceNote]); // 他人の非公開ノートも突っ込める await addNote({ clipId: aliceClip.id, noteId: bobHomeNote.id }); @@ -723,7 +727,7 @@ describe('クリップ', () => { test('として同じノートを二回紐づけることはできない', async () => { await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); await failedApiCall({ - endpoint: 'clips/add-note', + endpoint: '/clips/add-note', parameters: { clipId: aliceClip.id, noteId: aliceNote.id, @@ -738,14 +742,14 @@ describe('クリップ', () => { // TODO: 17000msくらいかかる... test('をポリシーで定められた上限いっぱい(200)を超えて追加はできない。', async () => { - const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit; + const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1; const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, { text: `test ${i}`, - }) as unknown)) as Misskey.entities.Note[]; + }) as unknown)) as Note[]; await Promise.all(noteList.map(s => addNote({ clipId: aliceClip.id, noteId: s.id }))); await failedApiCall({ - endpoint: 'clips/add-note', + endpoint: '/clips/add-note', parameters: { clipId: aliceClip.id, noteId: aliceNote.id, @@ -759,7 +763,7 @@ describe('クリップ', () => { }); test('は他人のクリップへ追加できない。', async () => await failedApiCall({ - endpoint: 'clips/add-note', + endpoint: '/clips/add-note', parameters: { clipId: aliceClip.id, noteId: aliceNote.id, @@ -782,20 +786,18 @@ describe('クリップ', () => { code: 'NO_SUCH_NOTE', id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b', } }, - { label: '他人のクリップ', user: () => bob, assetion: { + { label: '他人のクリップ', user: (): object => bob, assetion: { code: 'NO_SUCH_CLIP', id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', } }, ])('の追加は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({ - endpoint: 'clips/add-note', + endpoint: '/clips/add-note', parameters: { - // @ts-expect-error clipId must not be undefined clipId: aliceClip.id, - // @ts-expect-error noteId must not be undefined noteId: aliceNote.id, ...parameters, }, - user: (user ?? (() => alice))(), + user: (user ?? ((): User => alice))(), }, { status: 400, code: 'INVALID_PARAM', @@ -820,20 +822,18 @@ describe('クリップ', () => { code: 'NO_SUCH_NOTE', id: 'aff017de-190e-434b-893e-33a9ff5049d8', // add-noteと異なる } }, - { label: '他人のクリップ', user: () => bob, assetion: { + { label: '他人のクリップ', user: (): object => bob, assetion: { code: 'NO_SUCH_CLIP', id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteと異なる } }, ])('の削除は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({ - endpoint: 'clips/remove-note', + endpoint: '/clips/remove-note', parameters: { - // @ts-expect-error clipId must not be undefined clipId: aliceClip.id, - // @ts-expect-error noteId must not be undefined noteId: aliceNote.id, ...parameters, }, - user: (user ?? (() => alice))(), + user: (user ?? ((): User => alice))(), }, { status: 400, code: 'INVALID_PARAM', @@ -856,8 +856,8 @@ describe('クリップ', () => { bobNote, bobHomeNote, ]; assert.deepStrictEqual( - res.sort(compareBy(s => s.id)).map(x => x.id), - expects.sort(compareBy(s => s.id)).map(x => x.id)); + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); }); test('を始端IDとlimitで取得できる。', async () => { @@ -876,8 +876,8 @@ describe('クリップ', () => { // Promise.allで返ってくる配列はID順で並んでないのでソートして厳密比較 const expects = [noteList[3], noteList[4], noteList[5]]; assert.deepStrictEqual( - res.sort(compareBy(s => s.id)).map(x => x.id), - expects.sort(compareBy(s => s.id)).map(x => x.id)); + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); }); test('をID範囲指定で取得できる。', async () => { @@ -896,8 +896,8 @@ describe('クリップ', () => { // Promise.allで返ってくる配列はID順で並んでないのでソートして厳密比較 const expects = [noteList[2], noteList[3]]; assert.deepStrictEqual( - res.sort(compareBy(s => s.id)).map(x => x.id), - expects.sort(compareBy(s => s.id)).map(x => x.id)); + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); }); test.todo('Remoteのノートもクリップできる。どうテストしよう?'); @@ -906,10 +906,10 @@ describe('クリップ', () => { const bobClip = await create({ isPublic: true }, { user: bob } ); await addNote({ clipId: bobClip.id, noteId: aliceNote.id }, { user: bob }); const res = await notes({ clipId: bobClip.id }); - assert.deepStrictEqual(res.map(x => x.id), [aliceNote.id]); + assert.deepStrictEqual(res, [aliceNote]); }); - test('はPublicなクリップなら認証なしでも取得できる。(非公開ノートは含まれない)', async () => { + test('はPublicなクリップなら認証なしでも取得できる。(非公開ノートはhideされて返ってくる)', async () => { const publicClip = await create({ isPublic: true }); await addNote({ clipId: publicClip.id, noteId: aliceNote.id }); await addNote({ clipId: publicClip.id, noteId: aliceHomeNote.id }); @@ -919,10 +919,12 @@ describe('クリップ', () => { const res = await notes({ clipId: publicClip.id }, { user: undefined }); const expects = [ aliceNote, aliceHomeNote, + // 認証なしだと非公開ノートは結果には含むけどhideされる。 + hiddenNote(aliceFollowersNote), hiddenNote(aliceSpecifiedNote), ]; assert.deepStrictEqual( - res.sort(compareBy(s => s.id)).map(x => x.id), - expects.sort(compareBy(s => s.id)).map(x => x.id)); + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); }); test.todo('ブロック、ミュートされたユーザーからの設定&取得etc.'); @@ -935,22 +937,21 @@ describe('クリップ', () => { code: 'NO_SUCH_CLIP', id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', } }, - { label: '他人のPrivateなクリップから', user: () => bob, assertion: { + { label: '他人のPrivateなクリップから', user: (): object => bob, assertion: { code: 'NO_SUCH_CLIP', id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', } }, - { label: '未認証でPrivateなクリップから', user: () => undefined, assertion: { + { label: '未認証でPrivateなクリップから', user: (): undefined => undefined, assertion: { code: 'NO_SUCH_CLIP', id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', } }, ])('は$labelだと取得できない', async ({ parameters, user, assertion }) => failedApiCall({ - endpoint: 'clips/notes', + endpoint: '/clips/notes', parameters: { - // @ts-expect-error clipId must not be undefined clipId: aliceClip.id, ...parameters, }, - user: (user ?? (() => alice))(), + user: (user ?? ((): User => alice))(), }, { status: 400, code: 'INVALID_PARAM', diff --git a/packages/backend/test/e2e/drive.ts b/packages/backend/test/e2e/drive.ts deleted file mode 100644 index 43a73163eb..0000000000 --- a/packages/backend/test/e2e/drive.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import { api, makeStreamCatcher, post, signup, uploadFile } from '../utils.js'; -import type * as misskey from 'misskey-js'; - -describe('Drive', () => { - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - - beforeAll(async () => { - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - }, 1000 * 60 * 2); - - test('ファイルURLからアップロードできる', async () => { - // utils.js uploadUrl の処理だがAPIレスポンスも見るためここで同様の処理を書いている - - const marker = Math.random().toString(); - - const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg'; - - const catcher = makeStreamCatcher( - alice, - 'main', - (msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker, - (msg) => msg.body.file, - 10 * 1000); - - const res = await api('drive/files/upload-from-url', { - url, - marker, - force: true, - }, alice); - - const file = await catcher; - - assert.strictEqual(res.status, 204); - assert.strictEqual(file.name, '192.jpg'); - assert.strictEqual(file.type, 'image/jpeg'); - }); - - test('ローカルからアップロードできる', async () => { - // APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする - - const res = await uploadFile(alice, { path: '192.jpg', name: 'テスト画像' }); - - assert.strictEqual(res.body?.name, 'テスト画像.jpg'); - assert.strictEqual(res.body.type, 'image/jpeg'); - }); - - test('添付ノート一覧を取得できる', async () => { - const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id); - - const note0 = await post(alice, { fileIds: [ids[0]] }); - const note1 = await post(alice, { fileIds: [ids[0], ids[1]] }); - - const attached0 = await api('drive/files/attached-notes', { fileId: ids[0] }, alice); - assert.strictEqual(attached0.body.length, 2); - assert.strictEqual(attached0.body[0].id, note1.id); - assert.strictEqual(attached0.body[1].id, note0.id); - - const attached1 = await api('drive/files/attached-notes', { fileId: ids[1] }, alice); - assert.strictEqual(attached1.body.length, 1); - assert.strictEqual(attached1.body[0].id, note1.id); - - const attached2 = await api('drive/files/attached-notes', { fileId: ids[2] }, alice); - assert.strictEqual(attached2.body.length, 0); - }); - - test('添付ノート一覧は他の人から見えない', async () => { - const file = await uploadFile(alice); - - await post(alice, { fileIds: [file.body!.id] }); - - const res = await api('drive/files/attached-notes', { fileId: file.body!.id }, bob); - assert.strictEqual(res.status, 400); - assert.strictEqual('error' in res.body, true); - }); -}); diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index b52162a687..a1e89d4833 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -1,31 +1,34 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; // node-fetch only supports it's own Blob yet // https://github.com/node-fetch/node-fetch/pull/1664 import { Blob } from 'node-fetch'; -import { api, castAsError, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js'; +import { User } from '@/models/index.js'; +import { startServer, signup, post, api, uploadFile, simpleGet, initTestDb } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; -import { MiUser } from '@/models/_.js'; describe('Endpoints', () => { - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - let carol: misskey.entities.SignupResponse; - let dave: misskey.entities.SignupResponse; + let app: INestApplicationContext; + + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; + let dave: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); dave = await signup({ username: 'dave' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + describe('signup', () => { test('不正なユーザー名でアカウントが作成できない', async () => { const res = await api('signup', { @@ -66,9 +69,9 @@ describe('Endpoints', () => { }); }); - describe('signin-flow', () => { + describe('signin', () => { test('間違ったパスワードでサインインできない', async () => { - const res = await api('signin-flow', { + const res = await api('signin', { username: 'test1', password: 'bar', }); @@ -77,9 +80,8 @@ describe('Endpoints', () => { }); test('クエリをインジェクションできない', async () => { - const res = await api('signin-flow', { + const res = await api('signin', { username: 'test1', - // @ts-expect-error password must be string password: { $gt: '', }, @@ -89,7 +91,7 @@ describe('Endpoints', () => { }); test('正しい情報でサインインできる', async () => { - const res = await api('signin-flow', { + const res = await api('signin', { username: 'test1', password: 'test1', }); @@ -104,7 +106,7 @@ describe('Endpoints', () => { const myLocation = '七森中'; const myBirthday = '2000-09-07'; - const res = await api('i/update', { + const res = await api('/i/update', { name: myName, location: myLocation, birthday: myBirthday, @@ -117,29 +119,20 @@ describe('Endpoints', () => { assert.strictEqual(res.body.birthday, myBirthday); }); - test('名前を空白のみにした場合nullになる', async () => { - const res = await api('i/update', { + test('名前を空白にできる', async () => { + const res = await api('/i/update', { name: ' ', }, alice); assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.name, null); - }); - - test('名前の前後に空白(ホワイトスペース)を入れてもトリムされる', async () => { - const res = await api('i/update', { - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#white_space - name: ' あ い う \u0009\u000b\u000c\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff', - }, alice); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.name, 'あ い う'); + assert.strictEqual(res.body.name, ' '); }); test('誕生日の設定を削除できる', async () => { - await api('i/update', { + await api('/i/update', { birthday: '2000-09-07', }, alice); - const res = await api('i/update', { + const res = await api('/i/update', { birthday: null, }, alice); @@ -149,7 +142,7 @@ describe('Endpoints', () => { }); test('不正な誕生日の形式で怒られる', async () => { - const res = await api('i/update', { + const res = await api('/i/update', { birthday: '2000/09/07', }, alice); assert.strictEqual(res.status, 400); @@ -158,24 +151,24 @@ describe('Endpoints', () => { describe('users/show', () => { test('ユーザーが取得できる', async () => { - const res = await api('users/show', { + const res = await api('/users/show', { userId: alice.id, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual((res.body as unknown as { id: string }).id, alice.id); + assert.strictEqual(res.body.id, alice.id); }); test('ユーザーが存在しなかったら怒る', async () => { - const res = await api('users/show', { + const res = await api('/users/show', { userId: '000000000000000000000000', }); assert.strictEqual(res.status, 404); }); test('間違ったIDで怒られる', async () => { - const res = await api('users/show', { + const res = await api('/users/show', { userId: 'kyoppie', }); assert.strictEqual(res.status, 404); @@ -188,7 +181,7 @@ describe('Endpoints', () => { text: 'test', }); - const res = await api('notes/show', { + const res = await api('/notes/show', { noteId: myPost.id, }, alice); @@ -199,14 +192,14 @@ describe('Endpoints', () => { }); test('投稿が存在しなかったら怒る', async () => { - const res = await api('notes/show', { + const res = await api('/notes/show', { noteId: '000000000000000000000000', }); assert.strictEqual(res.status, 400); }); test('間違ったIDで怒られる', async () => { - const res = await api('notes/show', { + const res = await api('/notes/show', { noteId: 'kyoppie', }); assert.strictEqual(res.status, 400); @@ -217,14 +210,14 @@ describe('Endpoints', () => { test('リアクションできる', async () => { const bobPost = await post(bob, { text: 'hi' }); - const res = await api('notes/reactions/create', { + const res = await api('/notes/reactions/create', { noteId: bobPost.id, reaction: '🚀', }, alice); assert.strictEqual(res.status, 204); - const resNote = await api('notes/show', { + const resNote = await api('/notes/show', { noteId: bobPost.id, }, alice); @@ -235,7 +228,7 @@ describe('Endpoints', () => { test('自分の投稿にもリアクションできる', async () => { const myPost = await post(alice, { text: 'hi' }); - const res = await api('notes/reactions/create', { + const res = await api('/notes/reactions/create', { noteId: myPost.id, reaction: '🚀', }, alice); @@ -246,19 +239,19 @@ describe('Endpoints', () => { test('二重にリアクションすると上書きされる', async () => { const bobPost = await post(bob, { text: 'hi' }); - await api('notes/reactions/create', { + await api('/notes/reactions/create', { noteId: bobPost.id, reaction: '🥰', }, alice); - const res = await api('notes/reactions/create', { + const res = await api('/notes/reactions/create', { noteId: bobPost.id, reaction: '🚀', }, alice); assert.strictEqual(res.status, 204); - const resNote = await api('notes/show', { + const resNote = await api('/notes/show', { noteId: bobPost.id, }, alice); @@ -267,7 +260,7 @@ describe('Endpoints', () => { }); test('存在しない投稿にはリアクションできない', async () => { - const res = await api('notes/reactions/create', { + const res = await api('/notes/reactions/create', { noteId: '000000000000000000000000', reaction: '🚀', }, alice); @@ -275,77 +268,14 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 400); }); - test('リノートにリアクションできない', async () => { - const bobNote = await post(bob, { text: 'hi' }); - const bobRenote = await post(bob, { renoteId: bobNote.id }); - - const res = await api('notes/reactions/create', { - noteId: bobRenote.id, - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 400); - assert.ok(res.body); - assert.strictEqual(castAsError(res.body).error.code, 'CANNOT_REACT_TO_RENOTE'); - }); - - test('引用にリアクションできる', async () => { - const bobNote = await post(bob, { text: 'hi' }); - const bobRenote = await post(bob, { text: 'hi again', renoteId: bobNote.id }); - - const res = await api('notes/reactions/create', { - noteId: bobRenote.id, - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 204); - }); - - test('空文字列のリアクションは\u2764にフォールバックされる', async () => { - const bobNote = await post(bob, { text: 'hi' }); - - const res = await api('notes/reactions/create', { - noteId: bobNote.id, - reaction: '', - }, alice); - - assert.strictEqual(res.status, 204); - - const reaction = await api('notes/reactions', { - noteId: bobNote.id, - }); - - assert.strictEqual(reaction.body.length, 1); - assert.strictEqual(reaction.body[0].type, '\u2764'); - }); - - test('絵文字ではない文字列のリアクションは\u2764にフォールバックされる', async () => { - const bobNote = await post(bob, { text: 'hi' }); - - const res = await api('notes/reactions/create', { - noteId: bobNote.id, - reaction: 'Hello!', - }, alice); - - assert.strictEqual(res.status, 204); - - const reaction = await api('notes/reactions', { - noteId: bobNote.id, - }); - - assert.strictEqual(reaction.body.length, 1); - assert.strictEqual(reaction.body[0].type, '\u2764'); - }); - test('空のパラメータで怒られる', async () => { - // @ts-expect-error param must not be empty - const res = await api('notes/reactions/create', {}, alice); + const res = await api('/notes/reactions/create', {}, alice); assert.strictEqual(res.status, 400); }); test('間違ったIDで怒られる', async () => { - const res = await api('notes/reactions/create', { + const res = await api('/notes/reactions/create', { noteId: 'kyoppie', reaction: '🚀', }, alice); @@ -356,14 +286,14 @@ describe('Endpoints', () => { describe('following/create', () => { test('フォローできる', async () => { - const res = await api('following/create', { + const res = await api('/following/create', { userId: alice.id, }, bob); assert.strictEqual(res.status, 200); const connection = await initTestDb(true); - const Users = connection.getRepository(MiUser); + const Users = connection.getRepository(User); const newBob = await Users.findOneByOrFail({ id: bob.id }); assert.strictEqual(newBob.followersCount, 0); assert.strictEqual(newBob.followingCount, 1); @@ -374,7 +304,7 @@ describe('Endpoints', () => { }); test('既にフォローしている場合は怒る', async () => { - const res = await api('following/create', { + const res = await api('/following/create', { userId: alice.id, }, bob); @@ -382,7 +312,7 @@ describe('Endpoints', () => { }); test('存在しないユーザーはフォローできない', async () => { - const res = await api('following/create', { + const res = await api('/following/create', { userId: '000000000000000000000000', }, alice); @@ -390,7 +320,7 @@ describe('Endpoints', () => { }); test('自分自身はフォローできない', async () => { - const res = await api('following/create', { + const res = await api('/following/create', { userId: alice.id, }, alice); @@ -398,14 +328,13 @@ describe('Endpoints', () => { }); test('空のパラメータで怒られる', async () => { - // @ts-expect-error params must not be empty - const res = await api('following/create', {}, alice); + const res = await api('/following/create', {}, alice); assert.strictEqual(res.status, 400); }); test('間違ったIDで怒られる', async () => { - const res = await api('following/create', { + const res = await api('/following/create', { userId: 'foo', }, alice); @@ -415,18 +344,18 @@ describe('Endpoints', () => { describe('following/delete', () => { test('フォロー解除できる', async () => { - await api('following/create', { + await api('/following/create', { userId: alice.id, }, bob); - const res = await api('following/delete', { + const res = await api('/following/delete', { userId: alice.id, }, bob); assert.strictEqual(res.status, 200); const connection = await initTestDb(true); - const Users = connection.getRepository(MiUser); + const Users = connection.getRepository(User); const newBob = await Users.findOneByOrFail({ id: bob.id }); assert.strictEqual(newBob.followersCount, 0); assert.strictEqual(newBob.followingCount, 0); @@ -437,7 +366,7 @@ describe('Endpoints', () => { }); test('フォローしていない場合は怒る', async () => { - const res = await api('following/delete', { + const res = await api('/following/delete', { userId: alice.id, }, bob); @@ -445,7 +374,7 @@ describe('Endpoints', () => { }); test('存在しないユーザーはフォロー解除できない', async () => { - const res = await api('following/delete', { + const res = await api('/following/delete', { userId: '000000000000000000000000', }, alice); @@ -453,7 +382,7 @@ describe('Endpoints', () => { }); test('自分自身はフォロー解除できない', async () => { - const res = await api('following/delete', { + const res = await api('/following/delete', { userId: alice.id, }, alice); @@ -461,14 +390,13 @@ describe('Endpoints', () => { }); test('空のパラメータで怒られる', async () => { - // @ts-expect-error params must not be empty - const res = await api('following/delete', {}, alice); + const res = await api('/following/delete', {}, alice); assert.strictEqual(res.status, 400); }); test('間違ったIDで怒られる', async () => { - const res = await api('following/delete', { + const res = await api('/following/delete', { userId: 'kyoppie', }, alice); @@ -478,20 +406,20 @@ describe('Endpoints', () => { describe('channels/search', () => { test('空白検索で一覧を取得できる', async () => { - await api('channels/create', { + await api('/channels/create', { name: 'aaa', description: 'bbb', }, bob); - await api('channels/create', { + await api('/channels/create', { name: 'ccc1', description: 'ddd1', }, bob); - await api('channels/create', { + await api('/channels/create', { name: 'ccc2', description: 'ddd2', }, bob); - const res = await api('channels/search', { + const res = await api('/channels/search', { query: '', }, bob); @@ -500,7 +428,7 @@ describe('Endpoints', () => { assert.strictEqual(res.body.length, 3); }); test('名前のみの検索で名前を検索できる', async () => { - const res = await api('channels/search', { + const res = await api('/channels/search', { query: 'aaa', type: 'nameOnly', }, bob); @@ -511,7 +439,7 @@ describe('Endpoints', () => { assert.strictEqual(res.body[0].name, 'aaa'); }); test('名前のみの検索で名前を複数検索できる', async () => { - const res = await api('channels/search', { + const res = await api('/channels/search', { query: 'ccc', type: 'nameOnly', }, bob); @@ -521,7 +449,7 @@ describe('Endpoints', () => { assert.strictEqual(res.body.length, 2); }); test('名前のみの検索で説明は検索できない', async () => { - const res = await api('channels/search', { + const res = await api('/channels/search', { query: 'bbb', type: 'nameOnly', }, bob); @@ -531,7 +459,7 @@ describe('Endpoints', () => { assert.strictEqual(res.body.length, 0); }); test('名前と説明の検索で名前を検索できる', async () => { - const res = await api('channels/search', { + const res = await api('/channels/search', { query: 'ccc1', }, bob); @@ -541,7 +469,7 @@ describe('Endpoints', () => { assert.strictEqual(res.body[0].name, 'ccc1'); }); test('名前と説明での検索で説明を検索できる', async () => { - const res = await api('channels/search', { + const res = await api('/channels/search', { query: 'ddd1', }, bob); @@ -551,7 +479,7 @@ describe('Endpoints', () => { assert.strictEqual(res.body[0].name, 'ccc1'); }); test('名前と説明の検索で名前を複数検索できる', async () => { - const res = await api('channels/search', { + const res = await api('/channels/search', { query: 'ccc', }, bob); @@ -560,7 +488,7 @@ describe('Endpoints', () => { assert.strictEqual(res.body.length, 2); }); test('名前と説明での検索で説明を複数検索できる', async () => { - const res = await api('channels/search', { + const res = await api('/channels/search', { query: 'ddd', }, bob); @@ -572,10 +500,19 @@ describe('Endpoints', () => { describe('drive', () => { test('ドライブ情報を取得できる', async () => { - const res = await api('drive', {}, alice); + await uploadFile(alice, { + blob: new Blob([new Uint8Array(256)]), + }); + await uploadFile(alice, { + blob: new Blob([new Uint8Array(512)]), + }); + await uploadFile(alice, { + blob: new Blob([new Uint8Array(1024)]), + }); + const res = await api('/drive', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - expect(res.body).toHaveProperty('usage', 0); + expect(res.body).toHaveProperty('usage', 1792); }); }); @@ -585,7 +522,7 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body!.name, '192.jpg'); + assert.strictEqual(res.body.name, 'Lenna.jpg'); }); test('ファイルに名前を付けられる', async () => { @@ -593,7 +530,7 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body!.name, 'Belmond.jpg'); + assert.strictEqual(res.body.name, 'Belmond.jpg'); }); test('ファイルに名前を付けられるが、拡張子は正しいものになる', async () => { @@ -601,12 +538,11 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body!.name, 'Belmond.png.jpg'); + assert.strictEqual(res.body.name, 'Belmond.png.jpg'); }); test('ファイル無しで怒られる', async () => { - // @ts-expect-error params must not be empty - const res = await api('drive/files/create', {}, alice); + const res = await api('/drive/files/create', {}, alice); assert.strictEqual(res.status, 400); }); @@ -616,14 +552,14 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body!.name, 'image.svg'); - assert.strictEqual(res.body!.type, 'image/svg+xml'); + assert.strictEqual(res.body.name, 'image.svg'); + assert.strictEqual(res.body.type, 'image/svg+xml'); }); for (const type of ['webp', 'avif']) { const mediaType = `image/${type}`; - const getWebpublicType = async (user: misskey.entities.SignupResponse, fileId: string): Promise => { + const getWebpublicType = async (user: any, fileId: string): Promise => { // drive/files/create does not expose webpublicType directly, so get it by posting it const res = await post(user, { text: mediaType, @@ -640,10 +576,10 @@ describe('Endpoints', () => { const res = await uploadFile(alice, { path }); assert.strictEqual(res.status, 200); - assert.strictEqual(res.body!.name, path); - assert.strictEqual(res.body!.type, mediaType); + assert.strictEqual(res.body.name, path); + assert.strictEqual(res.body.type, mediaType); - const webpublicType = await getWebpublicType(alice, res.body!.id); + const webpublicType = await getWebpublicType(alice, res.body.id); assert.strictEqual(webpublicType, 'image/webp'); }); @@ -651,10 +587,10 @@ describe('Endpoints', () => { const path = `without-alpha.${type}`; const res = await uploadFile(alice, { path }); assert.strictEqual(res.status, 200); - assert.strictEqual(res.body!.name, path); - assert.strictEqual(res.body!.type, mediaType); + assert.strictEqual(res.body.name, path); + assert.strictEqual(res.body.type, mediaType); - const webpublicType = await getWebpublicType(alice, res.body!.id); + const webpublicType = await getWebpublicType(alice, res.body.id); assert.strictEqual(webpublicType, 'image/webp'); }); } @@ -665,8 +601,8 @@ describe('Endpoints', () => { const file = (await uploadFile(alice)).body; const newName = 'いちごパスタ.png'; - const res = await api('drive/files/update', { - fileId: file!.id, + const res = await api('/drive/files/update', { + fileId: file.id, name: newName, }, alice); @@ -678,8 +614,8 @@ describe('Endpoints', () => { test('他人のファイルは更新できない', async () => { const file = (await uploadFile(alice)).body; - const res = await api('drive/files/update', { - fileId: file!.id, + const res = await api('/drive/files/update', { + fileId: file.id, name: 'いちごパスタ.png', }, bob); @@ -688,12 +624,12 @@ describe('Endpoints', () => { test('親フォルダを更新できる', async () => { const file = (await uploadFile(alice)).body; - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const res = await api('drive/files/update', { - fileId: file!.id, + const res = await api('/drive/files/update', { + fileId: file.id, folderId: folder.id, }, alice); @@ -705,17 +641,17 @@ describe('Endpoints', () => { test('親フォルダを無しにできる', async () => { const file = (await uploadFile(alice)).body; - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - await api('drive/files/update', { - fileId: file!.id, + await api('/drive/files/update', { + fileId: file.id, folderId: folder.id, }, alice); - const res = await api('drive/files/update', { - fileId: file!.id, + const res = await api('/drive/files/update', { + fileId: file.id, folderId: null, }, alice); @@ -726,12 +662,12 @@ describe('Endpoints', () => { test('他人のフォルダには入れられない', async () => { const file = (await uploadFile(alice)).body; - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, bob)).body; - const res = await api('drive/files/update', { - fileId: file!.id, + const res = await api('/drive/files/update', { + fileId: file.id, folderId: folder.id, }, alice); @@ -741,8 +677,8 @@ describe('Endpoints', () => { test('存在しないフォルダで怒られる', async () => { const file = (await uploadFile(alice)).body; - const res = await api('drive/files/update', { - fileId: file!.id, + const res = await api('/drive/files/update', { + fileId: file.id, folderId: '000000000000000000000000', }, alice); @@ -752,8 +688,8 @@ describe('Endpoints', () => { test('不正なフォルダIDで怒られる', async () => { const file = (await uploadFile(alice)).body; - const res = await api('drive/files/update', { - fileId: file!.id, + const res = await api('/drive/files/update', { + fileId: file.id, folderId: 'foo', }, alice); @@ -761,7 +697,7 @@ describe('Endpoints', () => { }); test('ファイルが存在しなかったら怒る', async () => { - const res = await api('drive/files/update', { + const res = await api('/drive/files/update', { fileId: '000000000000000000000000', name: 'いちごパスタ.png', }, alice); @@ -769,20 +705,8 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 400); }); - test('不正なファイル名で怒られる', async () => { - const file = (await uploadFile(alice)).body; - const newName = ''; - - const res = await api('drive/files/update', { - fileId: file!.id, - name: newName, - }, alice); - - assert.strictEqual(res.status, 400); - }); - test('間違ったIDで怒られる', async () => { - const res = await api('drive/files/update', { + const res = await api('/drive/files/update', { fileId: 'kyoppie', name: 'いちごパスタ.png', }, alice); @@ -793,7 +717,7 @@ describe('Endpoints', () => { describe('drive/folders/create', () => { test('フォルダを作成できる', async () => { - const res = await api('drive/folders/create', { + const res = await api('/drive/folders/create', { name: 'test', }, alice); @@ -805,11 +729,11 @@ describe('Endpoints', () => { describe('drive/folders/update', () => { test('名前を更新できる', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, name: 'new name', }, alice); @@ -820,11 +744,11 @@ describe('Endpoints', () => { }); test('他人のフォルダを更新できない', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, bob)).body; - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, name: 'new name', }, alice); @@ -833,14 +757,14 @@ describe('Endpoints', () => { }); test('親フォルダを更新できる', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const parentFolder = (await api('drive/folders/create', { + const parentFolder = (await api('/drive/folders/create', { name: 'parent', }, alice)).body; - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, parentId: parentFolder.id, }, alice); @@ -851,18 +775,18 @@ describe('Endpoints', () => { }); test('親フォルダを無しに更新できる', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const parentFolder = (await api('drive/folders/create', { + const parentFolder = (await api('/drive/folders/create', { name: 'parent', }, alice)).body; - await api('drive/folders/update', { + await api('/drive/folders/update', { folderId: folder.id, parentId: parentFolder.id, }, alice); - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, parentId: null, }, alice); @@ -873,14 +797,14 @@ describe('Endpoints', () => { }); test('他人のフォルダを親フォルダに設定できない', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const parentFolder = (await api('drive/folders/create', { + const parentFolder = (await api('/drive/folders/create', { name: 'parent', }, bob)).body; - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, parentId: parentFolder.id, }, alice); @@ -889,18 +813,18 @@ describe('Endpoints', () => { }); test('フォルダが循環するような構造にできない', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const parentFolder = (await api('drive/folders/create', { + const parentFolder = (await api('/drive/folders/create', { name: 'parent', }, alice)).body; - await api('drive/folders/update', { + await api('/drive/folders/update', { folderId: parentFolder.id, parentId: folder.id, }, alice); - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, parentId: parentFolder.id, }, alice); @@ -909,25 +833,25 @@ describe('Endpoints', () => { }); test('フォルダが循環するような構造にできない(再帰的)', async () => { - const folderA = (await api('drive/folders/create', { + const folderA = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const folderB = (await api('drive/folders/create', { + const folderB = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const folderC = (await api('drive/folders/create', { + const folderC = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - await api('drive/folders/update', { + await api('/drive/folders/update', { folderId: folderB.id, parentId: folderA.id, }, alice); - await api('drive/folders/update', { + await api('/drive/folders/update', { folderId: folderC.id, parentId: folderB.id, }, alice); - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folderA.id, parentId: folderC.id, }, alice); @@ -936,11 +860,11 @@ describe('Endpoints', () => { }); test('フォルダが循環するような構造にできない(自身)', async () => { - const folderA = (await api('drive/folders/create', { + const folderA = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folderA.id, parentId: folderA.id, }, alice); @@ -949,11 +873,11 @@ describe('Endpoints', () => { }); test('存在しない親フォルダを設定できない', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, parentId: '000000000000000000000000', }, alice); @@ -962,11 +886,11 @@ describe('Endpoints', () => { }); test('不正な親フォルダIDで怒られる', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, parentId: 'foo', }, alice); @@ -975,7 +899,7 @@ describe('Endpoints', () => { }); test('存在しないフォルダを更新できない', async () => { - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: '000000000000000000000000', }, alice); @@ -983,7 +907,7 @@ describe('Endpoints', () => { }); test('不正なフォルダIDで怒られる', async () => { - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: 'foo', }, alice); @@ -1004,7 +928,7 @@ describe('Endpoints', () => { visibleUserIds: [alice.id], }); - const res = await api('notes/replies', { + const res = await api('/notes/replies', { noteId: alicePost.id, }, carol); @@ -1016,7 +940,7 @@ describe('Endpoints', () => { describe('notes/timeline', () => { test('フォロワー限定投稿が含まれる', async () => { - await api('following/create', { + await api('/following/create', { userId: carol.id, }, dave); @@ -1025,7 +949,7 @@ describe('Endpoints', () => { visibility: 'followers', }); - const res = await api('notes/timeline', {}, dave); + const res = await api('/notes/timeline', {}, dave); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); @@ -1046,52 +970,52 @@ describe('Endpoints', () => { test('他者に関するメモを更新できる', async () => { const memo = '10月まで低浮上とのこと。'; - const res1 = await api('users/update-memo', { + const res1 = await api('/users/update-memo', { memo, userId: bob.id, }, alice); - const res2 = await api('users/show', { + const res2 = await api('/users/show', { userId: bob.id, }, alice); assert.strictEqual(res1.status, 204); - assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo); + assert.strictEqual(res2.body?.memo, memo); }); test('自分に関するメモを更新できる', async () => { const memo = 'チケットを月末までに買う。'; - const res1 = await api('users/update-memo', { + const res1 = await api('/users/update-memo', { memo, userId: alice.id, }, alice); - const res2 = await api('users/show', { + const res2 = await api('/users/show', { userId: alice.id, }, alice); assert.strictEqual(res1.status, 204); - assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo); + assert.strictEqual(res2.body?.memo, memo); }); test('メモを削除できる', async () => { const memo = '10月まで低浮上とのこと。'; - await api('users/update-memo', { + await api('/users/update-memo', { memo, userId: bob.id, }, alice); - await api('users/update-memo', { + await api('/users/update-memo', { memo: '', userId: bob.id, }, alice); - const res = await api('users/show', { + const res = await api('/users/show', { userId: bob.id, }, alice); // memoには常に文字列かnullが入っている(5cac151) - assert.strictEqual((res.body as unknown as { memo: string | null }).memo, null); + assert.strictEqual(res.body.memo, null); }); test('メモは個人ごとに独立して保存される', async () => { @@ -1099,27 +1023,27 @@ describe('Endpoints', () => { const memoCarolToBob = '例の件について今度問いただす。'; await Promise.all([ - api('users/update-memo', { + api('/users/update-memo', { memo: memoAliceToBob, userId: bob.id, }, alice), - api('users/update-memo', { + api('/users/update-memo', { memo: memoCarolToBob, userId: bob.id, }, carol), ]); const [resAlice, resCarol] = await Promise.all([ - api('users/show', { + api('/users/show', { userId: bob.id, }, alice), - api('users/show', { + api('/users/show', { userId: bob.id, }, carol), ]); - assert.strictEqual((resAlice.body as unknown as { memo: string }).memo, memoAliceToBob); - assert.strictEqual((resCarol.body as unknown as { memo: string }).memo, memoCarolToBob); + assert.strictEqual(resAlice.body.memo, memoAliceToBob); + assert.strictEqual(resCarol.body.memo, memoCarolToBob); }); }); }); diff --git a/packages/backend/test/e2e/exports.ts b/packages/backend/test/e2e/exports.ts deleted file mode 100644 index 4bcecc9716..0000000000 --- a/packages/backend/test/e2e/exports.ts +++ /dev/null @@ -1,199 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import { api, port, post, signup, startJobQueue } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; -import type * as misskey from 'misskey-js'; - -describe('export-clips', () => { - let queue: INestApplicationContext; - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - - // XXX: Any better way to get the result? - async function pollFirstDriveFile() { - while (true) { - const files = (await api('drive/files', {}, alice)).body; - if (!files.length) { - await new Promise(r => setTimeout(r, 100)); - continue; - } - if (files.length > 1) { - throw new Error('Too many files?'); - } - const file = (await api('drive/files/show', { fileId: files[0].id }, alice)).body; - const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`)); - return await res.json(); - } - } - - beforeAll(async () => { - queue = await startJobQueue(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - }, 1000 * 60 * 2); - - afterAll(async () => { - await queue.close(); - }); - - beforeEach(async () => { - // Clean all clips and files of alice - const clips = (await api('clips/list', {}, alice)).body; - for (const clip of clips) { - const res = await api('clips/delete', { clipId: clip.id }, alice); - if (res.status !== 204) { - throw new Error('Failed to delete clip'); - } - } - const files = (await api('drive/files', {}, alice)).body; - for (const file of files) { - const res = await api('drive/files/delete', { fileId: file.id }, alice); - if (res.status !== 204) { - throw new Error('Failed to delete file'); - } - } - }); - - test('basic export', async () => { - const res1 = await api('clips/create', { - name: 'foo', - description: 'bar', - }, alice); - assert.strictEqual(res1.status, 200); - - const res2 = await api('i/export-clips', {}, alice); - assert.strictEqual(res2.status, 204); - - const exported = await pollFirstDriveFile(); - assert.strictEqual(exported[0].name, 'foo'); - assert.strictEqual(exported[0].description, 'bar'); - assert.strictEqual(exported[0].clipNotes.length, 0); - }); - - test('export with notes', async () => { - const res = await api('clips/create', { - name: 'foo', - description: 'bar', - }, alice); - assert.strictEqual(res.status, 200); - const clip = res.body; - - const note1 = await post(alice, { - text: 'baz1', - }); - - const note2 = await post(alice, { - text: 'baz2', - poll: { - choices: ['sakura', 'izumi', 'ako'], - }, - }); - - for (const note of [note1, note2]) { - const res2 = await api('clips/add-note', { - clipId: clip.id, - noteId: note.id, - }, alice); - assert.strictEqual(res2.status, 204); - } - - const res3 = await api('i/export-clips', {}, alice); - assert.strictEqual(res3.status, 204); - - const exported = await pollFirstDriveFile(); - assert.strictEqual(exported[0].name, 'foo'); - assert.strictEqual(exported[0].description, 'bar'); - assert.strictEqual(exported[0].clipNotes.length, 2); - assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1'); - assert.strictEqual(exported[0].clipNotes[1].note.text, 'baz2'); - assert.deepStrictEqual(exported[0].clipNotes[1].note.poll.choices[0], 'sakura'); - }); - - test('multiple clips', async () => { - const res1 = await api('clips/create', { - name: 'kawaii', - description: 'kawaii', - }, alice); - assert.strictEqual(res1.status, 200); - const clip1 = res1.body; - - const res2 = await api('clips/create', { - name: 'yuri', - description: 'yuri', - }, alice); - assert.strictEqual(res2.status, 200); - const clip2 = res2.body; - - const note1 = await post(alice, { - text: 'baz1', - }); - - const note2 = await post(alice, { - text: 'baz2', - }); - - { - const res = await api('clips/add-note', { - clipId: clip1.id, - noteId: note1.id, - }, alice); - assert.strictEqual(res.status, 204); - } - - { - const res = await api('clips/add-note', { - clipId: clip2.id, - noteId: note2.id, - }, alice); - assert.strictEqual(res.status, 204); - } - - { - const res = await api('i/export-clips', {}, alice); - assert.strictEqual(res.status, 204); - } - - const exported = await pollFirstDriveFile(); - assert.strictEqual(exported[0].name, 'kawaii'); - assert.strictEqual(exported[0].clipNotes.length, 1); - assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1'); - assert.strictEqual(exported[1].name, 'yuri'); - assert.strictEqual(exported[1].clipNotes.length, 1); - assert.strictEqual(exported[1].clipNotes[0].note.text, 'baz2'); - }); - - test('Clipping other user\'s note', async () => { - const res = await api('clips/create', { - name: 'kawaii', - description: 'kawaii', - }, alice); - assert.strictEqual(res.status, 200); - const clip = res.body; - - const note = await post(bob, { - text: 'baz', - visibility: 'followers', - }); - - const res2 = await api('clips/add-note', { - clipId: clip.id, - noteId: note.id, - }, alice); - assert.strictEqual(res2.status, 204); - - const res3 = await api('i/export-clips', {}, alice); - assert.strictEqual(res3.status, 204); - - const exported = await pollFirstDriveFile(); - assert.strictEqual(exported[0].name, 'kawaii'); - assert.strictEqual(exported[0].clipNotes.length, 1); - assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz'); - assert.strictEqual(exported[0].clipNotes[0].note.user.username, 'bob'); - }); -}); diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index 740295bda8..115945dd3d 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -1,37 +1,33 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { channel, clip, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js'; +import { startServer, channel, clip, cookie, galleryPost, signup, page, play, post, simpleGet, uploadFile } from '../utils.js'; import type { SimpleGetResponse } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; -// Request Accept in lowercase +// Request Accept const ONLY_AP = 'application/activity+json'; const PREFER_AP = 'application/activity+json, */*'; const PREFER_HTML = 'text/html, */*'; const UNSPECIFIED = '*/*'; -// Response Content-Type in lowercase +// Response Content-Type const AP = 'application/activity+json; charset=utf-8'; const HTML = 'text/html; charset=utf-8'; const JSON_UTF8 = 'application/json; charset=utf-8'; describe('Webリソース', () => { - let alice: misskey.entities.SignupResponse; - let aliceUploadedFile: misskey.entities.DriveFile | null; - let alicesPost: misskey.entities.Note; - let alicePage: misskey.entities.Page; - let alicePlay: misskey.entities.Flash; - let aliceClip: misskey.entities.Clip; - let aliceGalleryPost: misskey.entities.GalleryPost; - let aliceChannel: misskey.entities.Channel; + let app: INestApplicationContext; - let bob: misskey.entities.SignupResponse; + let alice: misskey.entities.MeSignup; + let aliceUploadedFile: any; + let alicesPost: any; + let alicePage: any; + let alicePlay: any; + let aliceClip: any; + let aliceGalleryPost: any; + let aliceChannel: any; type Request = { path: string, @@ -44,8 +40,7 @@ describe('Webリソース', () => { const { path, accept, cookie, type } = param; const res = await simpleGet(path, accept, cookie); assert.strictEqual(res.status, 200); - // Header values are case-insensitive - assert.strictEqual(res.type?.toLowerCase(), (type ?? HTML).toLowerCase()); + assert.strictEqual(res.type, type ?? HTML); return res; }; @@ -77,8 +72,9 @@ describe('Webリソース', () => { }; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); - aliceUploadedFile = (await uploadFile(alice)).body; + aliceUploadedFile = await uploadFile(alice); alicesPost = await post(alice, { text: 'test', }); @@ -86,17 +82,20 @@ describe('Webリソース', () => { alicePlay = await play(alice, {}); aliceClip = await clip(alice, {}); aliceGalleryPost = await galleryPost(alice, { - fileIds: [aliceUploadedFile!.id], + fileIds: [aliceUploadedFile.body.id], }); aliceChannel = await channel(alice, {}); - - bob = await signup({ username: 'bob' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + describe.each([ { path: '/', type: HTML }, { path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。" - { path: '/api-doc', type: HTML }, + // fastify-static gives charset=UTF-8 instead of utf-8 and that's okay + { path: '/api-doc', type: 'text/html; charset=UTF-8' }, { path: '/api.json', type: JSON_UTF8 }, { path: '/api-console', type: HTML }, { path: '/_info_card_', type: HTML }, @@ -144,32 +143,10 @@ describe('Webリソース', () => { type, })); - test('がGETできる。(ノートが存在しない場合でも。)', async () => await ok({ - path: path(bob.username), - type, - })); - test('は存在しないユーザーはGETできない。', async () => await notOk({ path: path('nonexisting'), status: 404, })); - - describe(' has entry such ', () => { - beforeEach(() => { - post(alice, { text: '**a**' }); - }); - - test('MFMを含まない。', async () => { - const content = await simpleGet(path(alice.username), '*/*', undefined, res => res.text()); - const _body: unknown = content.body; - // JSONフィードのときは改めて文字列化する - const body: string = typeof (_body) === 'object' ? JSON.stringify(_body) : _body as string; - - if (body.includes('**a**')) { - throw new Error('MFM shouldn\'t be included'); - } - }); - }); }); describe.each([{ path: '/api/foo' }])('$path', ({ path }) => { @@ -180,6 +157,18 @@ describe('Webリソース', () => { })); }); + describe.each([{ path: '/queue' }])('$path', ({ path }) => { + test('はadminでなければGETできない。', async () => await notOk({ + path, + status: 500, // FIXME? 403ではない。 + })); + + test('はadminならGETできる。', async () => await ok({ + path, + cookie: cookie(alice), + })); + }); + describe.each([{ path: '/streaming' }])('$path', ({ path }) => { test('はGETできない。', async () => await notOk({ path, @@ -212,7 +201,6 @@ describe('Webリソース', () => { path: path('xxxxxxxxxx'), type: HTML, })); - test.todo('HTMLとしてGETできる。(リモートユーザーでもリダイレクトせず)'); }); describe.each([ @@ -232,7 +220,6 @@ describe('Webリソース', () => { path: path('xxxxxxxxxx'), accept, })); - test.todo('はオリジナルにリダイレクトされる。(リモートユーザー)'); }); }); diff --git a/packages/backend/test/e2e/fetch-validate-ap-deny.ts b/packages/backend/test/e2e/fetch-validate-ap-deny.ts deleted file mode 100644 index 434a9fe209..0000000000 --- a/packages/backend/test/e2e/fetch-validate-ap-deny.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -process.env.NODE_ENV = 'test'; - -import { validateContentTypeSetAsActivityPub, validateContentTypeSetAsJsonLD } from '@/core/activitypub/misc/validator.js'; -import { signup, uploadFile, relativeFetch } from '../utils.js'; -import type * as misskey from 'misskey-js'; - -describe('validateContentTypeSetAsActivityPub/JsonLD (deny case)', () => { - let alice: misskey.entities.SignupResponse; - let aliceUploadedFile: any; - - beforeAll(async () => { - alice = await signup({ username: 'alice' }); - aliceUploadedFile = await uploadFile(alice); - }, 1000 * 60 * 2); - - test('ActivityStreams: ファイルはエラーになる', async () => { - const res = await relativeFetch(aliceUploadedFile.webpublicUrl); - - function doValidate() { - validateContentTypeSetAsActivityPub(res); - } - - expect(doValidate).toThrow('Content type is not'); - }); - - test('JSON-LD: ファイルはエラーになる', async () => { - const res = await relativeFetch(aliceUploadedFile.webpublicUrl); - - function doValidate() { - validateContentTypeSetAsJsonLD(res); - } - - expect(doValidate).toThrow('Content type is not'); - }); -}); diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts index 5d0c70a3c2..9082c77f07 100644 --- a/packages/backend/test/e2e/ff-visibility.ts +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -1,33 +1,35 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { api, signup, simpleGet } from '../utils.js'; +import { signup, api, startServer, simpleGet } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; describe('FF visibility', () => { - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; + let app: INestApplicationContext; + + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); }, 1000 * 60 * 2); - test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { - await api('i/update', { - followingVisibility: 'public', - followersVisibility: 'public', + afterAll(async () => { + await app.close(); + }); + + test('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { + await api('/i/update', { + ffVisibility: 'public', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); @@ -37,94 +39,15 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('followingVisibility が public であれば followersVisibility の設定に関わらずユーザーのフォローを誰でも見れる', async () => { - { - await api('i/update', { - followingVisibility: 'public', - followersVisibility: 'public', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, bob); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'public', - followersVisibility: 'followers', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, bob); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'public', - followersVisibility: 'private', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, bob); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - } - }); - - test('followersVisibility が public であれば followingVisibility の設定に関わらずユーザーのフォロワーを誰でも見れる', async () => { - { - await api('i/update', { - followingVisibility: 'public', - followersVisibility: 'public', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, bob); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'public', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, bob); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'private', - followersVisibility: 'public', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, bob); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - } - }); - - test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'followers', + test('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { + await api('/i/update', { + ffVisibility: 'followers', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, alice); @@ -134,94 +57,15 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => { - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'public', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, alice); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'followers', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, alice); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'private', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, alice); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - } - }); - - test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => { - { - await api('i/update', { - followingVisibility: 'public', - followersVisibility: 'followers', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, alice); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'followers', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, alice); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'private', - followersVisibility: 'followers', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, alice); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - } - }); - - test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'followers', + test('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { + await api('/i/update', { + ffVisibility: 'followers', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); @@ -229,92 +73,19 @@ describe('FF visibility', () => { assert.strictEqual(followersRes.status, 400); }); - test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず非フォロワーが見れない', async () => { - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'public', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, bob); - assert.strictEqual(followingRes.status, 400); - } - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'followers', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, bob); - assert.strictEqual(followingRes.status, 400); - } - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'private', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, bob); - assert.strictEqual(followingRes.status, 400); - } - }); - - test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず非フォロワーが見れない', async () => { - { - await api('i/update', { - followingVisibility: 'public', - followersVisibility: 'followers', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, bob); - assert.strictEqual(followersRes.status, 400); - } - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'followers', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, bob); - assert.strictEqual(followersRes.status, 400); - } - { - await api('i/update', { - followingVisibility: 'private', - followersVisibility: 'followers', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, bob); - assert.strictEqual(followersRes.status, 400); - } - }); - - test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'followers', + test('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { + await api('/i/update', { + ffVisibility: 'followers', }, alice); - await api('following/create', { + await api('/following/create', { userId: alice.id, }, bob); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); @@ -324,112 +95,15 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらずフォロワーが見れる', async () => { - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'public', - }, alice); - await api('following/create', { - userId: alice.id, - }, bob); - - const followingRes = await api('users/following', { - userId: alice.id, - }, bob); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'followers', - }, alice); - await api('following/create', { - userId: alice.id, - }, bob); - - const followingRes = await api('users/following', { - userId: alice.id, - }, bob); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'private', - }, alice); - await api('following/create', { - userId: alice.id, - }, bob); - - const followingRes = await api('users/following', { - userId: alice.id, - }, bob); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - } - }); - - test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらずフォロワーが見れる', async () => { - { - await api('i/update', { - followingVisibility: 'public', - followersVisibility: 'followers', - }, alice); - await api('following/create', { - userId: alice.id, - }, bob); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, bob); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'followers', - }, alice); - await api('following/create', { - userId: alice.id, - }, bob); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, bob); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'private', - followersVisibility: 'followers', - }, alice); - await api('following/create', { - userId: alice.id, - }, bob); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, bob); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - } - }); - - test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを自分で見れる', async () => { - await api('i/update', { - followingVisibility: 'private', - followersVisibility: 'private', + test('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => { + await api('/i/update', { + ffVisibility: 'private', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, alice); @@ -439,94 +113,15 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => { - { - await api('i/update', { - followingVisibility: 'private', - followersVisibility: 'public', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, alice); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'private', - followersVisibility: 'followers', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, alice); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'private', - followersVisibility: 'private', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, alice); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - } - }); - - test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => { - { - await api('i/update', { - followingVisibility: 'public', - followersVisibility: 'private', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, alice); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'private', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, alice); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - } - { - await api('i/update', { - followingVisibility: 'private', - followersVisibility: 'private', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, alice); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - } - }); - - test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを他人が見れない', async () => { - await api('i/update', { - followingVisibility: 'private', - followersVisibility: 'private', + test('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => { + await api('/i/update', { + ffVisibility: 'private', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); @@ -534,129 +129,36 @@ describe('FF visibility', () => { assert.strictEqual(followersRes.status, 400); }); - test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず他人が見れない', async () => { - { - await api('i/update', { - followingVisibility: 'private', - followersVisibility: 'public', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, bob); - assert.strictEqual(followingRes.status, 400); - } - { - await api('i/update', { - followingVisibility: 'private', - followersVisibility: 'followers', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, bob); - assert.strictEqual(followingRes.status, 400); - } - { - await api('i/update', { - followingVisibility: 'private', - followersVisibility: 'private', - }, alice); - - const followingRes = await api('users/following', { - userId: alice.id, - }, bob); - assert.strictEqual(followingRes.status, 400); - } - }); - - test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず他人が見れない', async () => { - { - await api('i/update', { - followingVisibility: 'public', - followersVisibility: 'private', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, bob); - assert.strictEqual(followersRes.status, 400); - } - { - await api('i/update', { - followingVisibility: 'followers', - followersVisibility: 'private', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, bob); - assert.strictEqual(followersRes.status, 400); - } - { - await api('i/update', { - followingVisibility: 'private', - followersVisibility: 'private', - }, alice); - - const followersRes = await api('users/followers', { - userId: alice.id, - }, bob); - assert.strictEqual(followersRes.status, 400); - } - }); - describe('AP', () => { - test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => { + test('ffVisibility が public 以外ならばAPからは取得できない', async () => { { - await api('i/update', { - followingVisibility: 'public', + await api('/i/update', { + ffVisibility: 'public', }, alice); const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); - assert.strictEqual(followingRes.status, 200); - } - { - await api('i/update', { - followingVisibility: 'followers', - }, alice); - - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); - assert.strictEqual(followingRes.status, 403); - } - { - await api('i/update', { - followingVisibility: 'private', - }, alice); - - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); - assert.strictEqual(followingRes.status, 403); - } - }); - - test('followersVisibility が public 以外ならばAPからはフォロワーを取得できない', async () => { - { - await api('i/update', { - followersVisibility: 'public', - }, alice); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); + assert.strictEqual(followingRes.status, 200); assert.strictEqual(followersRes.status, 200); } { - await api('i/update', { - followersVisibility: 'followers', + await api('/i/update', { + ffVisibility: 'followers', }, alice); + const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); + assert.strictEqual(followingRes.status, 403); assert.strictEqual(followersRes.status, 403); } { - await api('i/update', { - followersVisibility: 'private', + await api('/i/update', { + ffVisibility: 'private', }, alice); + const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); + assert.strictEqual(followingRes.status, 403); assert.strictEqual(followersRes.status, 403); } }); diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index fd798bdb25..2fefcd0f0e 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -1,38 +1,32 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { INestApplicationContext } from '@nestjs/common'; - process.env.NODE_ENV = 'test'; -import { setTimeout } from 'node:timers/promises'; import * as assert from 'assert'; import { loadConfig } from '@/config.js'; -import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js'; -import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { User, UsersRepository } from '@/models/index.js'; import { jobQueue } from '@/boot/common.js'; -import { api, castAsError, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { uploadFile, signup, startServer, initTestDb, api, sleep, successfulApiCall } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; describe('Account Move', () => { + let app: INestApplicationContext; let jq: INestApplicationContext; let url: URL; - let root: misskey.entities.SignupResponse; - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - let carol: misskey.entities.SignupResponse; - let dave: misskey.entities.SignupResponse; - let eve: misskey.entities.SignupResponse; - let frank: misskey.entities.SignupResponse; + let root: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; + let dave: misskey.entities.MeSignup; + let eve: misskey.entities.MeSignup; + let frank: misskey.entities.MeSignup; let Users: UsersRepository; beforeAll(async () => { + app = await startServer(); jq = await jobQueue(); - const config = loadConfig(); url = new URL(config.url); const connection = await initTestDb(false); @@ -43,11 +37,11 @@ describe('Account Move', () => { dave = await signup({ username: 'dave' }); eve = await signup({ username: 'eve' }); frank = await signup({ username: 'frank' }); - Users = connection.getRepository(MiUser).extend(miRepository as MiRepository); + Users = connection.getRepository(User); }, 1000 * 60 * 2); afterAll(async () => { - await jq.close(); + await Promise.all([app.close(), jq.close()]); }); describe('Create Alias', () => { @@ -56,7 +50,7 @@ describe('Account Move', () => { }, 1000 * 10); test('Able to create an alias', async () => { - const res = await api('i/update', { + const res = await api('/i/update', { alsoKnownAs: [`@alice@${url.hostname}`], }, bob); @@ -68,7 +62,7 @@ describe('Account Move', () => { }); test('Able to create a local alias without hostname', async () => { - await api('i/update', { + await api('/i/update', { alsoKnownAs: ['@alice'], }, bob); @@ -78,7 +72,7 @@ describe('Account Move', () => { }); test('Able to create a local alias without @', async () => { - await api('i/update', { + await api('/i/update', { alsoKnownAs: ['alice'], }, bob); @@ -88,55 +82,55 @@ describe('Account Move', () => { }); test('Able to set remote user (but may fail)', async () => { - const res = await api('i/update', { + const res = await api('/i/update', { alsoKnownAs: ['@syuilo@example.com'], }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER'); - assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); + assert.strictEqual(res.body.error.code, 'NO_SUCH_USER'); + assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); }); test('Unable to add duplicated aliases to alsoKnownAs', async () => { - const res = await api('i/update', { + const res = await api('/i/update', { alsoKnownAs: [`@alice@${url.hostname}`, `@alice@${url.hostname}`], }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'INVALID_PARAM'); - assert.strictEqual(castAsError(res.body).error.id, '3d81ceae-475f-4600-b2a8-2bc116157532'); + assert.strictEqual(res.body.error.code, 'INVALID_PARAM'); + assert.strictEqual(res.body.error.id, '3d81ceae-475f-4600-b2a8-2bc116157532'); }); test('Unable to add itself', async () => { - const res = await api('i/update', { + const res = await api('/i/update', { alsoKnownAs: [`@bob@${url.hostname}`], }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'FORBIDDEN_TO_SET_YOURSELF'); - assert.strictEqual(castAsError(res.body).error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4'); + assert.strictEqual(res.body.error.code, 'FORBIDDEN_TO_SET_YOURSELF'); + assert.strictEqual(res.body.error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4'); }); test('Unable to add a nonexisting local account to alsoKnownAs', async () => { - const res1 = await api('i/update', { + const res1 = await api('/i/update', { alsoKnownAs: [`@nonexist@${url.hostname}`], }, bob); assert.strictEqual(res1.status, 400); - assert.strictEqual(castAsError(res1.body).error.code, 'NO_SUCH_USER'); - assert.strictEqual(castAsError(res1.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); + assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER'); + assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); - const res2 = await api('i/update', { + const res2 = await api('/i/update', { alsoKnownAs: ['@alice', 'nonexist'], }, bob); assert.strictEqual(res2.status, 400); - assert.strictEqual(castAsError(res2.body).error.code, 'NO_SUCH_USER'); - assert.strictEqual(castAsError(res2.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); + assert.strictEqual(res2.body.error.code, 'NO_SUCH_USER'); + assert.strictEqual(res2.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); }); test('Able to add two existing local account to alsoKnownAs', async () => { - await api('i/update', { + await api('/i/update', { alsoKnownAs: [`@alice@${url.hostname}`, `@carol@${url.hostname}`], }, bob); @@ -147,10 +141,10 @@ describe('Account Move', () => { }); test('Able to properly overwrite alsoKnownAs', async () => { - await api('i/update', { + await api('/i/update', { alsoKnownAs: [`@alice@${url.hostname}`], }, bob); - await api('i/update', { + await api('/i/update', { alsoKnownAs: [`@carol@${url.hostname}`, `@dave@${url.hostname}`], }, bob); @@ -165,171 +159,163 @@ describe('Account Move', () => { let antennaId = ''; beforeAll(async () => { - await api('i/update', { + await api('/i/update', { alsoKnownAs: [`@alice@${url.hostname}`], }, root); - const listRoot = await api('users/lists/create', { + const listRoot = await api('/users/lists/create', { name: secureRndstr(8), }, root); - await api('users/lists/push', { + await api('/users/lists/push', { listId: listRoot.body.id, userId: alice.id, }, root); - await api('following/create', { + await api('/following/create', { userId: root.id, }, alice); - await api('following/create', { + await api('/following/create', { userId: eve.id, }, alice); - const antenna = await api('antennas/create', { + const antenna = await api('/antennas/create', { name: secureRndstr(8), src: 'home', - keywords: [[secureRndstr(8)]], + keywords: [secureRndstr(8)], excludeKeywords: [], users: [], caseSensitive: false, - localOnly: false, withReplies: false, withFile: false, + notify: false, }, alice); antennaId = antenna.body.id; - await api('i/update', { + await api('/i/update', { alsoKnownAs: [`@alice@${url.hostname}`], }, bob); - await api('following/create', { + await api('/following/create', { userId: alice.id, }, carol); - await api('mute/create', { + await api('/mute/create', { userId: alice.id, }, dave); - await api('blocking/create', { + await api('/blocking/create', { userId: alice.id, }, dave); - await api('following/create', { + await api('/following/create', { userId: eve.id, }, dave); - await api('following/create', { + await api('/following/create', { userId: dave.id, }, eve); - const listEve = await api('users/lists/create', { + const listEve = await api('/users/lists/create', { name: secureRndstr(8), }, eve); - await api('users/lists/push', { + await api('/users/lists/push', { listId: listEve.body.id, userId: bob.id, }, eve); - await api('i/update', { + await api('/i/update', { isLocked: true, }, frank); - await api('following/create', { + await api('/following/create', { userId: frank.id, }, alice); - await api('following/requests/accept', { + await api('/following/requests/accept', { userId: alice.id, }, frank); }, 1000 * 10); test('Prohibit the root account from moving', async () => { - const res = await api('i/move', { + const res = await api('/i/move', { moveToAccount: `@bob@${url.hostname}`, }, root); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'NOT_ROOT_FORBIDDEN'); - assert.strictEqual(castAsError(res.body).error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24'); + assert.strictEqual(res.body.error.code, 'NOT_ROOT_FORBIDDEN'); + assert.strictEqual(res.body.error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24'); }); test('Unable to move to a nonexisting local account', async () => { - const res = await api('i/move', { + const res = await api('/i/move', { moveToAccount: `@nonexist@${url.hostname}`, }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER'); - assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); + assert.strictEqual(res.body.error.code, 'NO_SUCH_USER'); + assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); }); test('Unable to move if alsoKnownAs is invalid', async () => { - const res = await api('i/move', { + const res = await api('/i/move', { moveToAccount: `@carol@${url.hostname}`, }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS'); - assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); + assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS'); + assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); }); test('Relationships have been properly migrated', async () => { - const move = await api('i/move', { + const move = await api('/i/move', { moveToAccount: `@bob@${url.hostname}`, }, alice); assert.strictEqual(move.status, 200); - await setTimeout(1000 * 3); // wait for jobs to finish + await sleep(1000 * 3); // wait for jobs to finish // Unfollow delayed? - const aliceFollowings = await api('users/following', { + const aliceFollowings = await api('/users/following', { userId: alice.id, }, alice); assert.strictEqual(aliceFollowings.status, 200); - assert.ok(aliceFollowings); assert.strictEqual(aliceFollowings.body.length, 3); - const carolFollowings = await api('users/following', { + const carolFollowings = await api('/users/following', { userId: carol.id, }, carol); assert.strictEqual(carolFollowings.status, 200); - assert.ok(carolFollowings); assert.strictEqual(carolFollowings.body.length, 2); assert.strictEqual(carolFollowings.body[0].followeeId, bob.id); assert.strictEqual(carolFollowings.body[1].followeeId, alice.id); - const blockings = await api('blocking/list', {}, dave); + const blockings = await api('/blocking/list', {}, dave); assert.strictEqual(blockings.status, 200); - assert.ok(blockings); assert.strictEqual(blockings.body.length, 2); assert.strictEqual(blockings.body[0].blockeeId, bob.id); assert.strictEqual(blockings.body[1].blockeeId, alice.id); - const mutings = await api('mute/list', {}, dave); + const mutings = await api('/mute/list', {}, dave); assert.strictEqual(mutings.status, 200); - assert.ok(mutings); assert.strictEqual(mutings.body.length, 2); assert.strictEqual(mutings.body[0].muteeId, bob.id); assert.strictEqual(mutings.body[1].muteeId, alice.id); - const rootLists = await api('users/lists/list', {}, root); + const rootLists = await api('/users/lists/list', {}, root); assert.strictEqual(rootLists.status, 200); - assert.ok(rootLists); - assert.ok(rootLists.body[0].userIds); assert.strictEqual(rootLists.body[0].userIds.length, 2); assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id)); assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id)); - const eveLists = await api('users/lists/list', {}, eve); + const eveLists = await api('/users/lists/list', {}, eve); assert.strictEqual(eveLists.status, 200); - assert.ok(eveLists); - assert.ok(eveLists.body[0].userIds); assert.strictEqual(eveLists.body[0].userIds.length, 1); assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id)); }); test('A locked account automatically accept the follow request if it had already accepted the old account.', async () => { await successfulApiCall({ - endpoint: 'following/create', + endpoint: '/following/create', parameters: { userId: frank.id, }, user: bob, }); - const followers = await api('users/followers', { + const followers = await api('/users/followers', { userId: frank.id, }, frank); @@ -339,9 +325,9 @@ describe('Account Move', () => { }); test('Unfollowed after 10 sec (24 hours in production).', async () => { - await setTimeout(1000 * 8); + await sleep(1000 * 8); - const following = await api('users/following', { + const following = await api('/users/following', { userId: alice.id, }, alice); @@ -350,17 +336,17 @@ describe('Account Move', () => { }); test('Unable to move if the destination account has already moved.', async () => { - const res = await api('i/move', { + const res = await api('/i/move', { moveToAccount: `@alice@${url.hostname}`, }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS'); - assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); + assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS'); + assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); }); test('Follow and follower counts are properly adjusted', async () => { - await api('following/create', { + await api('/following/create', { userId: alice.id, }, eve); const newAlice = await Users.findOneByOrFail({ id: alice.id }); @@ -373,7 +359,7 @@ describe('Account Move', () => { assert.strictEqual(newEve.followingCount, 1); assert.strictEqual(newEve.followersCount, 1); - await api('following/delete', { + await api('/following/delete', { userId: alice.id, }, eve); newEve = await Users.findOneByOrFail({ id: eve.id }); @@ -382,94 +368,90 @@ describe('Account Move', () => { }); test.each([ - 'antennas/create', - 'channels/create', - 'channels/favorite', - 'channels/follow', - 'channels/unfavorite', - 'channels/unfollow', - 'clips/add-note', - 'clips/create', - 'clips/favorite', - 'clips/remove-note', - 'clips/unfavorite', - 'clips/update', - 'drive/files/upload-from-url', - 'flash/create', - 'flash/like', - 'flash/unlike', - 'flash/update', - 'following/create', - 'gallery/posts/create', - 'gallery/posts/like', - 'gallery/posts/unlike', - 'gallery/posts/update', - 'i/claim-achievement', - 'i/move', - 'i/import-blocking', - 'i/import-following', - 'i/import-muting', - 'i/import-user-lists', - 'i/pin', - 'mute/create', - 'notes/create', - 'notes/favorites/create', - 'notes/polls/vote', - 'notes/reactions/create', - 'pages/create', - 'pages/like', - 'pages/unlike', - 'pages/update', - 'renote-mute/create', - 'users/lists/create', - 'users/lists/pull', - 'users/lists/push', - ] as const)('Prohibit access after moving: %s', async (endpoint) => { + '/antennas/create', + '/channels/create', + '/channels/favorite', + '/channels/follow', + '/channels/unfavorite', + '/channels/unfollow', + '/clips/add-note', + '/clips/create', + '/clips/favorite', + '/clips/remove-note', + '/clips/unfavorite', + '/clips/update', + '/drive/files/upload-from-url', + '/flash/create', + '/flash/like', + '/flash/unlike', + '/flash/update', + '/following/create', + '/gallery/posts/create', + '/gallery/posts/like', + '/gallery/posts/unlike', + '/gallery/posts/update', + '/i/claim-achievement', + '/i/move', + '/i/import-blocking', + '/i/import-following', + '/i/import-muting', + '/i/import-user-lists', + '/i/pin', + '/mute/create', + '/notes/create', + '/notes/favorites/create', + '/notes/polls/vote', + '/notes/reactions/create', + '/pages/create', + '/pages/like', + '/pages/unlike', + '/pages/update', + '/renote-mute/create', + '/users/lists/create', + '/users/lists/pull', + '/users/lists/push', + ])('Prohibit access after moving: %s', async (endpoint) => { const res = await api(endpoint, {}, alice); assert.strictEqual(res.status, 403); - assert.ok(res.body); - assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED'); - assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); }); test('Prohibit access after moving: /antennas/update', async () => { - const res = await api('antennas/update', { + const res = await api('/antennas/update', { antennaId, name: secureRndstr(8), src: 'users', - keywords: [[secureRndstr(8)]], + keywords: [secureRndstr(8)], excludeKeywords: [], users: [eve.id], caseSensitive: false, - localOnly: false, withReplies: false, withFile: false, + notify: false, }, alice); assert.strictEqual(res.status, 403); - assert.ok(res.body); - assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED'); - assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); }); test('Prohibit access after moving: /drive/files/create', async () => { - // FIXME: 一旦逃げておく const res = await uploadFile(alice); assert.strictEqual(res.status, 403); - assert.ok(res.body); - assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED'); - assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); }); test('Prohibit updating alsoKnownAs after moving', async () => { - const res = await api('i/update', { + const res = await api('/i/update', { alsoKnownAs: [`@eve@${url.hostname}`], }, alice); assert.strictEqual(res.status, 403); - assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED'); - assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); }); }); }); diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts index b464c24287..79e2c90f64 100644 --- a/packages/backend/test/e2e/mute.ts +++ b/packages/backend/test/e2e/mute.ts @@ -1,59 +1,74 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { api, post, react, signup, waitFire } from '../utils.js'; +import { signup, api, post, react, startServer, waitFire } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; describe('Mute', () => { + let app: INestApplicationContext; + // alice mutes carol - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - let carol: misskey.entities.SignupResponse; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); - - // Mute: alice ==> carol - await api('mute/create', { - userId: carol.id, - }, alice); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + test('ミュート作成', async () => { - const res = await api('mute/create', { - userId: bob.id, + const res = await api('/mute/create', { + userId: carol.id, }, alice); assert.strictEqual(res.status, 204); - - // 単体でも走らせられるように副作用消す - await api('mute/delete', { - userId: bob.id, - }, alice); }); test('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => { const bobNote = await post(bob, { text: '@alice hi' }); const carolNote = await post(carol, { text: '@alice hi' }); - const res = await api('notes/mentions', {}, alice); + const res = await api('/notes/mentions', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => { + // 状態リセット + await api('/i/read-all-unread-notes', {}, alice); + + await post(carol, { text: '@alice hi' }); + + const res = await api('/i', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.hasUnreadMentions, false); + }); + + test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => { + // 状態リセット + await api('/i/read-all-unread-notes', {}, alice); + + const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention'); + + assert.strictEqual(fired, false); }); test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => { // 状態リセット - await api('notifications/mark-all-as-read', {}, alice); + await api('/i/read-all-unread-notes', {}, alice); + await api('/notifications/mark-all-as-read', {}, alice); const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification'); @@ -66,13 +81,13 @@ describe('Mute', () => { const bobNote = await post(bob, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' }); - const res = await api('notes/local-timeline', {}, alice); + const res = await api('/notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => { @@ -82,13 +97,13 @@ describe('Mute', () => { renoteId: carolNote.id, }); - const res = await api('notes/local-timeline', {}, alice); + const res = await api('/notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); }); @@ -98,201 +113,12 @@ describe('Mute', () => { await react(bob, aliceNote, 'like'); await react(carol, aliceNote, 'like'); - const res = await api('i/notifications', {}, alice); + const res = await api('/i/notifications', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからのリプライが含まれない', async () => { - const aliceNote = await post(alice, { text: 'hi' }); - await post(bob, { text: '@alice hi', replyId: aliceNote.id }); - await post(carol, { text: '@alice hi', replyId: aliceNote.id }); - - const res = await api('i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからのリプライが含まれない', async () => { - await post(alice, { text: 'hi' }); - await post(bob, { text: '@alice hi' }); - await post(carol, { text: '@alice hi' }); - - const res = await api('i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => { - const aliceNote = await post(alice, { text: 'hi' }); - await post(bob, { text: 'hi', renoteId: aliceNote.id }); - await post(carol, { text: 'hi', renoteId: aliceNote.id }); - - const res = await api('i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからのリノートが含まれない', async () => { - const aliceNote = await post(alice, { text: 'hi' }); - await post(bob, { renoteId: aliceNote.id }); - await post(carol, { renoteId: aliceNote.id }); - - const res = await api('i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => { - await api('following/create', { userId: alice.id }, bob); - await api('following/create', { userId: alice.id }, carol); - - const res = await api('i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - - await api('following/delete', { userId: alice.id }, bob); - await api('following/delete', { userId: alice.id }, carol); - }); - - test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => { - await api('i/update', { isLocked: true }, alice); - await api('following/create', { userId: alice.id }, bob); - await api('following/create', { userId: alice.id }, carol); - - const res = await api('i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - - await api('following/delete', { userId: alice.id }, bob); - await api('following/delete', { userId: alice.id }, carol); - }); - }); - - describe('Notification (Grouped)', () => { - test('通知にミュートしているユーザーの通知が含まれない(リアクション)', async () => { - const aliceNote = await post(alice, { text: 'hi' }); - await react(bob, aliceNote, 'like'); - await react(carol, aliceNote, 'like'); - - const res = await api('i/notifications-grouped', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - test('通知にミュートしているユーザーからのリプライが含まれない', async () => { - const aliceNote = await post(alice, { text: 'hi' }); - await post(bob, { text: '@alice hi', replyId: aliceNote.id }); - await post(carol, { text: '@alice hi', replyId: aliceNote.id }); - - const res = await api('i/notifications-grouped', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからのリプライが含まれない', async () => { - await post(alice, { text: 'hi' }); - await post(bob, { text: '@alice hi' }); - await post(carol, { text: '@alice hi' }); - - const res = await api('i/notifications-grouped', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => { - const aliceNote = await post(alice, { text: 'hi' }); - await post(bob, { text: 'hi', renoteId: aliceNote.id }); - await post(carol, { text: 'hi', renoteId: aliceNote.id }); - - const res = await api('i/notifications-grouped', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからのリノートが含まれない', async () => { - const aliceNote = await post(alice, { text: 'hi' }); - await post(bob, { renoteId: aliceNote.id }); - await post(carol, { renoteId: aliceNote.id }); - - const res = await api('i/notifications-grouped', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => { - await api('following/create', { userId: alice.id }, bob); - await api('following/create', { userId: alice.id }, carol); - - const res = await api('i/notifications-grouped', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - - await api('following/delete', { userId: alice.id }, bob); - await api('following/delete', { userId: alice.id }, carol); - }); - - test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => { - await api('i/update', { isLocked: true }, alice); - await api('following/create', { userId: alice.id }, bob); - await api('following/create', { userId: alice.id }, carol); - - const res = await api('i/notifications-grouped', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); + assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); + assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); }); }); }); diff --git a/packages/backend/test/e2e/nodeinfo.ts b/packages/backend/test/e2e/nodeinfo.ts deleted file mode 100644 index 28b96fe8c8..0000000000 --- a/packages/backend/test/e2e/nodeinfo.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import { relativeFetch } from '../utils.js'; - -describe('nodeinfo', () => { - test('nodeinfo 2.1', async () => { - const res = await relativeFetch('nodeinfo/2.1'); - assert.ok(res.ok); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); - - const nodeInfo = await res.json() as any; - assert.strictEqual(nodeInfo.software.name, 'misskey'); - }); - - test('nodeinfo 2.0', async () => { - const res = await relativeFetch('nodeinfo/2.0'); - assert.ok(res.ok); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); - - const nodeInfo = await res.json() as any; - assert.strictEqual(nodeInfo.software.name, 'misskey'); - }); -}); diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 5937eb9b49..33da811a26 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -1,41 +1,36 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { Repository } from "typeorm"; - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { MiNote } from '@/models/Note.js'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { api, castAsError, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js'; +import { Note } from '@/models/entities/Note.js'; +import { signup, post, uploadUrl, startServer, initTestDb, api, uploadFile } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; describe('Note', () => { - let Notes: Repository; + let app: INestApplicationContext; + let Notes: any; - let root: misskey.entities.SignupResponse; - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - let tom: misskey.entities.SignupResponse; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); const connection = await initTestDb(true); - Notes = connection.getRepository(MiNote); - root = await signup({ username: 'root' }); + Notes = connection.getRepository(Note); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); - tom = await signup({ username: 'tom', host: 'example.com' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + test('投稿できる', async () => { const post = { text: 'test', }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); @@ -43,9 +38,9 @@ describe('Note', () => { }); test('ファイルを添付できる', async () => { - const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg'); + const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); - const res = await api('notes/create', { + const res = await api('/notes/create', { fileIds: [file.id], }, alice); @@ -55,36 +50,36 @@ describe('Note', () => { }, 1000 * 10); test('他人のファイルで怒られる', async () => { - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg'); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); - const res = await api('notes/create', { + const res = await api('/notes/create', { text: 'test', fileIds: [file.id], }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE'); - assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); + assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); + assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); }, 1000 * 10); test('存在しないファイルで怒られる', async () => { - const res = await api('notes/create', { + const res = await api('/notes/create', { text: 'test', fileIds: ['000000000000000000000000'], }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE'); - assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); + assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); + assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); }); test('不正なファイルIDで怒られる', async () => { - const res = await api('notes/create', { + const res = await api('/notes/create', { fileIds: ['kyoppie'], }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE'); - assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); + assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); + assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); }); test('返信できる', async () => { @@ -97,13 +92,12 @@ describe('Note', () => { replyId: bobPost.id, }; - const res = await api('notes/create', alicePost, alice); + const res = await api('/notes/create', alicePost, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.createdNote.text, alicePost.text); assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId); - assert.ok(res.body.createdNote.reply); assert.strictEqual(res.body.createdNote.reply.text, bobPost.text); }); @@ -116,12 +110,11 @@ describe('Note', () => { renoteId: bobPost.id, }; - const res = await api('notes/create', alicePost, alice); + const res = await api('/notes/create', alicePost, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); - assert.ok(res.body.createdNote.renote); assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); }); @@ -135,31 +128,17 @@ describe('Note', () => { renoteId: bobPost.id, }; - const res = await api('notes/create', alicePost, alice); + const res = await api('/notes/create', alicePost, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.createdNote.text, alicePost.text); assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); - assert.ok(res.body.createdNote.renote); assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); }); - test('引用renoteで空白文字のみで構成されたtextにするとレスポンスがtext: nullになる', async () => { - const bobPost = await post(bob, { - text: 'test', - }); - const res = await api('notes/create', { - text: ' ', - renoteId: bobPost.id, - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.createdNote.text, null); - }); - test('visibility: followersでrenoteできる', async () => { - const createRes = await api('notes/create', { + const createRes = await api('/notes/create', { text: 'test', visibility: 'followers', }, alice); @@ -167,7 +146,7 @@ describe('Note', () => { assert.strictEqual(createRes.status, 200); const renoteId = createRes.body.createdNote.id; - const renoteRes = await api('notes/create', { + const renoteRes = await api('/notes/create', { visibility: 'followers', renoteId, }, alice); @@ -176,107 +155,26 @@ describe('Note', () => { assert.strictEqual(renoteRes.body.createdNote.renoteId, renoteId); assert.strictEqual(renoteRes.body.createdNote.visibility, 'followers'); - const deleteRes = await api('notes/delete', { + const deleteRes = await api('/notes/delete', { noteId: renoteRes.body.createdNote.id, }, alice); assert.strictEqual(deleteRes.status, 204); }); - test('visibility: followersなノートに対してフォロワーはリプライできる', async () => { - await api('following/create', { - userId: alice.id, - }, bob); - - const aliceNote = await api('notes/create', { - text: 'direct note to bob', - visibility: 'followers', - }, alice); - - assert.strictEqual(aliceNote.status, 200); - - const replyId = aliceNote.body.createdNote.id; - const bobReply = await api('notes/create', { - text: 'reply to alice note', - replyId, - }, bob); - - assert.strictEqual(bobReply.status, 200); - assert.strictEqual(bobReply.body.createdNote.replyId, replyId); - - await api('following/delete', { - userId: alice.id, - }, bob); - }); - - test('visibility: followersなノートに対してフォロワーでないユーザーがリプライしようとすると怒られる', async () => { - const aliceNote = await api('notes/create', { - text: 'direct note to bob', - visibility: 'followers', - }, alice); - - assert.strictEqual(aliceNote.status, 200); - - const bobReply = await api('notes/create', { - text: 'reply to alice note', - replyId: aliceNote.body.createdNote.id, - }, bob); - - assert.strictEqual(bobReply.status, 400); - assert.strictEqual(castAsError(bobReply.body).error.code, 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE'); - }); - - test('visibility: specifiedなノートに対してvisibility: specifiedで返信できる', async () => { - const aliceNote = await api('notes/create', { - text: 'direct note to bob', - visibility: 'specified', - visibleUserIds: [bob.id], - }, alice); - - assert.strictEqual(aliceNote.status, 200); - - const bobReply = await api('notes/create', { - text: 'reply to alice note', - replyId: aliceNote.body.createdNote.id, - visibility: 'specified', - visibleUserIds: [alice.id], - }, bob); - - assert.strictEqual(bobReply.status, 200); - }); - - test('visibility: specifiedなノートに対してvisibility: follwersで返信しようとすると怒られる', async () => { - const aliceNote = await api('notes/create', { - text: 'direct note to bob', - visibility: 'specified', - visibleUserIds: [bob.id], - }, alice); - - assert.strictEqual(aliceNote.status, 200); - - const bobReply = await api('notes/create', { - text: 'reply to alice note with visibility: followers', - replyId: aliceNote.body.createdNote.id, - visibility: 'followers', - }, bob); - - assert.strictEqual(bobReply.status, 400); - assert.strictEqual(castAsError(bobReply.body).error.code, 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY'); - }); - test('文字数ぎりぎりで怒られない', async () => { const post = { - text: '!'.repeat(MAX_NOTE_TEXT_LENGTH), // 3000文字 + text: '!'.repeat(3000), }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 200); }); test('文字数オーバーで怒られる', async () => { const post = { - text: '!'.repeat(MAX_NOTE_TEXT_LENGTH + 1), // 3001文字 + text: '!'.repeat(3001), }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -285,7 +183,7 @@ describe('Note', () => { text: 'test', replyId: '000000000000000000000000', }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -293,7 +191,7 @@ describe('Note', () => { const post = { renoteId: '000000000000000000000000', }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -302,7 +200,7 @@ describe('Note', () => { text: 'test', replyId: 'foo', }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -310,7 +208,7 @@ describe('Note', () => { const post = { renoteId: 'foo', }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -319,7 +217,7 @@ describe('Note', () => { text: '@ghost yo', }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); @@ -331,139 +229,129 @@ describe('Note', () => { text: '@bob @bob @bob yo', }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.createdNote.text, post.text); const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id }); - assert.ok(noteDoc); assert.deepStrictEqual(noteDoc.mentions, [bob.id]); }); describe('添付ファイル情報', () => { test('ファイルを添付した場合、投稿成功時にファイル情報入りのレスポンスが帰ってくる', async () => { const file = await uploadFile(alice); - const res = await api('notes/create', { - fileIds: [file.body!.id], + const res = await api('/notes/create', { + fileIds: [file.body.id], }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.ok(res.body.createdNote.files); assert.strictEqual(res.body.createdNote.files.length, 1); - assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id); + assert.strictEqual(res.body.createdNote.files[0].id, file.body.id); }); test('ファイルを添付した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { const file = await uploadFile(alice); - const createdNote = await api('notes/create', { - fileIds: [file.body!.id], + const createdNote = await api('/notes/create', { + fileIds: [file.body.id], }, alice); assert.strictEqual(createdNote.status, 200); - const res = await api('notes', { + const res = await api('/notes', { withFiles: true, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - const myNote = res.body.find(note => note.id === createdNote.body.createdNote.id); - assert.ok(myNote); - assert.ok(myNote.files); + const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id); + assert.notEqual(myNote, null); assert.strictEqual(myNote.files.length, 1); - assert.strictEqual(myNote.files[0].id, file.body!.id); + assert.strictEqual(myNote.files[0].id, file.body.id); }); test('ファイルが添付されたノートをリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { const file = await uploadFile(alice); - const createdNote = await api('notes/create', { - fileIds: [file.body!.id], + const createdNote = await api('/notes/create', { + fileIds: [file.body.id], }, alice); assert.strictEqual(createdNote.status, 200); - const renoted = await api('notes/create', { + const renoted = await api('/notes/create', { renoteId: createdNote.body.createdNote.id, }, alice); assert.strictEqual(renoted.status, 200); - const res = await api('notes', { + const res = await api('/notes', { renote: true, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id); - assert.ok(myNote); - assert.ok(myNote.renote); - assert.ok(myNote.renote.files); + assert.notEqual(myNote, null); assert.strictEqual(myNote.renote.files.length, 1); - assert.strictEqual(myNote.renote.files[0].id, file.body!.id); + assert.strictEqual(myNote.renote.files[0].id, file.body.id); }); test('ファイルが添付されたノートに返信した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { const file = await uploadFile(alice); - const createdNote = await api('notes/create', { - fileIds: [file.body!.id], + const createdNote = await api('/notes/create', { + fileIds: [file.body.id], }, alice); assert.strictEqual(createdNote.status, 200); - const reply = await api('notes/create', { + const reply = await api('/notes/create', { replyId: createdNote.body.createdNote.id, text: 'this is reply', }, alice); assert.strictEqual(reply.status, 200); - const res = await api('notes', { + const res = await api('/notes', { reply: true, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id); - assert.ok(myNote); - assert.ok(myNote.reply); - assert.ok(myNote.reply.files); + assert.notEqual(myNote, null); assert.strictEqual(myNote.reply.files.length, 1); - assert.strictEqual(myNote.reply.files[0].id, file.body!.id); + assert.strictEqual(myNote.reply.files[0].id, file.body.id); }); test('ファイルが添付されたノートへの返信をリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { const file = await uploadFile(alice); - const createdNote = await api('notes/create', { - fileIds: [file.body!.id], + const createdNote = await api('/notes/create', { + fileIds: [file.body.id], }, alice); assert.strictEqual(createdNote.status, 200); - const reply = await api('notes/create', { + const reply = await api('/notes/create', { replyId: createdNote.body.createdNote.id, text: 'this is reply', }, alice); assert.strictEqual(reply.status, 200); - const renoted = await api('notes/create', { + const renoted = await api('/notes/create', { renoteId: reply.body.createdNote.id, }, alice); assert.strictEqual(renoted.status, 200); - const res = await api('notes', { + const res = await api('/notes', { renote: true, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id); - assert.ok(myNote); - assert.ok(myNote.renote); - assert.ok(myNote.renote.reply); - assert.ok(myNote.renote.reply.files); + assert.notEqual(myNote, null); assert.strictEqual(myNote.renote.reply.files.length, 1); - assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id); + assert.strictEqual(myNote.renote.reply.files[0].id, file.body.id); }); test('NSFWが強制されている場合変更できない', async () => { @@ -490,33 +378,33 @@ describe('Note', () => { value: true, }, }, - }, root); + }, alice); assert.strictEqual(res.status, 200); const assign = await api('admin/roles/assign', { userId: alice.id, roleId: res.body.id, - }, root); + }, alice); assert.strictEqual(assign.status, 204); - assert.strictEqual(file.body!.isSensitive, false); + assert.strictEqual(file.body.isSensitive, false); const nsfwfile = await uploadFile(alice); assert.strictEqual(nsfwfile.status, 200); - assert.strictEqual(nsfwfile.body!.isSensitive, true); + assert.strictEqual(nsfwfile.body.isSensitive, true); const liftnsfw = await api('drive/files/update', { - fileId: nsfwfile.body!.id, + fileId: nsfwfile.body.id, isSensitive: false, }, alice); assert.strictEqual(liftnsfw.status, 400); - assert.strictEqual(castAsError(liftnsfw.body).error.code, 'RESTRICTED_BY_ROLE'); + assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE'); const oldaddnsfw = await api('drive/files/update', { - fileId: file.body!.id, + fileId: file.body.id, isSensitive: true, }, alice); @@ -525,17 +413,17 @@ describe('Note', () => { await api('admin/roles/unassign', { userId: alice.id, roleId: res.body.id, - }, root); + }); await api('admin/roles/delete', { roleId: res.body.id, - }, root); + }, alice); }); }); describe('notes/create', () => { test('投票を添付できる', async () => { - const res = await api('notes/create', { + const res = await api('/notes/create', { text: 'test', poll: { choices: ['foo', 'bar'], @@ -548,15 +436,14 @@ describe('Note', () => { }); test('投票の選択肢が無くて怒られる', async () => { - const res = await api('notes/create', { - // @ts-expect-error poll must not be empty + const res = await api('/notes/create', { poll: {}, }, alice); assert.strictEqual(res.status, 400); }); test('投票の選択肢が無くて怒られる (空の配列)', async () => { - const res = await api('notes/create', { + const res = await api('/notes/create', { poll: { choices: [], }, @@ -565,7 +452,7 @@ describe('Note', () => { }); test('投票の選択肢が1つで怒られる', async () => { - const res = await api('notes/create', { + const res = await api('/notes/create', { poll: { choices: ['Strawberry Pasta'], }, @@ -574,14 +461,14 @@ describe('Note', () => { }); test('投票できる', async () => { - const { body } = await api('notes/create', { + const { body } = await api('/notes/create', { text: 'test', poll: { choices: ['sakura', 'izumi', 'ako'], }, }, alice); - const res = await api('notes/polls/vote', { + const res = await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 1, }, alice); @@ -590,19 +477,19 @@ describe('Note', () => { }); test('複数投票できない', async () => { - const { body } = await api('notes/create', { + const { body } = await api('/notes/create', { text: 'test', poll: { choices: ['sakura', 'izumi', 'ako'], }, }, alice); - await api('notes/polls/vote', { + await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 0, }, alice); - const res = await api('notes/polls/vote', { + const res = await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 2, }, alice); @@ -611,7 +498,7 @@ describe('Note', () => { }); test('許可されている場合は複数投票できる', async () => { - const { body } = await api('notes/create', { + const { body } = await api('/notes/create', { text: 'test', poll: { choices: ['sakura', 'izumi', 'ako'], @@ -619,17 +506,17 @@ describe('Note', () => { }, }, alice); - await api('notes/polls/vote', { + await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 0, }, alice); - await api('notes/polls/vote', { + await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 1, }, alice); - const res = await api('notes/polls/vote', { + const res = await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 2, }, alice); @@ -638,7 +525,7 @@ describe('Note', () => { }); test('締め切られている場合は投票できない', async () => { - const { body } = await api('notes/create', { + const { body } = await api('/notes/create', { text: 'test', poll: { choices: ['sakura', 'izumi', 'ako'], @@ -648,7 +535,7 @@ describe('Note', () => { await new Promise(x => setTimeout(x, 2)); - const res = await api('notes/polls/vote', { + const res = await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 1, }, alice); @@ -659,32 +546,33 @@ describe('Note', () => { test('センシティブな投稿はhomeになる (単語指定)', async () => { const sensitive = await api('admin/update-meta', { sensitiveWords: [ - 'test', - ], - }, root); + "test", + ] + }, alice); assert.strictEqual(sensitive.status, 204); await new Promise(x => setTimeout(x, 2)); - const note1 = await api('notes/create', { + const note1 = await api('/notes/create', { text: 'hogetesthuge', }, alice); assert.strictEqual(note1.status, 200); assert.strictEqual(note1.body.createdNote.visibility, 'home'); + }); test('センシティブな投稿はhomeになる (正規表現)', async () => { const sensitive = await api('admin/update-meta', { sensitiveWords: [ - '/Test/i', - ], - }, root); + "/Test/i", + ] + }, alice); assert.strictEqual(sensitive.status, 204); - const note2 = await api('notes/create', { + const note2 = await api('/notes/create', { text: 'hogetesthuge', }, alice); @@ -695,254 +583,19 @@ describe('Note', () => { test('センシティブな投稿はhomeになる (スペースアンド)', async () => { const sensitive = await api('admin/update-meta', { sensitiveWords: [ - 'Test hoge', - ], - }, root); + "Test hoge" + ] + }, alice); assert.strictEqual(sensitive.status, 204); - const note2 = await api('notes/create', { + const note2 = await api('/notes/create', { text: 'hogeTesthuge', }, alice); assert.strictEqual(note2.status, 200); assert.strictEqual(note2.body.createdNote.visibility, 'home'); - }); - test('禁止ワードを含む投稿はエラーになる (単語指定)', async () => { - const prohibited = await api('admin/update-meta', { - prohibitedWords: [ - 'test', - ], - }, root); - - assert.strictEqual(prohibited.status, 204); - - await new Promise(x => setTimeout(x, 2)); - - const note1 = await api('notes/create', { - text: 'hogetesthuge', - }, alice); - - assert.strictEqual(note1.status, 400); - assert.strictEqual(castAsError(note1.body).error.code, 'CONTAINS_PROHIBITED_WORDS'); - }); - - test('禁止ワードを含む投稿はエラーになる (正規表現)', async () => { - const prohibited = await api('admin/update-meta', { - prohibitedWords: [ - '/Test/i', - ], - }, root); - - assert.strictEqual(prohibited.status, 204); - - const note2 = await api('notes/create', { - text: 'hogetesthuge', - }, alice); - - assert.strictEqual(note2.status, 400); - assert.strictEqual(castAsError(note2.body).error.code, 'CONTAINS_PROHIBITED_WORDS'); - }); - - test('禁止ワードを含む投稿はエラーになる (スペースアンド)', async () => { - const prohibited = await api('admin/update-meta', { - prohibitedWords: [ - 'Test hoge', - ], - }, root); - - assert.strictEqual(prohibited.status, 204); - - const note2 = await api('notes/create', { - text: 'hogeTesthuge', - }, alice); - - assert.strictEqual(note2.status, 400); - assert.strictEqual(castAsError(note2.body).error.code, 'CONTAINS_PROHIBITED_WORDS'); - }); - - test('禁止ワードを含んでるリモートノートもエラーになる', async () => { - const prohibited = await api('admin/update-meta', { - prohibitedWords: [ - 'test', - ], - }, root); - - assert.strictEqual(prohibited.status, 204); - - await new Promise(x => setTimeout(x, 2)); - - const note1 = await api('notes/create', { - text: 'hogetesthuge', - }, tom); - - assert.strictEqual(note1.status, 400); - }); - - test('メンションの数が上限を超えるとエラーになる', async () => { - const res = await api('admin/roles/create', { - name: 'test', - description: '', - color: null, - iconUrl: null, - displayOrder: 0, - target: 'manual', - condFormula: {}, - isAdministrator: false, - isModerator: false, - isPublic: false, - isExplorable: false, - asBadge: false, - canEditMembersByModerator: false, - policies: { - mentionLimit: { - useDefault: false, - priority: 1, - value: 0, - }, - }, - }, root); - - assert.strictEqual(res.status, 200); - - await new Promise(x => setTimeout(x, 2)); - - const assign = await api('admin/roles/assign', { - userId: alice.id, - roleId: res.body.id, - }, root); - - assert.strictEqual(assign.status, 204); - - await new Promise(x => setTimeout(x, 2)); - - const note = await api('notes/create', { - text: '@bob potentially annoying text', - }, alice); - - assert.strictEqual(note.status, 400); - assert.strictEqual(castAsError(note.body).error.code, 'CONTAINS_TOO_MANY_MENTIONS'); - - await api('admin/roles/unassign', { - userId: alice.id, - roleId: res.body.id, - }, root); - - await api('admin/roles/delete', { - roleId: res.body.id, - }, root); - }); - - test('ダイレクト投稿もエラーになる', async () => { - const res = await api('admin/roles/create', { - name: 'test', - description: '', - color: null, - iconUrl: null, - displayOrder: 0, - target: 'manual', - condFormula: {}, - isAdministrator: false, - isModerator: false, - isPublic: false, - isExplorable: false, - asBadge: false, - canEditMembersByModerator: false, - policies: { - mentionLimit: { - useDefault: false, - priority: 1, - value: 0, - }, - }, - }, root); - - assert.strictEqual(res.status, 200); - - await new Promise(x => setTimeout(x, 2)); - - const assign = await api('admin/roles/assign', { - userId: alice.id, - roleId: res.body.id, - }, root); - - assert.strictEqual(assign.status, 204); - - await new Promise(x => setTimeout(x, 2)); - - const note = await api('notes/create', { - text: 'potentially annoying text', - visibility: 'specified', - visibleUserIds: [bob.id], - }, alice); - - assert.strictEqual(note.status, 400); - assert.strictEqual(castAsError(note.body).error.code, 'CONTAINS_TOO_MANY_MENTIONS'); - - await api('admin/roles/unassign', { - userId: alice.id, - roleId: res.body.id, - }, root); - - await api('admin/roles/delete', { - roleId: res.body.id, - }, root); - }); - - test('ダイレクトの宛先とメンションが同じ場合は重複してカウントしない', async () => { - const res = await api('admin/roles/create', { - name: 'test', - description: '', - color: null, - iconUrl: null, - displayOrder: 0, - target: 'manual', - condFormula: {}, - isAdministrator: false, - isModerator: false, - isPublic: false, - isExplorable: false, - asBadge: false, - canEditMembersByModerator: false, - policies: { - mentionLimit: { - useDefault: false, - priority: 1, - value: 1, - }, - }, - }, root); - - assert.strictEqual(res.status, 200); - - await new Promise(x => setTimeout(x, 2)); - - const assign = await api('admin/roles/assign', { - userId: alice.id, - roleId: res.body.id, - }, root); - - assert.strictEqual(assign.status, 204); - - await new Promise(x => setTimeout(x, 2)); - - const note = await api('notes/create', { - text: '@bob potentially annoying text', - visibility: 'specified', - visibleUserIds: [bob.id], - }, alice); - - assert.strictEqual(note.status, 200); - - await api('admin/roles/unassign', { - userId: alice.id, - roleId: res.body.id, - }, root); - - await api('admin/roles/delete', { - roleId: res.body.id, - }, root); }); }); @@ -966,7 +619,6 @@ describe('Note', () => { assert.strictEqual(deleteOneRes.status, 204); let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); - assert.ok(mainNote); assert.strictEqual(mainNote.repliesCount, 1); const deleteTwoRes = await api('notes/delete', { @@ -975,65 +627,7 @@ describe('Note', () => { assert.strictEqual(deleteTwoRes.status, 204); mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); - assert.ok(mainNote); assert.strictEqual(mainNote.repliesCount, 0); }); }); - - describe('notes/translate', () => { - describe('翻訳機能の利用が許可されていない場合', () => { - let cannotTranslateRole: misskey.entities.Role; - - beforeAll(async () => { - cannotTranslateRole = await role(root, {}, { canUseTranslator: false }); - await api('admin/roles/assign', { roleId: cannotTranslateRole.id, userId: alice.id }, root); - }); - - test('翻訳機能の利用が許可されていない場合翻訳できない', async () => { - const aliceNote = await post(alice, { text: 'Hello' }); - const res = await api('notes/translate', { - noteId: aliceNote.id, - targetLang: 'ja', - }, alice); - - assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE'); - }); - - afterAll(async () => { - await api('admin/roles/unassign', { roleId: cannotTranslateRole.id, userId: alice.id }, root); - }); - }); - - test('存在しないノートは翻訳できない', async () => { - const res = await api('notes/translate', { noteId: 'foo', targetLang: 'ja' }, alice); - - assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_NOTE'); - }); - - test('不可視なノートは翻訳できない', async () => { - const aliceNote = await post(alice, { visibility: 'followers', text: 'Hello' }); - const bobTranslateAttempt = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, bob); - - assert.strictEqual(bobTranslateAttempt.status, 400); - assert.strictEqual(castAsError(bobTranslateAttempt.body).error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE'); - }); - - test('text: null なノートを翻訳すると空のレスポンスが返ってくる', async () => { - const aliceNote = await post(alice, { text: null, poll: { choices: ['kinoko', 'takenoko'] } }); - const res = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, alice); - - assert.strictEqual(res.status, 204); - }); - - test('サーバーに DeepL 認証キーが登録されていない場合翻訳できない', async () => { - const aliceNote = await post(alice, { text: 'Hello' }); - const res = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, alice); - - // NOTE: デフォルトでは登録されていないので落ちる - assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE'); - }); - }); }); diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts index f639f90ea6..3762762ebc 100644 --- a/packages/backend/test/e2e/oauth.ts +++ b/packages/backend/test/e2e/oauth.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - /** * Basic OAuth tests to make sure the library is correctly integrated to Misskey * and not regressed by version updates or potential migration to another library. @@ -11,18 +6,13 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { - AuthorizationCode, - type AuthorizationTokenConfig, - ClientCredentials, - ModuleOptions, - ResourceOwnerPassword, -} from 'simple-oauth2'; +import { AuthorizationCode, ResourceOwnerPassword, type AuthorizationTokenConfig, ClientCredentials, ModuleOptions } from 'simple-oauth2'; import pkceChallenge from 'pkce-challenge'; import { JSDOM } from 'jsdom'; -import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify'; -import { api, port, sendEnvUpdateRequest, signup } from '../utils.js'; +import Fastify, { type FastifyReply, type FastifyInstance } from 'fastify'; +import { api, port, signup, startServer } from '../utils.js'; import type * as misskey from 'misskey-js'; +import type { INestApplicationContext } from '@nestjs/common'; const host = `http://127.0.0.1:${port}`; @@ -72,16 +62,15 @@ const clientConfig: ModuleOptions<'client_id'> = { }, }; -function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined, clientLogo: string | undefined } { +function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined } { const fragment = JSDOM.fragment(html); return { transactionId: fragment.querySelector('meta[name="misskey:oauth:transaction-id"]')?.content, clientName: fragment.querySelector('meta[name="misskey:oauth:client-name"]')?.content, - clientLogo: fragment.querySelector('meta[name="misskey:oauth:client-logo"]')?.content, }; } -function fetchDecision(transactionId: string, user: misskey.entities.SignupResponse, { cancel }: { cancel?: boolean } = {}): Promise { +function fetchDecision(transactionId: string, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise { return fetch(new URL('/oauth/decision', host), { method: 'post', body: new URLSearchParams({ @@ -96,14 +85,14 @@ function fetchDecision(transactionId: string, user: misskey.entities.SignupRespo }); } -async function fetchDecisionFromResponse(response: Response, user: misskey.entities.SignupResponse, { cancel }: { cancel?: boolean } = {}): Promise { +async function fetchDecisionFromResponse(response: Response, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise { const { transactionId } = getMeta(await response.text()); assert.ok(transactionId); return await fetchDecision(transactionId, user, { cancel }); } -async function fetchAuthorizationCode(user: misskey.entities.SignupResponse, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> { +async function fetchAuthorizationCode(user: misskey.entities.MeSignup, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> { const client = new AuthorizationCode(clientConfig); const response = await fetch(client.authorizeURL({ @@ -153,36 +142,36 @@ async function assertDirectError(response: Response, status: number, error: stri } describe('OAuth', () => { + let app: INestApplicationContext; let fastify: FastifyInstance; - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - - let sender: (reply: FastifyReply) => void; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); - - fastify = Fastify(); - fastify.get('/', async (request, reply) => { - sender(reply); - }); - await fastify.listen({ port: clientPort }); }, 1000 * 60 * 2); beforeEach(async () => { - await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '' }); - sender = (reply): void => { + process.env.MISSKEY_TEST_CHECK_IP_RANGE = ''; + fastify = Fastify(); + fastify.get('/', async (request, reply) => { reply.send(` -
Misklient +
Misklient `); - }; + }); + await fastify.listen({ port: clientPort }); }); afterAll(async () => { + await app.close(); + }); + + afterEach(async () => { await fastify.close(); }); @@ -815,7 +804,7 @@ describe('OAuth', () => { reply.header('Link', '; rel="redirect_uri"'); reply.send(` -
Misklient +
Misklient `); }, 'Mixed links': reply => { @@ -823,14 +812,14 @@ describe('OAuth', () => { reply.send(` -
Misklient +
Misklient `); }, 'Multiple items in Link header': reply => { reply.header('Link', '; rel="redirect_uri",; rel="redirect_uri"'); reply.send(` -
Misklient +
Misklient `); }, 'Multiple items in HTML': reply => { @@ -838,14 +827,18 @@ describe('OAuth', () => { -
Misklient +
Misklient `); }, }; for (const [title, replyFunc] of Object.entries(tests)) { test(title, async () => { - sender = replyFunc; + await fastify.close(); + + fastify = Fastify(); + fastify.get('/', async (request, reply) => replyFunc(reply)); + await fastify.listen({ port: clientPort }); const client = new AuthorizationCode(clientConfig); @@ -861,12 +854,16 @@ describe('OAuth', () => { } test('No item', async () => { - sender = (reply): void => { + await fastify.close(); + + fastify = Fastify(); + fastify.get('/', async (request, reply) => { reply.send(` - -
Misklient - `); - }; + +
Misklient + `); + }); + await fastify.listen({ port: clientPort }); const client = new AuthorizationCode(clientConfig); @@ -884,7 +881,7 @@ describe('OAuth', () => { }); test('Disallow loopback', async () => { - await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '1' }); + process.env.MISSKEY_TEST_CHECK_IP_RANGE = '1'; const client = new AuthorizationCode(clientConfig); const response = await fetch(client.authorizeURL({ @@ -898,86 +895,14 @@ describe('OAuth', () => { }); test('Missing name', async () => { - sender = (reply): void => { + await fastify.close(); + + fastify = Fastify(); + fastify.get('/', async (request, reply) => { reply.header('Link', '; rel="redirect_uri"'); reply.send(); - }; - - const client = new AuthorizationCode(clientConfig); - - const response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - assert.strictEqual(response.status, 200); - assert.strictEqual(getMeta(await response.text()).clientName, `http://127.0.0.1:${clientPort}/`); - }); - - test('With Logo', async () => { - sender = (reply): void => { - reply.header('Link', '; rel="redirect_uri"'); - reply.send(` - - - `); - reply.send(); - }; - - const client = new AuthorizationCode(clientConfig); - - const response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - assert.strictEqual(response.status, 200); - const meta = getMeta(await response.text()); - assert.strictEqual(meta.clientName, 'Misklient'); - assert.strictEqual(meta.clientLogo, `http://127.0.0.1:${clientPort}/logo.png`); - }); - - test('Missing Logo', async () => { - sender = (reply): void => { - reply.header('Link', '; rel="redirect_uri"'); - reply.send(` - -
Misklient - `); - reply.send(); - }; - - const client = new AuthorizationCode(clientConfig); - - const response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - assert.strictEqual(response.status, 200); - const meta = getMeta(await response.text()); - assert.strictEqual(meta.clientName, 'Misklient'); - assert.strictEqual(meta.clientLogo, undefined); - }); - - test('Mismatching URL in h-app', async () => { - sender = (reply): void => { - reply.header('Link', '; rel="redirect_uri"'); - reply.send(` - -
Misklient - `); - reply.send(); - }; + }); + await fastify.listen({ port: clientPort }); const client = new AuthorizationCode(clientConfig); @@ -997,24 +922,4 @@ describe('OAuth', () => { const response = await fetch(new URL('/oauth/foo', host)); assert.strictEqual(response.status, 404); }); - - describe('CORS', () => { - test('Token endpoint should support CORS', async () => { - const response = await fetch(new URL('/oauth/token', host), { method: 'POST' }); - assert.ok(!response.ok); - assert.strictEqual(response.headers.get('Access-Control-Allow-Origin'), '*'); - }); - - test('Authorize endpoint should not support CORS', async () => { - const response = await fetch(new URL('/oauth/authorize', host), { method: 'GET' }); - assert.ok(!response.ok); - assert.ok(!response.headers.has('Access-Control-Allow-Origin')); - }); - - test('Decision endpoint should not support CORS', async () => { - const response = await fetch(new URL('/oauth/decision', host), { method: 'POST' }); - assert.ok(!response.ok); - assert.ok(!response.headers.has('Access-Control-Allow-Origin')); - }); - }); }); diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts index 0f636b9ae2..72fc599aaf 100644 --- a/packages/backend/test/e2e/renote-mute.ts +++ b/packages/backend/test/e2e/renote-mute.ts @@ -1,29 +1,31 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { setTimeout } from 'node:timers/promises'; -import { api, post, signup, waitFire } from '../utils.js'; +import { signup, api, post, react, startServer, waitFire } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; describe('Renote Mute', () => { + let app: INestApplicationContext; + // alice mutes carol - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - let carol: misskey.entities.SignupResponse; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + test('ミュート作成', async () => { - const res = await api('renote-mute/create', { + const res = await api('/renote-mute/create', { userId: carol.id, }, alice); @@ -35,16 +37,13 @@ describe('Renote Mute', () => { const carolRenote = await post(carol, { renoteId: bobNote.id }); const carolNote = await post(carol, { text: 'hi' }); - // redisに追加されるのを待つ - await setTimeout(100); - - const res = await api('notes/local-timeline', {}, alice); + const res = await api('/notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolRenote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); }); test('タイムラインにリノートミュートしているユーザーの引用が含まれる', async () => { @@ -52,32 +51,13 @@ describe('Renote Mute', () => { const carolRenote = await post(carol, { renoteId: bobNote.id, text: 'kore' }); const carolNote = await post(carol, { text: 'hi' }); - // redisに追加されるのを待つ - await setTimeout(100); - - const res = await api('notes/local-timeline', {}, alice); + const res = await api('/notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolRenote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); - }); - - // #12956 - test('タイムラインにリノートミュートしているユーザーの通常ノートのリノートが含まれる', async () => { - const carolNote = await post(carol, { text: 'hi' }); - const bobRenote = await post(bob, { renoteId: carolNote.id }); - - // redisに追加されるのを待つ - await setTimeout(100); - - const res = await api('notes/local-timeline', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); }); test('ストリームにリノートミュートしているユーザーのリノートが流れない', async () => { @@ -103,17 +83,4 @@ describe('Renote Mute', () => { assert.strictEqual(fired, true); }); - - // #12956 - test('ストリームにリノートミュートしているユーザーの通常ノートのリノートが流れてくる', async () => { - const carolbNote = await post(carol, { text: 'hi' }); - - const fired = await waitFire( - alice, 'localTimeline', - () => api('notes/create', { renoteId: carolbNote.id }, bob), - msg => msg.type === 'note' && msg.body.userId === bob.id, - ); - - assert.strictEqual(fired, true); - }); }); diff --git a/packages/backend/test/e2e/reversi-game.ts b/packages/backend/test/e2e/reversi-game.ts deleted file mode 100644 index 788255beac..0000000000 --- a/packages/backend/test/e2e/reversi-game.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import { ReversiMatchResponse } from 'misskey-js/entities.js'; -import { api, signup } from '../utils.js'; -import type * as misskey from 'misskey-js'; - -describe('ReversiGame', () => { - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - - beforeAll(async () => { - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - }, 1000 * 60 * 2); - - test('matches when alice invites bob and bob accepts', async () => { - const response1 = await api('reversi/match', { userId: bob.id }, alice); - assert.strictEqual(response1.status, 204); - assert.strictEqual(response1.body, null); - const response2 = await api('reversi/match', { userId: alice.id }, bob); - assert.strictEqual(response2.status, 200); - assert.notStrictEqual(response2.body, null); - const body = response2.body as ReversiMatchResponse; - assert.strictEqual(body.user1.id, alice.id); - assert.strictEqual(body.user2.id, bob.id); - }); -}); diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index 72f26a38e0..2cddafed2e 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -1,22 +1,19 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { WebSocket } from 'ws'; -import { MiFollowing } from '@/models/Following.js'; -import { api, createAppToken, initTestDb, port, post, signup, waitFire } from '../utils.js'; +import { Following } from '@/models/entities/Following.js'; +import { connectStream, signup, api, post, startServer, initTestDb, waitFire } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; describe('Streaming', () => { + let app: INestApplicationContext; let Followings: any; const follow = async (follower: any, followee: any) => { await Followings.save({ id: 'a', + createdAt: new Date(), followerId: follower.id, followeeId: followee.id, followerHost: follower.host, @@ -30,58 +27,37 @@ describe('Streaming', () => { describe('Streaming', () => { // Local users - let ayano: misskey.entities.SignupResponse; - let kyoko: misskey.entities.SignupResponse; - let chitose: misskey.entities.SignupResponse; - let kanako: misskey.entities.SignupResponse; - let erin: misskey.entities.SignupResponse; + let ayano: misskey.entities.MeSignup; + let kyoko: misskey.entities.MeSignup; + let chitose: misskey.entities.MeSignup; // Remote users - let akari: misskey.entities.SignupResponse; - let chinatsu: misskey.entities.SignupResponse; - let takumi: misskey.entities.SignupResponse; + let akari: misskey.entities.MeSignup; + let chinatsu: misskey.entities.MeSignup; - let kyokoNote: misskey.entities.Note; - let kanakoNote: misskey.entities.Note; - let takumiNote: misskey.entities.Note; + let kyokoNote: any; let list: any; beforeAll(async () => { + app = await startServer(); const connection = await initTestDb(true); - Followings = connection.getRepository(MiFollowing); + Followings = connection.getRepository(Following); ayano = await signup({ username: 'ayano' }); kyoko = await signup({ username: 'kyoko' }); chitose = await signup({ username: 'chitose' }); - kanako = await signup({ username: 'kanako' }); - erin = await signup({ username: 'erin' }); // erin: A generic fifth participant akari = await signup({ username: 'akari', host: 'example.com' }); chinatsu = await signup({ username: 'chinatsu', host: 'example.com' }); - takumi = await signup({ username: 'takumi', host: 'example.com' }); kyokoNote = await post(kyoko, { text: 'foo' }); - kanakoNote = await post(kanako, { text: 'hoge' }); - takumiNote = await post(takumi, { text: 'piyo' }); // Follow: ayano => kyoko - await api('following/create', { userId: kyoko.id, withReplies: false }, ayano); + await api('following/create', { userId: kyoko.id }, ayano); // Follow: ayano => akari await follow(ayano, akari); - // Follow: kyoko => chitose - await api('following/create', { userId: chitose.id }, kyoko); - - // Follow: erin <=> ayano each other. - // erin => ayano: withReplies: true - await api('following/create', { userId: ayano.id, withReplies: true }, erin); - // ayano => erin: withReplies: false - await api('following/create', { userId: erin.id, withReplies: false }, ayano); - - // Mute: chitose => kanako - await api('mute/create', { userId: kanako.id }, chitose); - // List: chitose => ayano, kyoko list = await api('users/lists/create', { name: 'my list', @@ -96,13 +72,12 @@ describe('Streaming', () => { listId: list.id, userId: kyoko.id, }, chitose); - - await api('users/lists/push', { - listId: list.id, - userId: takumi.id, - }, chitose); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + describe('Events', () => { test('mention event', async () => { const fired = await waitFire( @@ -136,16 +111,6 @@ describe('Streaming', () => { assert.strictEqual(fired, true); }); - test('自分の visibility: followers な投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:Home - () => api('notes/create', { text: 'foo', visibility: 'followers' }, ayano), // ayano posts - msg => msg.type === 'note' && msg.body.text === 'foo', - ); - - assert.strictEqual(fired, true); - }); - test('フォローしているユーザーの投稿が流れる', async () => { const fired = await waitFire( ayano, 'homeTimeline', // ayano:home @@ -156,53 +121,6 @@ describe('Streaming', () => { assert.strictEqual(fired, true); }); - test('フォローしているユーザーの visibility: followers な投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko), // kyoko posts - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, true); - }); - - test('フォローしているユーザーの visibility: followers な投稿への返信が流れる', async () => { - const note = await post(kyoko, { text: 'foo', visibility: 'followers' }); - - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'bar', visibility: 'followers', replyId: note.id }, kyoko), // kyoko posts - msg => msg.type === 'note' && msg.body.userId === kyoko.id && msg.body.reply.text === 'foo', - ); - - assert.strictEqual(fired, true); - }); - - test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信が流れない', async () => { - const chitoseNote = await post(chitose, { text: 'followers-only post', visibility: 'followers' }); - - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'reply to chitose\'s followers-only post', replyId: chitoseNote.id }, kyoko), // kyoko's reply to chitose's followers-only post - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - - test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信のリノートが流れない', async () => { - const chitoseNote = await post(chitose, { text: 'followers-only post', visibility: 'followers' }); - const kyokoReply = await post(kyoko, { text: 'reply to followers-only post', replyId: chitoseNote.id }); - - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { renoteId: kyokoReply.id }, kyoko), // kyoko's renote of kyoko's reply to chitose's followers-only post - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - test('フォローしていないユーザーの投稿は流れない', async () => { const fired = await waitFire( kyoko, 'homeTimeline', // kyoko:home @@ -232,101 +150,6 @@ describe('Streaming', () => { assert.strictEqual(fired, false); }); - - /** - * TODO: 落ちる - * @see https://github.com/misskey-dev/misskey/issues/13474 - test('visibility: specified なノートで visibleUserIds に自分が含まれているときそのノートへのリプライが流れてくる', async () => { - const chitoseToKyokoAndAyano = await post(chitose, { text: 'direct note from chitose to kyoko and ayano', visibility: 'specified', visibleUserIds: [kyoko.id, ayano.id] }); - - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'direct reply from kyoko to chitose and ayano', replyId: chitoseToKyokoAndAyano.id, visibility: 'specified', visibleUserIds: [chitose.id, ayano.id] }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, - ); - - assert.strictEqual(fired, true); - }); - */ - - test('visibility: specified な投稿に対するリプライで visibleUserIds が拡張されたとき、その拡張されたユーザーの HTL にはそのリプライが流れない', async () => { - const chitoseToKyoko = await post(chitose, { text: 'direct note from chitose to kyoko', visibility: 'specified', visibleUserIds: [kyoko.id] }); - - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'direct reply from kyoko to chitose and ayano', replyId: chitoseToKyoko.id, visibility: 'specified', visibleUserIds: [chitose.id, ayano.id] }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, - ); - - assert.strictEqual(fired, false); - }); - - test('visibility: specified な投稿に対するリプライで visibleUserIds が収縮されたとき、その収縮されたユーザーの HTL にはそのリプライが流れない', async () => { - const chitoseToKyokoAndAyano = await post(chitose, { text: 'direct note from chitose to kyoko and ayano', visibility: 'specified', visibleUserIds: [kyoko.id, ayano.id] }); - - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'direct reply from kyoko to chitose', replyId: chitoseToKyokoAndAyano.id, visibility: 'specified', visibleUserIds: [chitose.id] }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, - ); - - assert.strictEqual(fired, false); - }); - - test('withRenotes: false のときリノートが流れない', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { renoteId: kyokoNote.id }, kyoko), // kyoko renote - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - { withRenotes: false }, - ); - - assert.strictEqual(fired, false); - }); - - test('withRenotes: false のとき引用リノートが流れる', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'quote', renoteId: kyokoNote.id }, kyoko), // kyoko quote - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - { withRenotes: false }, - ); - - assert.strictEqual(fired, true); - }); - - test('withRenotes: false のとき投票のみのリノートが流れる', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { poll: { choices: ['kinoko', 'takenoko'] }, renoteId: kyokoNote.id }, kyoko), // kyoko renote with poll - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - { withRenotes: false }, - ); - - assert.strictEqual(fired, true); - }); - - test('withReplies: true のとき自分のfollowers投稿に対するリプライが流れる', async () => { - const erinNote = await post(erin, { text: 'hi', visibility: 'followers' }); - const fired = await waitFire( - erin, 'homeTimeline', // erin:home - () => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano), // ayano reply to erin's followers post - msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano - ); - - assert.strictEqual(fired, true); - }); - - test('withReplies: false でも自分の投稿に対するリプライが流れる', async () => { - const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' }); - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin), // erin reply to ayano's followers post - msg => msg.type === 'note' && msg.body.userId === erin.id, // wait erin - ); - - assert.strictEqual(fired, true); - }); }); // Home describe('Local Timeline', () => { @@ -414,16 +237,6 @@ describe('Streaming', () => { assert.strictEqual(fired, true); }); - test('自分の visibility: followers な投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', - () => api('notes/create', { text: 'foo', visibility: 'followers' }, ayano), // ayano posts - msg => msg.type === 'note' && msg.body.text === 'foo', - ); - - assert.strictEqual(fired, true); - }); - test('フォローしていないローカルユーザーの投稿が流れる', async () => { const fired = await waitFire( ayano, 'hybridTimeline', // ayano:Hybrid @@ -476,16 +289,6 @@ describe('Streaming', () => { assert.strictEqual(fired, true); }); - test('フォローしているユーザーの visibility: followers な投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, true); - }); - test('フォローしていないローカルユーザーのホーム投稿は流れない', async () => { const fired = await waitFire( ayano, 'hybridTimeline', // ayano:Hybrid @@ -505,38 +308,6 @@ describe('Streaming', () => { assert.strictEqual(fired, false); }); - - test('withReplies: true のとき自分のfollowers投稿に対するリプライが流れる', async () => { - const erinNote = await post(erin, { text: 'hi', visibility: 'followers' }); - const fired = await waitFire( - erin, 'homeTimeline', // erin:home - () => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano), // ayano reply to erin's followers post - msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano - ); - - assert.strictEqual(fired, true); - }); - - test('withReplies: false でも自分の投稿に対するリプライが流れる', async () => { - const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' }); - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin), // erin reply to ayano's followers post - msg => msg.type === 'note' && msg.body.userId === erin.id, // wait erin - ); - - assert.strictEqual(fired, true); - }); - - test('withReplies: true のフォローしていない人のfollowersノートに対するリプライが流れない', async () => { - const fired = await waitFire( - erin, 'homeTimeline', // erin:home - () => api('notes/create', { text: 'hello', replyId: chitose.id }, ayano), // ayano reply to chitose's post - msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano - ); - - assert.strictEqual(fired, false); - }); }); describe('Global Timeline', () => { @@ -571,16 +342,6 @@ describe('Streaming', () => { assert.strictEqual(fired, false); }); - - test('withReplies = falseでフォローしてる人によるリプライが流れてくる', async () => { - const fired = await waitFire( - ayano, 'globalTimeline', // ayano:Global - () => api('notes/create', { text: 'foo', replyId: kanakoNote.id }, kyoko), // kyoko posts - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, true); - }); }); describe('UserList Timeline', () => { @@ -629,118 +390,6 @@ describe('Streaming', () => { assert.strictEqual(fired, false); }); - - // #10443 - test('チャンネル投稿は流れない', async () => { - // リスインしている kyoko が 任意のチャンネルに投降した時の動きを見たい - const fired = await waitFire( - chitose, 'userList', - () => api('notes/create', { text: 'foo', channelId: 'dummy' }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, - { listId: list.id }, - ); - - assert.strictEqual(fired, false); - }); - - // #10443 - test('ミュートしているユーザへのリプライがリストTLに流れない', async () => { - // chitose が kanako をミュートしている状態で、リスインしている kyoko が kanako にリプライした時の動きを見たい - const fired = await waitFire( - chitose, 'userList', - () => api('notes/create', { text: 'foo', replyId: kanakoNote.id }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, - { listId: list.id }, - ); - - assert.strictEqual(fired, false); - }); - - // #10443 - test('ミュートしているユーザの投稿をリノートしたときリストTLに流れない', async () => { - // chitose が kanako をミュートしている状態で、リスインしている kyoko が kanako のノートをリノートした時の動きを見たい - const fired = await waitFire( - chitose, 'userList', - () => api('notes/create', { renoteId: kanakoNote.id }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, - { listId: list.id }, - ); - - assert.strictEqual(fired, false); - }); - - // #10443 - test('ミュートしているサーバのノートがリストTLに流れない', async () => { - await api('i/update', { - mutedInstances: ['example.com'], - }, chitose); - - // chitose が example.com をミュートしている状態で、リスインしている takumi が ノートした時の動きを見たい - const fired = await waitFire( - chitose, 'userList', - () => api('notes/create', { text: 'foo' }, takumi), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, - { listId: list.id }, - ); - - assert.strictEqual(fired, false); - }); - - // #10443 - test('ミュートしているサーバのノートに対するリプライがリストTLに流れない', async () => { - await api('i/update', { - mutedInstances: ['example.com'], - }, chitose); - - // chitose が example.com をミュートしている状態で、リスインしている kyoko が takumi のノートにリプライした時の動きを見たい - const fired = await waitFire( - chitose, 'userList', - () => api('notes/create', { text: 'foo', replyId: takumiNote.id }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, - { listId: list.id }, - ); - - assert.strictEqual(fired, false); - }); - - // #10443 - test('ミュートしているサーバのノートに対するリノートがリストTLに流れない', async () => { - await api('i/update', { - mutedInstances: ['example.com'], - }, chitose); - - // chitose が example.com をミュートしている状態で、リスインしている kyoko が takumi のノートをリノートした時の動きを見たい - const fired = await waitFire( - chitose, 'userList', - () => api('notes/create', { renoteId: takumiNote.id }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, - { listId: list.id }, - ); - - assert.strictEqual(fired, false); - }); - }); - - test('Authentication', async () => { - const application = await createAppToken(ayano, []); - const application2 = await createAppToken(ayano, ['read:account']); - const socket = new WebSocket(`ws://127.0.0.1:${port}/streaming?i=${application}`); - const established = await new Promise((resolve, reject) => { - socket.on('error', () => resolve(false)); - socket.on('unexpected-response', () => resolve(false)); - setTimeout(() => resolve(true), 3000); - }); - - socket.close(); - assert.strictEqual(established, false); - - const fired = await waitFire( - { token: application2 }, 'hybridTimeline', - () => api('notes/create', { text: 'Hello, world!' }, ayano), - msg => msg.type === 'note' && msg.body.userId === ayano.id, - ); - - assert.strictEqual(fired, true); }); // XXX: QueryFailedError: duplicate key value violates unique constraint "IDX_347fec870eafea7b26c8a73bac" diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts deleted file mode 100644 index c98d199f35..0000000000 --- a/packages/backend/test/e2e/synalio/abuse-report.ts +++ /dev/null @@ -1,354 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { entities } from 'misskey-js'; -import { beforeEach, describe, test } from '@jest/globals'; -import { - api, - captureWebhook, - randomString, - role, - signup, - startJobQueue, - UserToken, - WEBHOOK_HOST, -} from '../../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; - -describe('[シナリオ] ユーザ通報', () => { - let queue: INestApplicationContext; - let admin: entities.SignupResponse; - let alice: entities.SignupResponse; - let bob: entities.SignupResponse; - - async function createSystemWebhook(args?: Partial, credential?: UserToken): Promise { - const res = await api( - 'admin/system-webhook/create', - { - isActive: true, - name: randomString(), - on: ['abuseReport'], - url: WEBHOOK_HOST, - secret: randomString(), - ...args, - }, - credential ?? admin, - ); - return res.body; - } - - async function createAbuseReportNotificationRecipient(args?: Partial, credential?: UserToken): Promise { - const res = await api( - 'admin/abuse-report/notification-recipient/create', - { - isActive: true, - name: randomString(), - method: 'webhook', - ...args, - }, - credential ?? admin, - ); - return res.body; - } - - async function createAbuseReport(args?: Partial, credential?: UserToken): Promise { - const res = await api( - 'users/report-abuse', - { - userId: alice.id, - comment: randomString(), - ...args, - }, - credential ?? admin, - ); - return res.body; - } - - async function resolveAbuseReport(args?: Partial, credential?: UserToken): Promise { - const res = await api( - 'admin/resolve-abuse-user-report', - { - reportId: admin.id, - ...args, - }, - credential ?? admin, - ); - return res.body; - } - - // ------------------------------------------------------------------------------------------- - - beforeAll(async () => { - queue = await startJobQueue(); - admin = await signup({ username: 'admin' }); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - - await role(admin, { isAdministrator: true }); - }, 1000 * 60 * 2); - - afterAll(async () => { - await queue.close(); - }); - - // ------------------------------------------------------------------------------------------- - - describe('SystemWebhook', () => { - beforeEach(async () => { - const webhooks = await api('admin/system-webhook/list', {}, admin); - for (const webhook of webhooks.body) { - await api('admin/system-webhook/delete', { id: webhook.id }, admin); - } - }); - - test('通報を受けた -> abuseReportが送出される', async () => { - const webhook = await createSystemWebhook({ - on: ['abuseReport'], - isActive: true, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }); - - console.log(JSON.stringify(webhookBody, null, 2)); - - expect(webhookBody.hookId).toBe(webhook.id); - expect(webhookBody.type).toBe('abuseReport'); - expect(webhookBody.body.targetUserId).toBe(alice.id); - expect(webhookBody.body.reporterId).toBe(bob.id); - expect(webhookBody.body.comment).toBe(abuse.comment); - }); - - test('通報を受けた -> abuseReportが送出される -> 解決 -> abuseReportResolvedが送出される', async () => { - const webhook = await createSystemWebhook({ - on: ['abuseReport', 'abuseReportResolved'], - isActive: true, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody1 = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }); - - console.log(JSON.stringify(webhookBody1, null, 2)); - expect(webhookBody1.hookId).toBe(webhook.id); - expect(webhookBody1.type).toBe('abuseReport'); - expect(webhookBody1.body.targetUserId).toBe(alice.id); - expect(webhookBody1.body.reporterId).toBe(bob.id); - expect(webhookBody1.body.assigneeId).toBeNull(); - expect(webhookBody1.body.resolved).toBe(false); - expect(webhookBody1.body.comment).toBe(abuse.comment); - - // 解決 - const webhookBody2 = await captureWebhook(async () => { - await resolveAbuseReport({ - reportId: webhookBody1.body.id, - }, admin); - }); - - console.log(JSON.stringify(webhookBody2, null, 2)); - expect(webhookBody2.hookId).toBe(webhook.id); - expect(webhookBody2.type).toBe('abuseReportResolved'); - expect(webhookBody2.body.targetUserId).toBe(alice.id); - expect(webhookBody2.body.reporterId).toBe(bob.id); - expect(webhookBody2.body.assigneeId).toBe(admin.id); - expect(webhookBody2.body.resolved).toBe(true); - expect(webhookBody2.body.comment).toBe(abuse.comment); - }); - - test('通報を受けた -> abuseReportが未許可の場合は送出されない', async () => { - const webhook = await createSystemWebhook({ - on: [], - isActive: true, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }).catch(e => e.message); - - expect(webhookBody).toBe('timeout'); - }); - - test('通報を受けた -> abuseReportが未許可の場合は送出されない -> 解決 -> abuseReportResolvedが送出される', async () => { - const webhook = await createSystemWebhook({ - on: ['abuseReportResolved'], - isActive: true, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody1 = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }).catch(e => e.message); - - expect(webhookBody1).toBe('timeout'); - - const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id; - - // 解決 - const webhookBody2 = await captureWebhook(async () => { - await resolveAbuseReport({ - reportId: abuseReportId, - }, admin); - }); - - console.log(JSON.stringify(webhookBody2, null, 2)); - expect(webhookBody2.hookId).toBe(webhook.id); - expect(webhookBody2.type).toBe('abuseReportResolved'); - expect(webhookBody2.body.targetUserId).toBe(alice.id); - expect(webhookBody2.body.reporterId).toBe(bob.id); - expect(webhookBody2.body.assigneeId).toBe(admin.id); - expect(webhookBody2.body.resolved).toBe(true); - expect(webhookBody2.body.comment).toBe(abuse.comment); - }); - - test('通報を受けた -> abuseReportが送出される -> 解決 -> abuseReportResolvedが未許可の場合は送出されない', async () => { - const webhook = await createSystemWebhook({ - on: ['abuseReport'], - isActive: true, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody1 = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }); - - console.log(JSON.stringify(webhookBody1, null, 2)); - expect(webhookBody1.hookId).toBe(webhook.id); - expect(webhookBody1.type).toBe('abuseReport'); - expect(webhookBody1.body.targetUserId).toBe(alice.id); - expect(webhookBody1.body.reporterId).toBe(bob.id); - expect(webhookBody1.body.assigneeId).toBeNull(); - expect(webhookBody1.body.resolved).toBe(false); - expect(webhookBody1.body.comment).toBe(abuse.comment); - - // 解決 - const webhookBody2 = await captureWebhook(async () => { - await resolveAbuseReport({ - reportId: webhookBody1.body.id, - }, admin); - }).catch(e => e.message); - - expect(webhookBody2).toBe('timeout'); - }); - - test('通報を受けた -> abuseReportが未許可の場合は送出されない -> 解決 -> abuseReportResolvedが未許可の場合は送出されない', async () => { - const webhook = await createSystemWebhook({ - on: [], - isActive: true, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody1 = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }).catch(e => e.message); - - expect(webhookBody1).toBe('timeout'); - - const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id; - - // 解決 - const webhookBody2 = await captureWebhook(async () => { - await resolveAbuseReport({ - reportId: abuseReportId, - }, admin); - }).catch(e => e.message); - - expect(webhookBody2).toBe('timeout'); - }); - - test('通報を受けた -> Webhookが無効の場合は送出されない', async () => { - const webhook = await createSystemWebhook({ - on: ['abuseReport', 'abuseReportResolved'], - isActive: false, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody1 = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }).catch(e => e.message); - - expect(webhookBody1).toBe('timeout'); - - const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id; - - // 解決 - const webhookBody2 = await captureWebhook(async () => { - await resolveAbuseReport({ - reportId: abuseReportId, - }, admin); - }).catch(e => e.message); - - expect(webhookBody2).toBe('timeout'); - }); - - test('通報を受けた -> 通知設定が無効の場合は送出されない', async () => { - const webhook = await createSystemWebhook({ - on: ['abuseReport', 'abuseReportResolved'], - isActive: true, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id, isActive: false }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody1 = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }).catch(e => e.message); - - expect(webhookBody1).toBe('timeout'); - - const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id; - - // 解決 - const webhookBody2 = await captureWebhook(async () => { - await resolveAbuseReport({ - reportId: abuseReportId, - }, admin); - }).catch(e => e.message); - - expect(webhookBody2).toBe('timeout'); - }); - }); -}); diff --git a/packages/backend/test/e2e/synalio/user-create.ts b/packages/backend/test/e2e/synalio/user-create.ts deleted file mode 100644 index cb0f68dfea..0000000000 --- a/packages/backend/test/e2e/synalio/user-create.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { setTimeout } from 'node:timers/promises'; -import { entities } from 'misskey-js'; -import { beforeEach, describe, test } from '@jest/globals'; -import { - api, - captureWebhook, - randomString, - role, - signup, - startJobQueue, - UserToken, - WEBHOOK_HOST, -} from '../../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; - -describe('[シナリオ] ユーザ作成', () => { - let queue: INestApplicationContext; - let admin: entities.SignupResponse; - - async function createSystemWebhook(args?: Partial, credential?: UserToken): Promise { - const res = await api( - 'admin/system-webhook/create', - { - isActive: true, - name: randomString(), - on: ['userCreated'], - url: WEBHOOK_HOST, - secret: randomString(), - ...args, - }, - credential ?? admin, - ); - return res.body; - } - - // ------------------------------------------------------------------------------------------- - - beforeAll(async () => { - queue = await startJobQueue(); - admin = await signup({ username: 'admin' }); - - await role(admin, { isAdministrator: true }); - }, 1000 * 60 * 2); - - afterAll(async () => { - await queue.close(); - }); - - // ------------------------------------------------------------------------------------------- - - describe('SystemWebhook', () => { - beforeEach(async () => { - const webhooks = await api('admin/system-webhook/list', {}, admin); - for (const webhook of webhooks.body) { - await api('admin/system-webhook/delete', { id: webhook.id }, admin); - } - }); - - test('ユーザが作成された -> userCreatedが送出される', async () => { - const webhook = await createSystemWebhook({ - on: ['userCreated'], - isActive: true, - }); - - let alice: any = null; - const webhookBody = await captureWebhook(async () => { - alice = await signup({ username: 'alice' }); - }); - - // webhookの送出後にいろいろやってるのでちょっと待つ必要がある - await setTimeout(2000); - - console.log(alice); - console.log(JSON.stringify(webhookBody, null, 2)); - - expect(webhookBody.hookId).toBe(webhook.id); - expect(webhookBody.type).toBe('userCreated'); - - const body = webhookBody.body as entities.UserLite; - expect(alice.id).toBe(body.id); - expect(alice.name).toBe(body.name); - expect(alice.username).toBe(body.username); - expect(alice.host).toBe(body.host); - expect(alice.avatarUrl).toBe(body.avatarUrl); - expect(alice.avatarBlurhash).toBe(body.avatarBlurhash); - expect(alice.avatarDecorations).toEqual(body.avatarDecorations); - expect(alice.isBot).toBe(body.isBot); - expect(alice.isCat).toBe(body.isCat); - expect(alice.instance).toEqual(body.instance); - expect(alice.emojis).toEqual(body.emojis); - expect(alice.onlineStatus).toBe(body.onlineStatus); - expect(alice.badgeRoles).toEqual(body.badgeRoles); - }); - - test('ユーザ作成 -> userCreatedが未許可の場合は送出されない', async () => { - await createSystemWebhook({ - on: [], - isActive: true, - }); - - let alice: any = null; - const webhookBody = await captureWebhook(async () => { - alice = await signup({ username: 'alice' }); - }).catch(e => e.message); - - expect(webhookBody).toBe('timeout'); - expect(alice.id).not.toBeNull(); - }); - - test('ユーザ作成 -> Webhookが無効の場合は送出されない', async () => { - await createSystemWebhook({ - on: ['userCreated'], - isActive: false, - }); - - let alice: any = null; - const webhookBody = await captureWebhook(async () => { - alice = await signup({ username: 'alice' }); - }).catch(e => e.message); - - expect(webhookBody).toBe('timeout'); - expect(alice.id).not.toBeNull(); - }); - }); -}); diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts index 1edc178fc2..e01ea90fe0 100644 --- a/packages/backend/test/e2e/thread-mute.ts +++ b/packages/backend/test/e2e/thread-mute.ts @@ -1,58 +1,103 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { api, connectStream, post, signup } from '../utils.js'; +import { signup, api, post, connectStream, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; describe('Note thread mute', () => { - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - let carol: misskey.entities.SignupResponse; + let app: INestApplicationContext; + + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + 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' }); - await api('notes/thread-muting/create', { noteId: bobNote.id }, alice); + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); - const res = await api('notes/mentions', {}, alice); + const res = await api('/notes/mentions', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolReply.id), false); - assert.strictEqual(res.body.some(note => note.id === carolReplyWithoutMention.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false); }); + test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => { + // 状態リセット + await api('/i/read-all-unread-notes', {}, alice); + + const bobNote = await post(bob, { text: '@alice @carol root note' }); + + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + + const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); + + const res = await api('/i', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.hasUnreadMentions, false); + }); + + test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => { + // 状態リセット + await api('/i/read-all-unread-notes', {}, alice); + + const bobNote = await post(bob, { text: '@alice @carol root note' }); + + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + + let fired = false; + + const ws = await connectStream(alice, 'main', async ({ type, body }) => { + if (type === 'unreadMention') { + if (body === bobNote.id) return; + fired = true; + } + }); + + const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 5000); + })); + 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' }); - await api('notes/thread-muting/create', { noteId: bobNote.id }, alice); + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); - const res = await api('i/notifications', {}, alice); + const res = await api('/i/notifications', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(notification => 'note' in notification && notification.note.id === carolReply.id), false); - assert.strictEqual(res.body.some(notification => 'note' in notification && notification.note.id === carolReplyWithoutMention.id), false); + assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false); + assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false); // NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい }); diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts deleted file mode 100644 index e53c3d8f34..0000000000 --- a/packages/backend/test/e2e/timelines.ts +++ /dev/null @@ -1,1569 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// How to run: -// pnpm jest -- e2e/timelines.ts - -import * as assert from 'assert'; -import { setTimeout } from 'node:timers/promises'; -import { Redis } from 'ioredis'; -import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js'; -import { loadConfig } from '@/config.js'; - -function genHost() { - return randomString() + '.example.com'; -} - -function waitForPushToTl() { - return setTimeout(500); -} - -let redisForTimelines: Redis; - -describe('Timelines', () => { - beforeAll(() => { - redisForTimelines = new Redis(loadConfig().redisForTimelines); - }); - - describe('Home TL', () => { - test.concurrent('自分の visibility: followers なノートが含まれる', async () => { - const [alice] = await Promise.all([signup()]); - - const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); - }); - - test.concurrent('フォローしているユーザーのノートが含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi' }); - const carolNote = await post(carol, { text: 'hi' }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); - const carolNote = await post(carol, { text: 'hi' }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: carol.id }, bob); - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: carol.id }, alice); - await api('following/create', { userId: carol.id }, bob); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); - assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi'); - }); - - test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: alice.id }, bob); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - }); - - test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: carol.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); - }); - - test.concurrent('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }); - - test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('自分の他人への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const bobNote = await post(bob, { text: 'hi' }); - const aliceNote = await post(alice, { text: 'hi', replyId: bobNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - }); - - test.concurrent('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { renoteId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { renoteId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { - withRenotes: false, - }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { - withRenotes: false, - }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、フォローしているユーザーによるリノートが含まれない', async () => { - const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id }); - const bobNote = await post(bob, { renoteId: daveNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - assert.strictEqual(res.body.some(note => note.id === daveNote.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、フォローしているユーザーによるリノートが含まれない', async () => { - const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id }); - const bobNote = await post(bob, { renoteId: daveNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - assert.strictEqual(res.body.some(note => note.id === daveNote.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - - await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); - await api('following/create', { userId: bob.id }, alice); - - const bobNote = await post(bob, { text: 'hi' }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - - await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); - await api('following/create', { userId: bob.id }, alice); - - const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const [bobFile, carolFile] = await Promise.all([ - uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), - uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), - ]); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { fileIds: [bobFile.id] }); - const carolNote1 = await post(carol, { text: 'hi' }); - const carolNote2 = await post(carol, { fileIds: [carolFile.id] }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100, withFiles: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false); - }, 1000 * 30); - - test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('自分の visibility: specified なノートが含まれる', async () => { - const [alice] = await Promise.all([signup()]); - - const aliceNote = await post(alice, { text: 'hi', visibility: 'specified' }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); - }); - - test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); - }); - - test.concurrent('フォローしていないユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定していない visibility: specified なノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('フォローしていないユーザーからの visibility: specified なノートに返信したときの自身のノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); - const aliceNote = await post(alice, { text: 'ok', visibility: 'specified', visibleUserIds: [bob.id], replyId: bobNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'ok'); - }); - - /* TODO - test.concurrent('自身の visibility: specified なノートへのフォローしていないユーザーからの返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const aliceNote = await post(alice, { text: 'hi', visibility: 'specified', visibleUserIds: [bob.id] }); - const bobNote = await post(bob, { text: 'ok', visibility: 'specified', visibleUserIds: [alice.id], replyId: aliceNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id).text, 'ok'); - }); - */ - - // ↑の挙動が理想だけど実装が面倒かも - test.concurrent('自身の visibility: specified なノートへのフォローしていないユーザーからの返信が含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const aliceNote = await post(alice, { text: 'hi', visibility: 'specified', visibleUserIds: [bob.id] }); - const bobNote = await post(bob, { text: 'ok', visibility: 'specified', visibleUserIds: [alice.id], replyId: aliceNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('FTT: ローカルユーザーの HTL にはプッシュされる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { - userId: alice.id, - }, bob); - - const aliceNote = await post(alice, { text: 'I\'m Alice.' }); - const bobNote = await post(bob, { text: 'I\'m Bob.' }); - const carolNote = await post(carol, { text: 'I\'m Carol.' }); - - await waitForPushToTl(); - - // NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる - assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 1); - - const bobHTL = await redisForTimelines.lrange(`list:homeTimeline:${bob.id}`, 0, -1); - assert.strictEqual(bobHTL.includes(aliceNote.id), true); - assert.strictEqual(bobHTL.includes(bobNote.id), true); - assert.strictEqual(bobHTL.includes(carolNote.id), false); - }); - - test.concurrent('FTT: リモートユーザーの HTL にはプッシュされない', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - - await api('following/create', { - userId: alice.id, - }, bob); - - await post(alice, { text: 'I\'m Alice.' }); - await post(bob, { text: 'I\'m Bob.' }); - - await waitForPushToTl(); - - // NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる - assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 0); - }); - }); - - describe('Local TL', () => { - test.concurrent('visibility: home なノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); - const bobNote = await post(bob, { text: 'hi' }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('他人の他人への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); - }); - - test.concurrent('他人のその人自身への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }); - - test.concurrent('チャンネル投稿が含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); - const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('リモートユーザーのノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - - const bobNote = await post(bob, { text: 'hi' }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - // 含まれても良いと思うけど実装が面倒なので含まれない - test.concurrent('フォローしているユーザーの visibility: home なノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); - const bobNote = await post(bob, { text: 'hi' }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('ミュートしているユーザーのノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi' }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); - - test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、リノートが含まれない', async () => { - const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); - - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id }); - const bobNote = await post(bob, { renoteId: daveNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - assert.strictEqual(res.body.some(note => note.id === daveNote.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、リノートが含まれない', async () => { - const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); - - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id }); - const bobNote = await post(bob, { renoteId: daveNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - assert.strictEqual(res.body.some(note => note.id === daveNote.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100, withReplies: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { fileIds: [file.id] }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100, withFiles: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }, 1000 * 10); - }); - - describe('Social TL', () => { - test.concurrent('ローカルユーザーのノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const bobNote = await post(bob, { text: 'hi' }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('ローカルユーザーの visibility: home なノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: carol.id }, bob); - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); - }); - - test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: carol.id }, alice); - await api('following/create', { userId: carol.id }, bob); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id)?.text, 'hi'); - }); - - test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: alice.id }, bob); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - }); - - test.concurrent('他人の他人への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); - }); - - test.concurrent('リモートユーザーのノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - - const bobNote = await post(bob, { text: 'hi' }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - - await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); - await api('following/create', { userId: bob.id }, alice); - - const bobNote = await post(bob, { text: 'hi' }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - - await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); - await api('following/create', { userId: bob.id }, alice); - - const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100, withReplies: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { fileIds: [file.id] }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100, withFiles: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }, 1000 * 10); - }); - - describe('User List TL', () => { - test.concurrent('リスインしているフォローしていないユーザーのノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi' }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }); - - test.concurrent('withReplies: false でリスインしているフォローしていないユーザーからの自分への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); - }); - - test.concurrent('リスインしている自分の visibility: followers なノートが含まれる', async () => { - const [alice] = await Promise.all([signup(), signup()]); - - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: alice.id }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); - }); - - test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { fileIds: [file.id] }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id, withFiles: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }, 1000 * 10); - - test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); - }); - - test.concurrent('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await api('users/lists/push', { listId: list.id, userId: carol.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - }); - - describe('User TL', () => { - test.concurrent('ノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const bobNote = await post(bob, { text: 'hi' }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); - }); - - test.concurrent('自身の visibility: followers なノートが含まれる', async () => { - const [alice] = await Promise.all([signup()]); - - const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: alice.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); - }); - - test.concurrent('チャンネル投稿が含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); - const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - const carolNote = await post(carol, { text: 'hi' }); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false); - }); - - test.concurrent('[withReplies: true] 他人への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - const carolNote = await post(carol, { text: 'hi' }); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }); - - test.concurrent('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - const carolNote = await post(carol, { text: 'hi' }); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified' }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false); - }); - - test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { fileIds: [file.id] }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id, withFiles: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }, 1000 * 10); - - test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); - const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('[withChannelNotes: true] 他人が取得した場合センシティブチャンネル投稿が含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const channel = await api('channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body); - const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('[withChannelNotes: true] 自分が取得した場合センシティブチャンネル投稿が含まれる', async () => { - const [bob] = await Promise.all([signup()]); - - const channel = await api('channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body); - const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, bob); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、リノートが含まれない', async () => { - const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); - - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id }); - const bobNote = await post(bob, { renoteId: daveNote.id }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id, limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、リノートが含まれない', async () => { - const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id }); - const bobNote = await post(bob, { renoteId: daveNote.id }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id, limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await api('mute/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); - const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id }); - const bobNote4 = await post(bob, { renoteId: bobNote2.id }); - const bobNote5 = await post(bob, { renoteId: bobNote3.id }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote4.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote5.id), true); - }); - - test.concurrent('自身の visibility: specified なノートが含まれる', async () => { - const [alice] = await Promise.all([signup()]); - - const aliceNote = await post(alice, { text: 'hi', visibility: 'specified' }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: alice.id, withReplies: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - }); - - test.concurrent('visibleUserIds に指定されてない visibility: specified なノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - const bobNote = await post(bob, { text: 'hi', visibility: 'specified' }); - - await waitForPushToTl(); - - const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - /** @see https://github.com/misskey-dev/misskey/issues/14000 */ - test.concurrent('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId による絞り込みが正しく動作する', async () => { - const alice = await signup(); - const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' }); - const note1 = await post(alice, { text: '1' }); - const note2 = await post(alice, { text: '2' }); - await redisForTimelines.del('list:userTimeline:' + alice.id); - const note3 = await post(alice, { text: '3' }); - - const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id }); - assert.deepStrictEqual(res.body, [note1, note2, note3]); - }); - - test.concurrent('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId と untilId による絞り込みが正しく動作する', async () => { - const alice = await signup(); - const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' }); - const note1 = await post(alice, { text: '1' }); - const note2 = await post(alice, { text: '2' }); - await redisForTimelines.del('list:userTimeline:' + alice.id); - const note3 = await post(alice, { text: '3' }); - const noteUntil = await post(alice, { text: 'Note where id will be `untilId`.' }); - await post(alice, { text: '4' }); - - const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id, untilId: noteUntil.id }); - assert.deepStrictEqual(res.body, [note3, note2, note1]); - }); - }); - - // TODO: リノートミュート済みユーザーのテスト - // TODO: ページネーションのテスト -}); diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts index cc07c5ae71..3681456c7e 100644 --- a/packages/backend/test/e2e/user-notes.ts +++ b/packages/backend/test/e2e/user-notes.ts @@ -1,24 +1,23 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { api, post, signup, uploadUrl } from '../utils.js'; +import { signup, api, post, uploadUrl, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; describe('users/notes', () => { - let alice: misskey.entities.SignupResponse; - let jpgNote: misskey.entities.Note; - let pngNote: misskey.entities.Note; - let jpgPngNote: misskey.entities.Note; + let app: INestApplicationContext; + + let alice: misskey.entities.MeSignup; + let jpgNote: any; + let pngNote: any; + let jpgPngNote: any; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); - const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg'); - const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.png'); + const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); + const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png'); jpgNote = await post(alice, { fileIds: [jpg.id], }); @@ -30,10 +29,27 @@ describe('users/notes', () => { }); }, 1000 * 60 * 2); - test('withFiles', async () => { - const res = await api('users/notes', { + afterAll(async() => { + await app.close(); + }); + + test('ファイルタイプ指定 (jpg)', async () => { + const res = await api('/users/notes', { userId: alice.id, - withFiles: true, + fileType: ['image/jpeg'], + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 2); + assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true); + }); + + test('ファイルタイプ指定 (jpg or png)', async () => { + const res = await api('/users/notes', { + userId: alice.id, + fileType: ['image/jpeg', 'image/png'], }, alice); assert.strictEqual(res.status, 200); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index a342bba64c..64efaa57cc 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -1,21 +1,28 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { inspect } from 'node:util'; -import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js'; -import type * as misskey from 'misskey-js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { + signup, + post, + page, + role, + startServer, + api, + successfulApiCall, + failedApiCall, + uploadFile, +} from '../utils.js'; +import type * as misskey from 'misskey-js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('ユーザー', () => { // エンティティとしてのユーザーを主眼においたテストを記述する // (Userを返すエンドポイントとUserエンティティを書き換えるエンドポイントをテストする) - const stripUndefined = (orig: T): Partial => { + const stripUndefined = (orig: T): Partial => { return Object.entries({ ...orig }) .filter(([, value]) => value !== undefined) .reduce((obj: Partial, [key, value]) => { @@ -24,12 +31,31 @@ describe('ユーザー', () => { }, {}); }; - const show = async (id: string, me = root): Promise => { - return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me }); + // BUG misskey-jsとjson-schemaと実際に返ってくるデータが全部違う + type UserLite = misskey.entities.UserLite & { + badgeRoles: any[], + }; + + type UserDetailedNotMe = UserLite & + misskey.entities.UserDetailed & { + roles: any[], + }; + + type MeDetailed = UserDetailedNotMe & + misskey.entities.MeDetailed & { + achievements: object[], + loggedInDays: number, + policies: object, + }; + + type User = MeDetailed & { token: string }; + + const show = async (id: string, me = root): Promise => { + return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me }) as any; }; // UserLiteのキーが過不足なく入っている? - const userLite = (user: misskey.entities.UserLite): Partial => { + const userLite = (user: User): Partial => { return stripUndefined({ id: user.id, name: user.name, @@ -37,7 +63,6 @@ describe('ユーザー', () => { host: user.host, avatarUrl: user.avatarUrl, avatarBlurhash: user.avatarBlurhash, - avatarDecorations: user.avatarDecorations, isBot: user.isBot, isCat: user.isCat, instance: user.instance, @@ -52,7 +77,7 @@ describe('ユーザー', () => { }; // UserDetailedNotMeのキーが過不足なく入っている? - const userDetailedNotMe = (user: misskey.entities.SignupResponse): Partial => { + const userDetailedNotMe = (user: User): Partial => { return stripUndefined({ ...userLite(user), url: user.url, @@ -72,7 +97,6 @@ describe('ユーザー', () => { birthday: user.birthday, lang: user.lang, fields: user.fields, - verifiedLinks: user.verifiedLinks, followersCount: user.followersCount, followingCount: user.followingCount, notesCount: user.notesCount, @@ -81,17 +105,17 @@ describe('ユーザー', () => { pinnedPageId: user.pinnedPageId, pinnedPage: user.pinnedPage, publicReactions: user.publicReactions, - followingVisibility: user.followingVisibility, - followersVisibility: user.followersVisibility, - chatScope: user.chatScope, - canChat: user.canChat, + ffVisibility: user.ffVisibility, + twoFactorEnabled: user.twoFactorEnabled, + usePasswordLessLogin: user.usePasswordLessLogin, + securityKeys: user.securityKeys, roles: user.roles, memo: user.memo, }); }; // Relations関連のキーが過不足なく入っている? - const userDetailedNotMeWithRelations = (user: misskey.entities.SignupResponse): Partial => { + const userDetailedNotMeWithRelations = (user: User): Partial => { return stripUndefined({ ...userDetailedNotMe(user), isFollowing: user.isFollowing ?? false, @@ -102,19 +126,15 @@ describe('ユーザー', () => { isBlocked: user.isBlocked ?? false, isMuted: user.isMuted ?? false, isRenoteMuted: user.isRenoteMuted ?? false, - notify: user.notify ?? 'none', - withReplies: user.withReplies ?? false, - followedMessage: user.isFollowing ? (user.followedMessage ?? null) : undefined, }); }; // MeDetailedのキーが過不足なく入っている? - const meDetailed = (user: misskey.entities.SignupResponse, security = false): Partial => { + const meDetailed = (user: User, security = false): Partial => { return stripUndefined({ ...userDetailedNotMe(user), avatarId: user.avatarId, bannerId: user.bannerId, - followedMessage: user.followedMessage, isModerator: user.isModerator, isAdmin: user.isAdmin, injectFeaturedNote: user.injectFeaturedNote, @@ -127,31 +147,21 @@ describe('ユーザー', () => { preventAiLearning: user.preventAiLearning, isExplorable: user.isExplorable, isDeleted: user.isDeleted, - twoFactorBackupCodesStock: user.twoFactorBackupCodesStock, hideOnlineStatus: user.hideOnlineStatus, hasUnreadSpecifiedNotes: user.hasUnreadSpecifiedNotes, hasUnreadMentions: user.hasUnreadMentions, hasUnreadAnnouncement: user.hasUnreadAnnouncement, hasUnreadAntenna: user.hasUnreadAntenna, hasUnreadChannel: user.hasUnreadChannel, - hasUnreadChatMessages: user.hasUnreadChatMessages, hasUnreadNotification: user.hasUnreadNotification, - unreadNotificationsCount: user.unreadNotificationsCount, hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest, - unreadAnnouncements: user.unreadAnnouncements, mutedWords: user.mutedWords, - hardMutedWords: user.hardMutedWords, mutedInstances: user.mutedInstances, - // @ts-expect-error 後方互換性 mutingNotificationTypes: user.mutingNotificationTypes, - notificationRecieveConfig: user.notificationRecieveConfig, emailNotificationTypes: user.emailNotificationTypes, achievements: user.achievements, loggedInDays: user.loggedInDays, policies: user.policies, - twoFactorEnabled: user.twoFactorEnabled, - usePasswordLessLogin: user.usePasswordLessLogin, - securityKeys: user.securityKeys, ...(security ? { email: user.email, emailVerified: user.emailVerified, @@ -160,53 +170,67 @@ describe('ユーザー', () => { }); }; - let root: misskey.entities.SignupResponse; - let alice: misskey.entities.SignupResponse; + let app: INestApplicationContext; + + let root: User; + let alice: User; let aliceNote: misskey.entities.Note; + let alicePage: misskey.entities.Page; + let aliceList: misskey.entities.UserList; - let bob: misskey.entities.SignupResponse; + let bob: User; + let bobNote: misskey.entities.Note; - // NOTE: これがないと落ちる(bob の updatedAt が null になってしまうため?) - let bobNote: misskey.entities.Note; // eslint-disable-line @typescript-eslint/no-unused-vars + let carol: User; + let dave: User; + let ellen: User; + let frank: User; - let carol: misskey.entities.SignupResponse; + let usersReplying: User[]; - let usersReplying: misskey.entities.SignupResponse[]; + let userNoNote: User; + let userNotExplorable: User; + let userLocking: User; + let userAdmin: User; + let roleAdmin: any; + let userModerator: User; + let roleModerator: any; + let userRolePublic: User; + let rolePublic: any; + let userRoleBadge: User; + let roleBadge: any; + let userSilenced: User; + let roleSilenced: any; + let userSuspended: User; + let userDeletedBySelf: User; + let userDeletedByAdmin: User; + let userFollowingAlice: User; + let userFollowedByAlice: User; + let userBlockingAlice: User; + let userBlockedByAlice: User; + let userMutingAlice: User; + let userMutedByAlice: User; + let userRnMutingAlice: User; + let userRnMutedByAlice: User; + let userFollowRequesting: User; + let userFollowRequested: User; - let userNoNote: misskey.entities.SignupResponse; - let userNotExplorable: misskey.entities.SignupResponse; - let userLocking: misskey.entities.SignupResponse; - let userAdmin: misskey.entities.SignupResponse; - let roleAdmin: misskey.entities.Role; - let userModerator: misskey.entities.SignupResponse; - let roleModerator: misskey.entities.Role; - let userRolePublic: misskey.entities.SignupResponse; - let rolePublic: misskey.entities.Role; - let userRoleBadge: misskey.entities.SignupResponse; - let roleBadge: misskey.entities.Role; - let userSilenced: misskey.entities.SignupResponse; - let roleSilenced: misskey.entities.Role; - let userSuspended: misskey.entities.SignupResponse; - let userDeletedBySelf: misskey.entities.SignupResponse; - let userDeletedByAdmin: misskey.entities.SignupResponse; - let userFollowingAlice: misskey.entities.SignupResponse; - let userFollowedByAlice: misskey.entities.SignupResponse; - let userBlockingAlice: misskey.entities.SignupResponse; - let userBlockedByAlice: misskey.entities.SignupResponse; - let userMutingAlice: misskey.entities.SignupResponse; - let userMutedByAlice: misskey.entities.SignupResponse; - let userRnMutingAlice: misskey.entities.SignupResponse; - let userRnMutedByAlice: misskey.entities.SignupResponse; - let userFollowRequesting: misskey.entities.SignupResponse; - let userFollowRequested: misskey.entities.SignupResponse; + beforeAll(async () => { + app = await startServer(); + }, 1000 * 60 * 2); beforeAll(async () => { root = await signup({ username: 'root' }); alice = await signup({ username: 'alice' }); - aliceNote = await post(alice, { text: 'test' }); + aliceNote = await post(alice, { text: 'test' }) as any; + alicePage = await page(alice); + aliceList = (await api('users/list/create', { name: 'aliceList' }, alice)).body; bob = await signup({ username: 'bob' }); - bobNote = await post(bob, { text: 'test' }); + bobNote = await post(bob, { text: 'test' }) as any; carol = await signup({ username: 'carol' }); + dave = await signup({ username: 'dave' }); + ellen = await signup({ username: 'ellen' }); + frank = await signup({ username: 'frank' }); // @alice -> @replyingへのリプライ。Promise.allで一気に作るとtimeoutしてしまうのでreduceで一つ一つawaitする usersReplying = await [...Array(10)].map((_, i) => i).reduce(async (acc, i) => { @@ -217,7 +241,7 @@ describe('ユーザー', () => { } return (await acc).concat(u); - }, Promise.resolve([] as misskey.entities.SignupResponse[])); + }, Promise.resolve([] as User[])); userNoNote = await signup({ username: 'userNoNote' }); userNotExplorable = await signup({ username: 'userNotExplorable' }); @@ -236,7 +260,7 @@ describe('ユーザー', () => { rolePublic = await role(root, { isPublic: true, name: 'Public Role' }); await api('admin/roles/assign', { userId: userRolePublic.id, roleId: rolePublic.id }, root); userRoleBadge = await signup({ username: 'userRoleBadge' }); - roleBadge = await role(root, { asBadge: true, name: 'Badge Role', isPublic: true }); + roleBadge = await role(root, { asBadge: true, name: 'Badge Role' }); await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root); userSilenced = await signup({ username: 'userSilenced' }); await post(userSilenced, { text: 'test' }); @@ -282,10 +306,14 @@ describe('ユーザー', () => { await api('following/create', { userId: userFollowRequested.id }, userFollowRequesting); }, 1000 * 60 * 10); + afterAll(async () => { + await app.close(); + }); + beforeEach(async () => { alice = { ...alice, - ...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }), + ...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }) as any, }; aliceNote = await successfulApiCall({ endpoint: 'notes/show', parameters: { noteId: aliceNote.id }, user: alice }); }); @@ -298,7 +326,7 @@ describe('ユーザー', () => { endpoint: 'signup', parameters: { username: 'zoe', password: 'password' }, user: undefined, - }) as unknown as misskey.entities.SignupResponse; // BUG MeDetailedに足りないキーがある + }) as unknown as User; // BUG MeDetailedに足りないキーがある // signupの時はtokenが含まれる特別なMeDetailedが返ってくる assert.match(response.token, /[a-zA-Z0-9]{16}/); @@ -308,9 +336,8 @@ describe('ユーザー', () => { assert.strictEqual(response.name, null); assert.strictEqual(response.username, 'zoe'); assert.strictEqual(response.host, null); - response.avatarUrl && assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); + assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); assert.strictEqual(response.avatarBlurhash, null); - assert.deepStrictEqual(response.avatarDecorations, []); assert.strictEqual(response.isBot, false); assert.strictEqual(response.isCat, false); assert.strictEqual(response.instance, undefined); @@ -335,7 +362,6 @@ describe('ユーザー', () => { assert.strictEqual(response.birthday, null); assert.strictEqual(response.lang, null); assert.deepStrictEqual(response.fields, []); - assert.deepStrictEqual(response.verifiedLinks, []); assert.strictEqual(response.followersCount, 0); assert.strictEqual(response.followingCount, 0); assert.strictEqual(response.notesCount, 0); @@ -344,17 +370,16 @@ describe('ユーザー', () => { assert.strictEqual(response.pinnedPageId, null); assert.strictEqual(response.pinnedPage, null); assert.strictEqual(response.publicReactions, true); - assert.strictEqual(response.followingVisibility, 'public'); - assert.strictEqual(response.followersVisibility, 'public'); - assert.strictEqual(response.chatScope, 'mutual'); - assert.strictEqual(response.canChat, true); + assert.strictEqual(response.ffVisibility, 'public'); + assert.strictEqual(response.twoFactorEnabled, false); + assert.strictEqual(response.usePasswordLessLogin, false); + assert.strictEqual(response.securityKeys, false); assert.deepStrictEqual(response.roles, []); assert.strictEqual(response.memo, null); // MeDetailedOnly assert.strictEqual(response.avatarId, null); assert.strictEqual(response.bannerId, null); - assert.strictEqual(response.followedMessage, null); assert.strictEqual(response.isModerator, false); assert.strictEqual(response.isAdmin, false); assert.strictEqual(response.injectFeaturedNote, true); @@ -367,30 +392,21 @@ describe('ユーザー', () => { assert.strictEqual(response.preventAiLearning, true); assert.strictEqual(response.isExplorable, true); assert.strictEqual(response.isDeleted, false); - assert.strictEqual(response.twoFactorBackupCodesStock, 'none'); assert.strictEqual(response.hideOnlineStatus, false); assert.strictEqual(response.hasUnreadSpecifiedNotes, false); assert.strictEqual(response.hasUnreadMentions, false); assert.strictEqual(response.hasUnreadAnnouncement, false); assert.strictEqual(response.hasUnreadAntenna, false); assert.strictEqual(response.hasUnreadChannel, false); - assert.strictEqual(response.hasUnreadChatMessages, false); assert.strictEqual(response.hasUnreadNotification, false); - assert.strictEqual(response.unreadNotificationsCount, 0); assert.strictEqual(response.hasPendingReceivedFollowRequest, false); - assert.deepStrictEqual(response.unreadAnnouncements, []); assert.deepStrictEqual(response.mutedWords, []); assert.deepStrictEqual(response.mutedInstances, []); - // @ts-expect-error 後方互換のため assert.deepStrictEqual(response.mutingNotificationTypes, []); - assert.deepStrictEqual(response.notificationRecieveConfig, {}); assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']); assert.deepStrictEqual(response.achievements, []); assert.deepStrictEqual(response.loggedInDays, 0); assert.deepStrictEqual(response.policies, DEFAULT_POLICIES); - assert.strictEqual(response.twoFactorEnabled, false); - assert.strictEqual(response.usePasswordLessLogin, false); - assert.strictEqual(response.securityKeys, false); assert.notStrictEqual(response.email, undefined); assert.strictEqual(response.emailVerified, false); assert.deepStrictEqual(response.securityKeysList, []); @@ -414,68 +430,63 @@ describe('ユーザー', () => { //#region 自分の情報の更新(i/update) test.each([ - { parameters: () => ({ name: null }) }, - { parameters: () => ({ name: 'x'.repeat(50) }) }, - { parameters: () => ({ name: 'x' }) }, - { parameters: () => ({ name: 'My name' }) }, - { parameters: () => ({ description: null }) }, - { parameters: () => ({ description: 'x'.repeat(1500) }) }, - { parameters: () => ({ description: 'x' }) }, - { parameters: () => ({ description: 'My description' }) }, - { parameters: () => ({ followedMessage: null }) }, - { parameters: () => ({ followedMessage: 'Thank you' }) }, - { parameters: () => ({ location: null }) }, - { parameters: () => ({ location: 'x'.repeat(50) }) }, - { parameters: () => ({ location: 'x' }) }, - { parameters: () => ({ location: 'My location' }) }, - { parameters: () => ({ birthday: '0000-00-00' }) }, - { parameters: () => ({ birthday: '9999-99-99' }) }, - { parameters: () => ({ lang: 'en-US' as const }) }, - { parameters: () => ({ fields: [] }) }, - { parameters: () => ({ fields: [{ name: 'x', value: 'x' }] }) }, - { parameters: () => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない - { parameters: () => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) }, - { parameters: () => ({ isLocked: true }) }, - { parameters: () => ({ isLocked: false }) }, - { parameters: () => ({ isExplorable: false }) }, - { parameters: () => ({ isExplorable: true }) }, - { parameters: () => ({ hideOnlineStatus: true }) }, - { parameters: () => ({ hideOnlineStatus: false }) }, - { parameters: () => ({ publicReactions: false }) }, - { parameters: () => ({ publicReactions: true }) }, - { parameters: () => ({ autoAcceptFollowed: true }) }, - { parameters: () => ({ autoAcceptFollowed: false }) }, - { parameters: () => ({ noCrawle: true }) }, - { parameters: () => ({ noCrawle: false }) }, - { parameters: () => ({ preventAiLearning: false }) }, - { parameters: () => ({ preventAiLearning: true }) }, - { parameters: () => ({ isBot: true }) }, - { parameters: () => ({ isBot: false }) }, - { parameters: () => ({ isCat: true }) }, - { parameters: () => ({ isCat: false }) }, - { parameters: () => ({ injectFeaturedNote: true }) }, - { parameters: () => ({ injectFeaturedNote: false }) }, - { parameters: () => ({ receiveAnnouncementEmail: true }) }, - { parameters: () => ({ receiveAnnouncementEmail: false }) }, - { parameters: () => ({ alwaysMarkNsfw: true }) }, - { parameters: () => ({ alwaysMarkNsfw: false }) }, - { parameters: () => ({ autoSensitive: true }) }, - { parameters: () => ({ autoSensitive: false }) }, - { parameters: () => ({ followingVisibility: 'private' as const }) }, - { parameters: () => ({ followingVisibility: 'followers' as const }) }, - { parameters: () => ({ followingVisibility: 'public' as const }) }, - { parameters: () => ({ followersVisibility: 'private' as const }) }, - { parameters: () => ({ followersVisibility: 'followers' as const }) }, - { parameters: () => ({ followersVisibility: 'public' as const }) }, - { parameters: () => ({ mutedWords: Array(19).fill(['xxxxx']) }) }, - { parameters: () => ({ mutedWords: [['x'.repeat(194)]] }) }, - { parameters: () => ({ mutedWords: [] }) }, - { parameters: () => ({ mutedInstances: ['xxxx.xxxxx'] }) }, - { parameters: () => ({ mutedInstances: [] }) }, - { parameters: () => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) }, - { parameters: () => ({ notificationRecieveConfig: {} }) }, - { parameters: () => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) }, - { parameters: () => ({ emailNotificationTypes: [] }) }, + { parameters: (): object => ({ name: null }) }, + { parameters: (): object => ({ name: 'x'.repeat(50) }) }, + { parameters: (): object => ({ name: 'x' }) }, + { parameters: (): object => ({ name: 'My name' }) }, + { parameters: (): object => ({ description: null }) }, + { parameters: (): object => ({ description: 'x'.repeat(1500) }) }, + { parameters: (): object => ({ description: 'x' }) }, + { parameters: (): object => ({ description: 'My description' }) }, + { parameters: (): object => ({ location: null }) }, + { parameters: (): object => ({ location: 'x'.repeat(50) }) }, + { parameters: (): object => ({ location: 'x' }) }, + { parameters: (): object => ({ location: 'My location' }) }, + { parameters: (): object => ({ birthday: '0000-00-00' }) }, + { parameters: (): object => ({ birthday: '9999-99-99' }) }, + { parameters: (): object => ({ lang: 'en-US' }) }, + { parameters: (): object => ({ fields: [] }) }, + { parameters: (): object => ({ fields: [{ name: 'x', value: 'x' }] }) }, + { parameters: (): object => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない + { parameters: (): object => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) }, + { parameters: (): object => ({ isLocked: true }) }, + { parameters: (): object => ({ isLocked: false }) }, + { parameters: (): object => ({ isExplorable: false }) }, + { parameters: (): object => ({ isExplorable: true }) }, + { parameters: (): object => ({ hideOnlineStatus: true }) }, + { parameters: (): object => ({ hideOnlineStatus: false }) }, + { parameters: (): object => ({ publicReactions: false }) }, + { parameters: (): object => ({ publicReactions: true }) }, + { parameters: (): object => ({ autoAcceptFollowed: true }) }, + { parameters: (): object => ({ autoAcceptFollowed: false }) }, + { parameters: (): object => ({ noCrawle: true }) }, + { parameters: (): object => ({ noCrawle: false }) }, + { parameters: (): object => ({ preventAiLearning: false }) }, + { parameters: (): object => ({ preventAiLearning: true }) }, + { parameters: (): object => ({ isBot: true }) }, + { parameters: (): object => ({ isBot: false }) }, + { parameters: (): object => ({ isCat: true }) }, + { parameters: (): object => ({ isCat: false }) }, + { parameters: (): object => ({ injectFeaturedNote: true }) }, + { parameters: (): object => ({ injectFeaturedNote: false }) }, + { parameters: (): object => ({ receiveAnnouncementEmail: true }) }, + { parameters: (): object => ({ receiveAnnouncementEmail: false }) }, + { parameters: (): object => ({ alwaysMarkNsfw: true }) }, + { parameters: (): object => ({ alwaysMarkNsfw: false }) }, + { parameters: (): object => ({ autoSensitive: true }) }, + { parameters: (): object => ({ autoSensitive: false }) }, + { parameters: (): object => ({ ffVisibility: 'private' }) }, + { parameters: (): object => ({ ffVisibility: 'followers' }) }, + { parameters: (): object => ({ ffVisibility: 'public' }) }, + { parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) }, + { parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) }, + { parameters: (): object => ({ mutedWords: [] }) }, + { parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) }, + { parameters: (): object => ({ mutedInstances: [] }) }, + { parameters: (): object => ({ mutingNotificationTypes: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) }, + { parameters: (): object => ({ mutingNotificationTypes: [] }) }, + { parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) }, + { parameters: (): object => ({ emailNotificationTypes: [] }) }, ] as const)('を書き換えることができる($#)', async ({ parameters }) => { const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters(), user: alice }); const expected = { ...meDetailed(alice, true), ...parameters() }; @@ -484,13 +495,13 @@ describe('ユーザー', () => { test('を書き換えることができる(Avatar)', async () => { const aliceFile = (await uploadFile(alice)).body; - const parameters = { avatarId: aliceFile!.id }; + const parameters = { avatarId: aliceFile.id }; const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice }); assert.match(response.avatarUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); assert.match(response.avatarBlurhash ?? '.', /[ -~]{54}/); const expected = { ...meDetailed(alice, true), - avatarId: aliceFile!.id, + avatarId: aliceFile.id, avatarBlurhash: response.avatarBlurhash, avatarUrl: response.avatarUrl, }; @@ -509,13 +520,13 @@ describe('ユーザー', () => { test('を書き換えることができる(Banner)', async () => { const aliceFile = (await uploadFile(alice)).body; - const parameters = { bannerId: aliceFile!.id }; + const parameters = { bannerId: aliceFile.id }; const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice }); assert.match(response.bannerUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); assert.match(response.bannerBlurhash ?? '.', /[ -~]{54}/); const expected = { ...meDetailed(alice, true), - bannerId: aliceFile!.id, + bannerId: aliceFile.id, bannerBlurhash: response.bannerBlurhash, bannerUrl: response.bannerUrl, }; @@ -565,13 +576,13 @@ describe('ユーザー', () => { //#region ユーザー(users) test.each([ - { label: 'ID昇順', parameters: { limit: 5 }, selector: (u: misskey.entities.UserLite): string => u.id }, - { label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) }, - { label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) }, - { label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt }, - { label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt }, - { label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) }, - { label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) }, + { label: 'ID昇順', parameters: { limit: 5 }, selector: (u: UserLite): string => u.id }, + { label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, + { label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, + { label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, + { label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, + { label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, + { label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, ] as const)('をリスト形式で取得することができる($label)', async ({ parameters, selector }) => { const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice }); @@ -584,15 +595,15 @@ describe('ユーザー', () => { assert.deepStrictEqual(response, expected); }); test.each([ - { label: '「見つけやすくする」がOFFのユーザーが含まれない', user: () => userNotExplorable, excluded: true }, - { label: 'ミュートユーザーが含まれない', user: () => userMutedByAlice, excluded: true }, - { label: 'ブロックされているユーザーが含まれない', user: () => userBlockedByAlice, excluded: true }, - { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice, excluded: true }, - { label: '承認制ユーザーが含まれる', user: () => userLocking }, - { label: 'サイレンスユーザーが含まれる', user: () => userSilenced }, - { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true }, - { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin }, + { label: '「見つけやすくする」がOFFのユーザーが含まれない', user: (): User => userNotExplorable, excluded: true }, + { label: 'ミュートユーザーが含まれない', user: (): User => userMutedByAlice, excluded: true }, + { label: 'ブロックされているユーザーが含まれない', user: (): User => userBlockedByAlice, excluded: true }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice, excluded: true }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('をリスト形式で取得することができ、結果に$label', async ({ user, excluded }) => { const parameters = { limit: 100 }; const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice }); @@ -606,47 +617,39 @@ describe('ユーザー', () => { //#region ユーザー情報(users/show) test.each([ - { label: 'ID指定で自分自身を', parameters: () => ({ userId: alice.id }), user: () => alice, type: meDetailed }, - { label: 'ID指定で他人を', parameters: () => ({ userId: alice.id }), user: () => bob, type: userDetailedNotMeWithRelations }, - { label: 'ID指定かつ未認証', parameters: () => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe }, - { label: '@指定で自分自身を', parameters: () => ({ username: alice.username }), user: () => alice, type: meDetailed }, - { label: '@指定で他人を', parameters: () => ({ username: alice.username }), user: () => bob, type: userDetailedNotMeWithRelations }, - { label: '@指定かつ未認証', parameters: () => ({ username: alice.username }), user: undefined, type: userDetailedNotMe }, + { label: 'ID指定で自分自身を', parameters: (): object => ({ userId: alice.id }), user: (): User => alice, type: meDetailed }, + { label: 'ID指定で他人を', parameters: (): object => ({ userId: alice.id }), user: (): User => bob, type: userDetailedNotMeWithRelations }, + { label: 'ID指定かつ未認証', parameters: (): object => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe }, + { label: '@指定で自分自身を', parameters: (): object => ({ username: alice.username }), user: (): User => alice, type: meDetailed }, + { label: '@指定で他人を', parameters: (): object => ({ username: alice.username }), user: (): User => bob, type: userDetailedNotMeWithRelations }, + { label: '@指定かつ未認証', parameters: (): object => ({ username: alice.username }), user: undefined, type: userDetailedNotMe }, ] as const)('を取得することができる($label)', async ({ parameters, user, type }) => { const response = await successfulApiCall({ endpoint: 'users/show', parameters: parameters(), user: user?.() }); const expected = type(alice); assert.deepStrictEqual(response, expected); }); test.each([ - { label: 'Administratorになっている', user: () => userAdmin, me: () => userAdmin, selector: (user: misskey.entities.MeDetailed) => user.isAdmin }, - // @ts-expect-error UserDetailedNotMe doesn't include isAdmin - { label: '自分以外から見たときはAdministratorか判定できない', user: () => userAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isAdmin, expected: () => undefined }, - { label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator }, - // @ts-expect-error UserDetailedNotMe doesn't include isModerator - { label: '自分以外から見たときはModeratorか判定できない', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined }, - { label: '自分から見た場合に二要素認証関連のプロパティがセットされている', user: () => alice, me: () => alice, selector: (user: misskey.entities.MeDetailed) => user.twoFactorEnabled, expected: () => false }, - { label: '自分以外から見た場合に二要素認証関連のプロパティがセットされていない', user: () => alice, me: () => bob, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => undefined }, - { label: 'モデレーターから見た場合に二要素認証関連のプロパティがセットされている', user: () => alice, me: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => false }, - { label: 'サイレンスになっている', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced }, - // FIXME: 落ちる - //{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended }, - { label: '削除済みになっている', user: () => userDeletedBySelf, me: () => userDeletedBySelf, selector: (user: misskey.entities.MeDetailed) => user.isDeleted }, - // @ts-expect-error UserDetailedNotMe doesn't include isDeleted - { label: '自分以外から見たときは削除済みか判定できない', user: () => userDeletedBySelf, selector: (user: misskey.entities.UserDetailedNotMe) => user.isDeleted, expected: () => undefined }, - { label: '削除済み(byAdmin)になっている', user: () => userDeletedByAdmin, me: () => userDeletedByAdmin, selector: (user: misskey.entities.MeDetailed) => user.isDeleted }, - // @ts-expect-error UserDetailedNotMe doesn't include isDeleted - { label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: () => userDeletedByAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isDeleted, expected: () => undefined }, - { label: 'フォロー中になっている', user: () => userFollowedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isFollowing }, - { label: 'フォローされている', user: () => userFollowingAlice, selector: (user: misskey.entities.UserDetailed) => user.isFollowed }, - { label: 'ブロック中になっている', user: () => userBlockedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isBlocking }, - { label: 'ブロックされている', user: () => userBlockingAlice, selector: (user: misskey.entities.UserDetailed) => user.isBlocked }, - { label: 'ミュート中になっている', user: () => userMutedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isMuted }, - { label: 'リノートミュート中になっている', user: () => userRnMutedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isRenoteMuted }, - { label: 'フォローリクエスト中になっている', user: () => userFollowRequested, me: () => userFollowRequesting, selector: (user: misskey.entities.UserDetailed) => user.hasPendingFollowRequestFromYou }, - { label: 'フォローリクエストされている', user: () => userFollowRequesting, me: () => userFollowRequested, selector: (user: misskey.entities.UserDetailed) => user.hasPendingFollowRequestToYou }, + { label: 'Administratorになっている', user: (): User => userAdmin, me: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin }, + { label: '自分以外から見たときはAdministratorか判定できない', user: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin, expected: (): undefined => undefined }, + { label: 'Moderatorになっている', user: (): User => userModerator, me: (): User => userModerator, selector: (user: User): unknown => user.isModerator }, + { label: '自分以外から見たときはModeratorか判定できない', user: (): User => userModerator, selector: (user: User): unknown => user.isModerator, expected: (): undefined => undefined }, + { label: 'サイレンスになっている', user: (): User => userSilenced, selector: (user: User): unknown => user.isSilenced }, + //{ label: 'サスペンドになっている', user: (): User => userSuspended, selector: (user: User): unknown => user.isSuspended }, + { label: '削除済みになっている', user: (): User => userDeletedBySelf, me: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted }, + { label: '自分以外から見たときは削除済みか判定できない', user: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined }, + { label: '削除済み(byAdmin)になっている', user: (): User => userDeletedByAdmin, me: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted }, + { label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined }, + { label: 'フォロー中になっている', user: (): User => userFollowedByAlice, selector: (user: User): unknown => user.isFollowing }, + { label: 'フォローされている', user: (): User => userFollowingAlice, selector: (user: User): unknown => user.isFollowed }, + { label: 'ブロック中になっている', user: (): User => userBlockedByAlice, selector: (user: User): unknown => user.isBlocking }, + { label: 'ブロックされている', user: (): User => userBlockingAlice, selector: (user: User): unknown => user.isBlocked }, + { label: 'ミュート中になっている', user: (): User => userMutedByAlice, selector: (user: User): unknown => user.isMuted }, + { label: 'リノートミュート中になっている', user: (): User => userRnMutedByAlice, selector: (user: User): unknown => user.isRenoteMuted }, + { label: 'フォローリクエスト中になっている', user: (): User => userFollowRequested, me: (): User => userFollowRequesting, selector: (user: User): unknown => user.hasPendingFollowRequestFromYou }, + { label: 'フォローリクエストされている', user: (): User => userFollowRequesting, me: (): User => userFollowRequested, selector: (user: User): unknown => user.hasPendingFollowRequestToYou }, ] as const)('を取得することができ、$labelこと', async ({ user, me, selector, expected }) => { const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: user().id }, user: me?.() ?? alice }); - assert.strictEqual(selector(response as any), (expected ?? ((): true => true))()); + assert.strictEqual(selector(response), (expected ?? ((): true => true))()); }); test('を取得することができ、Publicなロールがセットされていること', async () => { const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRolePublic.id }, user: alice }); @@ -669,16 +672,7 @@ describe('ユーザー', () => { iconUrl: roleBadge.iconUrl, displayOrder: roleBadge.displayOrder, }]); - assert.deepStrictEqual(response.roles, [{ - id: roleBadge.id, - name: roleBadge.name, - color: roleBadge.color, - iconUrl: roleBadge.iconUrl, - description: roleBadge.description, - isModerator: roleBadge.isModerator, - isAdministrator: roleBadge.isAdministrator, - displayOrder: roleBadge.displayOrder, - }]); + assert.deepStrictEqual(response.roles, []); // バッヂだからといってrolesが取れるとは限らない }); test('をID指定のリスト形式で取得することができる(空)', async () => { const parameters = { userIds: [] }; @@ -697,18 +691,17 @@ describe('ユーザー', () => { assert.deepStrictEqual(response, expected); }); test.each([ - { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable }, - { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice }, - { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice }, - { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice }, - { label: '承認制ユーザーが含まれる', user: () => userLocking }, - { label: 'サイレンスユーザーが含まれる', user: () => userSilenced }, - { label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: () => userSuspended, me: () => root }, + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: (): User => userSuspended, me: (): User => root }, // BUG サスペンドユーザーを一般ユーザーから見るとrootユーザーが返ってくる - //{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: () => userSuspended, me: () => bob, excluded: true }, - { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin }, - // @ts-expect-error excluded は上でコメントアウトされているので + //{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: (): User => userSuspended, me: (): User => bob, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('をID指定のリスト形式で取得することができ、結果に$label', async ({ user, me, excluded }) => { const parameters = { userIds: [user().id] }; const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: me?.() ?? alice }); @@ -733,15 +726,15 @@ describe('ユーザー', () => { assert.deepStrictEqual(response, expected); }); test.each([ - { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable }, - { label: 'ミュートユーザーが含まれない', user: () => userMutedByAlice, excluded: true }, - { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice }, - { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice }, - { label: '承認制ユーザーが含まれる', user: () => userLocking }, - { label: 'サイレンスユーザーが含まれる', user: () => userSilenced }, - { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true }, - { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin }, + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('を検索することができ、結果に$labelが含まれる', async ({ user, excluded }) => { const parameters = { query: user().username, limit: 1 }; const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice }); @@ -755,30 +748,30 @@ describe('ユーザー', () => { //#region ID指定検索(users/search-by-username-and-host) test.each([ - { label: '自分', parameters: { username: 'alice' }, user: () => [alice] }, - { label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: () => [alice] }, - { label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: () => [userFollowedByAlice] }, - { label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: () => [] }, - { label: 'ローカルの他人1', parameters: { username: 'bob' }, user: () => [bob] }, - { label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: () => [bob] }, - { label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: () => [bob] }, - { label: 'ローカル', parameters: { host: null, limit: 1 }, user: () => [userFollowedByAlice] }, - { label: 'ローカル', parameters: { host: '.', limit: 1 }, user: () => [userFollowedByAlice] }, + { label: '自分', parameters: { username: 'alice' }, user: (): User[] => [alice] }, + { label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: (): User[] => [alice] }, + { label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: (): User[] => [userFollowedByAlice] }, + { label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: (): User[] => [] }, + { label: 'ローカルの他人1', parameters: { username: 'bob' }, user: (): User[] => [bob] }, + { label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: (): User[] => [bob] }, + { label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: (): User[] => [bob] }, + { label: 'ローカル', parameters: { host: null, limit: 1 }, user: (): User[] => [userFollowedByAlice] }, + { label: 'ローカル', parameters: { host: '.', limit: 1 }, user: (): User[] => [userFollowedByAlice] }, ])('をID&ホスト指定で検索できる($label)', async ({ parameters, user }) => { const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice }); const expected = await Promise.all(user().map(u => show(u.id, alice))); assert.deepStrictEqual(response, expected); }); test.each([ - { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable }, - { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice }, - { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice }, - { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice }, - { label: '承認制ユーザーが含まれる', user: () => userLocking }, - { label: 'サイレンスユーザーが含まれる', user: () => userSilenced }, - { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true }, - { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin }, + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('をID&ホスト指定で検索でき、結果に$label', async ({ user, excluded }) => { const parameters = { username: user().username }; const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice }); @@ -800,15 +793,15 @@ describe('ユーザー', () => { assert.deepStrictEqual(response, expected); }); test.each([ - { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable }, - { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice }, - { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice }, - { label: 'ブロックしてきているユーザーが含まれない', user: () => userBlockingAlice, excluded: true }, - { label: '承認制ユーザーが含まれる', user: () => userLocking }, - { label: 'サイレンスユーザーが含まれる', user: () => userSilenced }, - //{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true }, - { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin }, + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれない', user: (): User => userBlockingAlice, excluded: true }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + //{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('がよくリプライをするユーザーのリストを取得でき、結果に$label', async ({ user, excluded }) => { const replyTo = (await successfulApiCall({ endpoint: 'users/notes', parameters: { userId: user().id }, user: undefined }))[0]; await post(alice, { text: `@${user().username} test`, replyId: replyTo.id }); @@ -822,12 +815,12 @@ describe('ユーザー', () => { //#region ハッシュタグ(hashtags/users) test.each([ - { label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) }, - { label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) }, - { label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt }, - { label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt }, - { label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) }, - { label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) }, + { label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, + { label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, + { label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, + { label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, + { label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, + { label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, ] as const)('をハッシュタグ指定で取得することができる($label)', async ({ sort, selector }) => { const hashtag = 'test_hashtag'; await successfulApiCall({ endpoint: 'i/update', parameters: { description: `#${hashtag}` }, user: alice }); @@ -841,15 +834,15 @@ describe('ユーザー', () => { assert.deepStrictEqual(response, expected); }); test.each([ - { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable }, - { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice }, - { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice }, - { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice }, - { label: '承認制ユーザーが含まれる', user: () => userLocking }, - { label: 'サイレンスユーザーが含まれる', user: () => userSilenced }, - { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true }, - { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin }, + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('をハッシュタグ指定で取得することができ、結果に$label', async ({ user, excluded }) => { const hashtag = `user_test${user().username}`; if (user() !== userSuspended) { diff --git a/packages/backend/test/e2e/well-known.ts b/packages/backend/test/e2e/well-known.ts deleted file mode 100644 index bdb298dfe4..0000000000 --- a/packages/backend/test/e2e/well-known.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import { host, origin, relativeFetch, signup } from '../utils.js'; -import type * as misskey from 'misskey-js'; - -describe('.well-known', () => { - let alice: misskey.entities.User; - - beforeAll(async () => { - alice = await signup({ username: 'alice' }); - }, 1000 * 60 * 2); - - test('nodeinfo', async () => { - const res = await relativeFetch('.well-known/nodeinfo'); - assert.ok(res.ok); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); - - const nodeInfo = await res.json(); - assert.deepStrictEqual(nodeInfo, { - links: [{ - rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', - href: `${origin}/nodeinfo/2.1`, - }, { - rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', - href: `${origin}/nodeinfo/2.0`, - }], - }); - }); - - test('webfinger', async () => { - const preflight = await relativeFetch(`.well-known/webfinger?resource=acct:alice@${host}`, { - method: 'options', - headers: { - 'Access-Control-Request-Method': 'GET', - Origin: 'http://example.com', - }, - }); - assert.ok(preflight.ok); - assert.strictEqual(preflight.headers.get('Access-Control-Allow-Headers'), 'Accept'); - - const res = await relativeFetch(`.well-known/webfinger?resource=acct:alice@${host}`); - assert.ok(res.ok); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); - assert.strictEqual(res.headers.get('Access-Control-Expose-Headers'), 'Vary'); - assert.strictEqual(res.headers.get('Vary'), 'Accept'); - - const webfinger = await res.json(); - - assert.deepStrictEqual(webfinger, { - subject: `acct:alice@${host}`, - links: [{ - rel: 'self', - type: 'application/activity+json', - href: `${origin}/users/${alice.id}`, - }, { - rel: 'http://webfinger.net/rel/profile-page', - type: 'text/html', - href: `${origin}/@alice`, - }, { - rel: 'http://ostatus.org/schema/1.0/subscribe', - template: `${origin}/authorize-follow?acct={uri}`, - }], - }); - }); - - test('host-meta', async () => { - const res = await relativeFetch('.well-known/host-meta'); - assert.ok(res.ok); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); - }); - - test('host-meta.json', async () => { - const res = await relativeFetch('.well-known/host-meta.json'); - assert.ok(res.ok); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); - - const hostMeta = await res.json(); - assert.deepStrictEqual(hostMeta, { - links: [{ - rel: 'lrdd', - type: 'application/jrd+json', - template: `${origin}/.well-known/webfinger?resource={uri}`, - }], - }); - }); - - test('oauth-authorization-server', async () => { - const res = await relativeFetch('.well-known/oauth-authorization-server'); - assert.ok(res.ok); - assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); - - const serverInfo = await res.json() as any; - assert.strictEqual(serverInfo.issuer, origin); - assert.strictEqual(serverInfo.authorization_endpoint, `${origin}/oauth/authorize`); - assert.strictEqual(serverInfo.token_endpoint, `${origin}/oauth/token`); - }); -}); diff --git a/packages/backend/test/eslint.config.js b/packages/backend/test/eslint.config.js deleted file mode 100644 index a0f43babad..0000000000 --- a/packages/backend/test/eslint.config.js +++ /dev/null @@ -1,22 +0,0 @@ -import globals from 'globals'; -import tsParser from '@typescript-eslint/parser'; -import sharedConfig from '../../shared/eslint.config.js'; - -export default [ - ...sharedConfig, - { - files: ['**/*.ts', '**/*.tsx'], - languageOptions: { - globals: { - ...globals.node, - ...globals.jest, - }, - parserOptions: { - parser: tsParser, - project: ['./tsconfig.json'], - sourceType: 'module', - tsconfigRootDir: import.meta.dirname, - }, - }, - }, -]; diff --git a/packages/backend/test/global.d.ts b/packages/backend/test/global.d.ts deleted file mode 100644 index 0363073356..0000000000 --- a/packages/backend/test/global.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type FIXME = any; diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/jest.setup.ts deleted file mode 100644 index 7c6dd6a55f..0000000000 --- a/packages/backend/test/jest.setup.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { initTestDb, sendEnvResetRequest } from './utils.js'; - -beforeAll(async () => { - await initTestDb(false); - await sendEnvResetRequest(); -}); diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index 53ff4feb7e..a7bcd859ae 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -1,27 +1,16 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import type { Config } from '@/config.js'; import type { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import type { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import type { ApRequestService } from '@/core/activitypub/ApRequestService.js'; +import { Resolver } from '@/core/activitypub/ApResolverService.js'; import type { IObject } from '@/core/activitypub/type.js'; import type { HttpRequestService } from '@/core/HttpRequestService.js'; +import type { InstanceActorService } from '@/core/InstanceActorService.js'; import type { LoggerService } from '@/core/LoggerService.js'; +import type { MetaService } from '@/core/MetaService.js'; import type { UtilityService } from '@/core/UtilityService.js'; -import type { - FollowRequestsRepository, - MiMeta, - NoteReactionsRepository, - NotesRepository, - PollsRepository, - UsersRepository, -} from '@/models/_.js'; -import { SystemAccountService } from '@/core/SystemAccountService.js'; import { bindThis } from '@/decorators.js'; -import { Resolver } from '@/core/activitypub/ApResolverService.js'; +import type { NoteReactionsRepository, NotesRepository, PollsRepository, UsersRepository } from '@/models/index.js'; type MockResponse = { type: string; @@ -29,20 +18,18 @@ type MockResponse = { }; export class MockResolver extends Resolver { - #responseMap = new Map(); - #remoteGetTrials: string[] = []; + private _rs = new Map(); constructor(loggerService: LoggerService) { super( {} as Config, - {} as MiMeta, {} as UsersRepository, {} as NotesRepository, {} as PollsRepository, {} as NoteReactionsRepository, - {} as FollowRequestsRepository, {} as UtilityService, - {} as SystemAccountService, + {} as InstanceActorService, + {} as MetaService, {} as ApRequestService, {} as HttpRequestService, {} as ApRendererService, @@ -51,28 +38,18 @@ export class MockResolver extends Resolver { ); } - public register(uri: string, content: string | Record, type = 'application/activity+json'): void { - this.#responseMap.set(uri, { + public async _register(uri: string, content: string | Record, type = 'application/activity+json') { + this._rs.set(uri, { type, content: typeof content === 'string' ? content : JSON.stringify(content), }); } - public clear(): void { - this.#responseMap.clear(); - this.#remoteGetTrials.length = 0; - } - - public remoteGetTrials(): string[] { - return this.#remoteGetTrials; - } - @bindThis public async resolve(value: string | IObject): Promise { if (typeof value !== 'string') return value; - this.#remoteGetTrials.push(value); - const r = this.#responseMap.get(value); + const r = this._rs.get(value); if (!r) { throw new Error('Not registed for mock'); diff --git a/packages/backend/test/prelude/get-api-validator.ts b/packages/backend/test/prelude/get-api-validator.ts index 7aa7a92702..1f4a2dbc95 100644 --- a/packages/backend/test/prelude/get-api-validator.ts +++ b/packages/backend/test/prelude/get-api-validator.ts @@ -1,16 +1,11 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - +import { Schema } from '@/misc/schema'; import Ajv from 'ajv'; -import { Schema } from '@/misc/json-schema.js'; export const getValidator = (paramDef: Schema) => { - const ajv = new Ajv.default({ - useDefaults: true, - }); - ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); + const ajv = new Ajv({ + useDefaults: true, + }); + ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); - return ajv.compile(paramDef); -}; + return ajv.compile(paramDef); +} diff --git a/packages/backend/test/prelude/maybe.ts b/packages/backend/test/prelude/maybe.ts new file mode 100644 index 0000000000..b8679c1071 --- /dev/null +++ b/packages/backend/test/prelude/maybe.ts @@ -0,0 +1,18 @@ +import * as assert from 'assert'; +import { just, nothing } from '../../src/misc/prelude/maybe.js'; + +describe('just', () => { + test('has a value', () => { + assert.deepStrictEqual(just(3).isJust(), true); + }); + + test('has the inverse called get', () => { + assert.deepStrictEqual(just(3).get(), 3); + }); +}); + +describe('nothing', () => { + test('has no value', () => { + assert.deepStrictEqual(nothing().isJust(), false); + }); +}); diff --git a/packages/backend/test/prelude/url.ts b/packages/backend/test/prelude/url.ts index b26ae09444..23b6b22bb0 100644 --- a/packages/backend/test/prelude/url.ts +++ b/packages/backend/test/prelude/url.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as assert from 'assert'; import { query } from '../../src/misc/prelude/url.js'; diff --git a/packages/backend/test/resources/192.jpg b/packages/backend/test/resources/192.jpg deleted file mode 100644 index 76374628e0..0000000000 Binary files a/packages/backend/test/resources/192.jpg and /dev/null differ diff --git a/packages/backend/test/resources/192.png b/packages/backend/test/resources/192.png deleted file mode 100644 index 15fd1e3731..0000000000 Binary files a/packages/backend/test/resources/192.png and /dev/null differ diff --git a/packages/backend/test/resources/Lenna.jpg b/packages/backend/test/resources/Lenna.jpg new file mode 100644 index 0000000000..6b5b32281c Binary files /dev/null and b/packages/backend/test/resources/Lenna.jpg differ diff --git a/packages/backend/test/resources/Lenna.png b/packages/backend/test/resources/Lenna.png new file mode 100644 index 0000000000..59ef68aabd Binary files /dev/null and b/packages/backend/test/resources/Lenna.png differ diff --git a/packages/backend/test/resources/hw.png b/packages/backend/test/resources/hw.png deleted file mode 100644 index afe93faea6..0000000000 Binary files a/packages/backend/test/resources/hw.png and /dev/null differ diff --git a/packages/backend/test/resources/kick_gaba7.m4a b/packages/backend/test/resources/kick_gaba7.m4a deleted file mode 100644 index 321df6349f..0000000000 Binary files a/packages/backend/test/resources/kick_gaba7.m4a and /dev/null differ diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json index 2b562acda8..21afe1aaf3 100644 --- a/packages/backend/test/tsconfig.json +++ b/packages/backend/test/tsconfig.json @@ -5,20 +5,19 @@ "noImplicitAny": true, "noImplicitReturns": true, "noUnusedParameters": false, - "noUnusedLocals": false, + "noUnusedLocals": true, "noFallthroughCasesInSwitch": true, "declaration": false, "sourceMap": true, "target": "ES2022", - "module": "nodenext", - "moduleResolution": "nodenext", + "module": "es2020", + "moduleResolution": "node16", "allowSyntheticDefaultImports": true, "removeComments": false, "noLib": false, "strict": true, "strictNullChecks": true, "strictPropertyInitialization": false, - "skipLibCheck": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "resolveJsonModule": true, diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts deleted file mode 100644 index 9dad8e229d..0000000000 --- a/packages/backend/test/unit/AbuseReportNotificationService.ts +++ /dev/null @@ -1,393 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { describe, jest } from '@jest/globals'; -import { Test, TestingModule } from '@nestjs/testing'; -import { randomString } from '../utils.js'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; -import { - AbuseReportNotificationRecipientRepository, - MiAbuseReportNotificationRecipient, - MiAbuseUserReport, - MiSystemWebhook, - MiUser, - SystemWebhooksRepository, - UserProfilesRepository, - UsersRepository, -} from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { IdService } from '@/core/IdService.js'; -import { EmailService } from '@/core/EmailService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { MetaService } from '@/core/MetaService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; - -process.env.NODE_ENV = 'test'; - -describe('AbuseReportNotificationService', () => { - let app: TestingModule; - let service: AbuseReportNotificationService; - - // -------------------------------------------------------------------------------------- - - let usersRepository: UsersRepository; - let userProfilesRepository: UserProfilesRepository; - let systemWebhooksRepository: SystemWebhooksRepository; - let abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository; - let idService: IdService; - let roleService: jest.Mocked; - let emailService: jest.Mocked; - let webhookService: jest.Mocked; - - // -------------------------------------------------------------------------------------- - - let root: MiUser; - let alice: MiUser; - let bob: MiUser; - let systemWebhook1: MiSystemWebhook; - let systemWebhook2: MiSystemWebhook; - - // -------------------------------------------------------------------------------------- - - async function createUser(data: Partial = {}) { - const user = await usersRepository - .insert({ - id: idService.gen(), - ...data, - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - - await userProfilesRepository.insert({ - userId: user.id, - }); - - return user; - } - - async function createWebhook(data: Partial = {}) { - return systemWebhooksRepository - .insert({ - id: idService.gen(), - name: randomString(), - on: ['abuseReport'], - url: 'https://example.com', - secret: randomString(), - ...data, - }) - .then(x => systemWebhooksRepository.findOneByOrFail(x.identifiers[0])); - } - - async function createRecipient(data: Partial = {}) { - return abuseReportNotificationRecipientRepository - .insert({ - id: idService.gen(), - isActive: true, - name: randomString(), - ...data, - }) - .then(x => abuseReportNotificationRecipientRepository.findOneByOrFail(x.identifiers[0])); - } - - // -------------------------------------------------------------------------------------- - - beforeAll(async () => { - app = await Test - .createTestingModule({ - imports: [ - GlobalModule, - ], - providers: [ - AbuseReportNotificationService, - IdService, - { - provide: RoleService, useFactory: () => ({ getModeratorIds: jest.fn() }), - }, - { - provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }), - }, - { - provide: UserEntityService, useFactory: () => ({ - pack: (v: any) => Promise.resolve(v), - packMany: (v: any) => Promise.resolve(v), - }), - }, - { - provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }), - }, - { - provide: MetaService, useFactory: () => ({ fetch: jest.fn() }), - }, - { - provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }), - }, - { - provide: GlobalEventService, useFactory: () => ({ publishAdminStream: jest.fn() }), - }, - ], - }) - .compile(); - - usersRepository = app.get(DI.usersRepository); - userProfilesRepository = app.get(DI.userProfilesRepository); - systemWebhooksRepository = app.get(DI.systemWebhooksRepository); - abuseReportNotificationRecipientRepository = app.get(DI.abuseReportNotificationRecipientRepository); - - service = app.get(AbuseReportNotificationService); - idService = app.get(IdService); - roleService = app.get(RoleService) as jest.Mocked; - emailService = app.get(EmailService) as jest.Mocked; - webhookService = app.get(SystemWebhookService) as jest.Mocked; - - app.enableShutdownHooks(); - }); - - beforeEach(async () => { - root = await createUser({ username: 'root', usernameLower: 'root' }); - alice = await createUser({ username: 'alice', usernameLower: 'alice' }); - bob = await createUser({ username: 'bob', usernameLower: 'bob' }); - systemWebhook1 = await createWebhook(); - systemWebhook2 = await createWebhook(); - - roleService.getModeratorIds.mockResolvedValue([root.id, alice.id, bob.id]); - }); - - afterEach(async () => { - emailService.sendEmail.mockClear(); - webhookService.enqueueSystemWebhook.mockClear(); - - await usersRepository.createQueryBuilder().delete().execute(); - await userProfilesRepository.createQueryBuilder().delete().execute(); - await systemWebhooksRepository.createQueryBuilder().delete().execute(); - await abuseReportNotificationRecipientRepository.createQueryBuilder().delete().execute(); - }); - - afterAll(async () => { - await app.close(); - }); - - // -------------------------------------------------------------------------------------- - - describe('createRecipient', () => { - test('作成成功1', async () => { - const params = { - isActive: true, - name: randomString(), - method: 'email' as RecipientMethod, - userId: alice.id, - systemWebhookId: null, - }; - - const recipient1 = await service.createRecipient(params, root); - expect(recipient1).toMatchObject(params); - }); - - test('作成成功2', async () => { - const params = { - isActive: true, - name: randomString(), - method: 'webhook' as RecipientMethod, - userId: null, - systemWebhookId: systemWebhook1.id, - }; - - const recipient1 = await service.createRecipient(params, root); - expect(recipient1).toMatchObject(params); - }); - }); - - describe('updateRecipient', () => { - test('更新成功1', async () => { - const recipient1 = await createRecipient({ - method: 'email', - userId: alice.id, - }); - - const params = { - id: recipient1.id, - isActive: false, - name: randomString(), - method: 'email' as RecipientMethod, - userId: bob.id, - systemWebhookId: null, - }; - - const recipient2 = await service.updateRecipient(params, root); - expect(recipient2).toMatchObject(params); - }); - - test('更新成功2', async () => { - const recipient1 = await createRecipient({ - method: 'webhook', - systemWebhookId: systemWebhook1.id, - }); - - const params = { - id: recipient1.id, - isActive: false, - name: randomString(), - method: 'webhook' as RecipientMethod, - userId: null, - systemWebhookId: systemWebhook2.id, - }; - - const recipient2 = await service.updateRecipient(params, root); - expect(recipient2).toMatchObject(params); - }); - }); - - describe('deleteRecipient', () => { - test('削除成功1', async () => { - const recipient1 = await createRecipient({ - method: 'email', - userId: alice.id, - }); - - await service.deleteRecipient(recipient1.id, root); - - await expect(abuseReportNotificationRecipientRepository.findOneBy({ id: recipient1.id })).resolves.toBeNull(); - }); - }); - - describe('fetchRecipients', () => { - async function create() { - const recipient1 = await createRecipient({ - method: 'email', - userId: alice.id, - }); - const recipient2 = await createRecipient({ - method: 'email', - userId: bob.id, - }); - - const recipient3 = await createRecipient({ - method: 'webhook', - systemWebhookId: systemWebhook1.id, - }); - const recipient4 = await createRecipient({ - method: 'webhook', - systemWebhookId: systemWebhook2.id, - }); - - return [recipient1, recipient2, recipient3, recipient4]; - } - - test('フィルタなし', async () => { - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({}); - expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]); - }); - - test('フィルタなし(非モデレータは除外される)', async () => { - roleService.getModeratorIds.mockClear(); - roleService.getModeratorIds.mockResolvedValue([root.id, bob.id]); - - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({}); - // aliceはモデレータではないので除外される - expect(recipients).toEqual([recipient2, recipient3, recipient4]); - }); - - test('フィルタなし(非モデレータでも除外されないオプション設定)', async () => { - roleService.getModeratorIds.mockClear(); - roleService.getModeratorIds.mockResolvedValue([root.id, bob.id]); - - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({}, { removeUnauthorized: false }); - expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]); - }); - - test('emailのみ', async () => { - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({ method: ['email'] }); - expect(recipients).toEqual([recipient1, recipient2]); - }); - - test('webhookのみ', async () => { - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({ method: ['webhook'] }); - expect(recipients).toEqual([recipient3, recipient4]); - }); - - test('すべて', async () => { - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({ method: ['email', 'webhook'] }); - expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]); - }); - - test('ID指定', async () => { - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id] }); - expect(recipients).toEqual([recipient1, recipient3]); - }); - - test('ID指定(method=emailではないIDが混ざりこまない)', async () => { - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id], method: ['email'] }); - expect(recipients).toEqual([recipient1]); - }); - - test('ID指定(method=webhookではないIDが混ざりこまない)', async () => { - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id], method: ['webhook'] }); - expect(recipients).toEqual([recipient3]); - }); - }); - - describe('notifySystemWebhook', () => { - test('非アクティブな通報通知はWebhook送信から除外される', async () => { - const recipient1 = await createRecipient({ - method: 'webhook', - systemWebhookId: systemWebhook1.id, - isActive: true, - }); - const recipient2 = await createRecipient({ - method: 'webhook', - systemWebhookId: systemWebhook2.id, - isActive: false, - }); - - const reports: MiAbuseUserReport[] = [ - { - id: idService.gen(), - targetUserId: alice.id, - targetUser: alice, - reporterId: bob.id, - reporter: bob, - assigneeId: null, - assignee: null, - resolved: false, - forwarded: false, - comment: 'test', - moderationNote: '', - resolvedAs: null, - targetUserHost: null, - reporterHost: null, - }, - ]; - - await service.notifySystemWebhook(reports, 'abuseReport'); - - // 実際に除外されるかはSystemWebhookService側で確認する. - // ここでは非アクティブな通報通知を除外設定できているかを確認する - expect(webhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1); - expect(webhookService.enqueueSystemWebhook.mock.calls[0][0]).toBe('abuseReport'); - expect(webhookService.enqueueSystemWebhook.mock.calls[0][2]).toEqual({ excludes: [systemWebhook2.id] }); - }); - }); -}); diff --git a/packages/backend/test/unit/AnnouncementService.ts b/packages/backend/test/unit/AnnouncementService.ts deleted file mode 100644 index 0b24f109f8..0000000000 --- a/packages/backend/test/unit/AnnouncementService.ts +++ /dev/null @@ -1,210 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -process.env.NODE_ENV = 'test'; - -import { jest } from '@jest/globals'; -import { ModuleMocker } from 'jest-mock'; -import { Test } from '@nestjs/testing'; -import { GlobalModule } from '@/GlobalModule.js'; -import { AnnouncementService } from '@/core/AnnouncementService.js'; -import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js'; -import type { - AnnouncementReadsRepository, - AnnouncementsRepository, - MiAnnouncement, - MiUser, - UsersRepository, -} from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { genAidx } from '@/misc/id/aidx.js'; -import { CacheService } from '@/core/CacheService.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { secureRndstr } from '@/misc/secure-rndstr.js'; -import type { TestingModule } from '@nestjs/testing'; -import type { MockFunctionMetadata } from 'jest-mock'; - -const moduleMocker = new ModuleMocker(global); - -describe('AnnouncementService', () => { - let app: TestingModule; - let announcementService: AnnouncementService; - let usersRepository: UsersRepository; - let announcementsRepository: AnnouncementsRepository; - let announcementReadsRepository: AnnouncementReadsRepository; - let globalEventService: jest.Mocked; - let moderationLogService: jest.Mocked; - - function createUser(data: Partial = {}) { - const un = secureRndstr(16); - return usersRepository.insert({ - id: genAidx(Date.now()), - username: un, - usernameLower: un.toLowerCase(), - ...data, - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - } - - function createAnnouncement(data: Partial = {}) { - return announcementsRepository.insert({ - id: genAidx(data.createdAt?.getTime() ?? Date.now()), - updatedAt: null, - title: 'Title', - text: 'Text', - ...data, - }) - .then(x => announcementsRepository.findOneByOrFail(x.identifiers[0])); - } - - beforeEach(async () => { - app = await Test.createTestingModule({ - imports: [ - GlobalModule, - ], - providers: [ - AnnouncementService, - AnnouncementEntityService, - CacheService, - IdService, - ], - }) - .useMocker((token) => { - if (token === GlobalEventService) { - return { - publishMainStream: jest.fn(), - publishBroadcastStream: jest.fn(), - }; - } else if (token === ModerationLogService) { - return { - log: jest.fn(), - }; - } else if (typeof token === 'function') { - const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata; - const Mock = moduleMocker.generateFromMetadata(mockMetadata); - return new Mock(); - } - }) - .compile(); - - app.enableShutdownHooks(); - - announcementService = app.get(AnnouncementService); - usersRepository = app.get(DI.usersRepository); - announcementsRepository = app.get(DI.announcementsRepository); - announcementReadsRepository = app.get(DI.announcementReadsRepository); - globalEventService = app.get(GlobalEventService) as jest.Mocked; - moderationLogService = app.get(ModerationLogService) as jest.Mocked; - }); - - afterEach(async () => { - await Promise.all([ - app.get(DI.metasRepository).createQueryBuilder().delete().execute(), - usersRepository.createQueryBuilder().delete().execute(), - announcementsRepository.createQueryBuilder().delete().execute(), - announcementReadsRepository.createQueryBuilder().delete().execute(), - ]); - - await app.close(); - }); - - describe('getUnreadAnnouncements', () => { - test('通常', async () => { - const user = await createUser(); - const announcement = await createAnnouncement({ - title: '1', - }); - - const result = await announcementService.getUnreadAnnouncements(user); - - expect(result.length).toBe(1); - expect(result[0].title).toBe(announcement.title); - }); - - test('isActiveがfalseは除外', async () => { - const user = await createUser(); - await createAnnouncement({ - isActive: false, - }); - - const result = await announcementService.getUnreadAnnouncements(user); - - expect(result.length).toBe(0); - }); - - test('forExistingUsers', async () => { - const user = await createUser(); - const [announcementAfter, announcementBefore, announcementBefore2] = await Promise.all([ - createAnnouncement({ - title: 'after', - createdAt: new Date(), - forExistingUsers: true, - }), - createAnnouncement({ - title: 'before', - createdAt: new Date(Date.now() - 1000), - forExistingUsers: true, - }), - createAnnouncement({ - title: 'before2', - createdAt: new Date(Date.now() - 1000), - forExistingUsers: false, - }), - ]); - - const result = await announcementService.getUnreadAnnouncements(user); - - expect(result.length).toBe(2); - expect(result.some(a => a.title === announcementAfter.title)).toBe(true); - expect(result.some(a => a.title === announcementBefore.title)).toBe(false); - expect(result.some(a => a.title === announcementBefore2.title)).toBe(true); - }); - }); - - describe('create', () => { - test('通常', async () => { - const me = await createUser(); - const result = await announcementService.create({ - title: 'Title', - text: 'Text', - }, me); - - expect(result.raw.title).toBe('Title'); - expect(result.packed.title).toBe('Title'); - - expect(globalEventService.publishBroadcastStream).toHaveBeenCalled(); - expect(globalEventService.publishBroadcastStream.mock.lastCall![0]).toBe('announcementCreated'); - expect((globalEventService.publishBroadcastStream.mock.lastCall![1] as any).announcement).toBe(result.packed); - expect(moderationLogService.log).toHaveBeenCalled(); - }); - - test('ユーザー指定', async () => { - const me = await createUser(); - const user = await createUser(); - const result = await announcementService.create({ - title: 'Title', - text: 'Text', - userId: user.id, - }, me); - - expect(result.raw.title).toBe('Title'); - expect(result.packed.title).toBe('Title'); - - expect(globalEventService.publishBroadcastStream).not.toHaveBeenCalled(); - expect(globalEventService.publishMainStream).toHaveBeenCalled(); - expect(globalEventService.publishMainStream.mock.lastCall![0]).toBe(user.id); - expect(globalEventService.publishMainStream.mock.lastCall![1]).toBe('announcementCreated'); - expect((globalEventService.publishMainStream.mock.lastCall![2] as any).announcement).toBe(result.packed); - expect(moderationLogService.log).toHaveBeenCalled(); - }); - }); - - describe('read', () => { - // TODO - }); -}); - diff --git a/packages/backend/test/unit/ApMfmService.ts b/packages/backend/test/unit/ApMfmService.ts deleted file mode 100644 index e81a321c9b..0000000000 --- a/packages/backend/test/unit/ApMfmService.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as assert from 'assert'; -import { Test } from '@nestjs/testing'; - -import { CoreModule } from '@/core/CoreModule.js'; -import { ApMfmService } from '@/core/activitypub/ApMfmService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { MiNote } from '@/models/Note.js'; - -describe('ApMfmService', () => { - let apMfmService: ApMfmService; - - beforeAll(async () => { - const app = await Test.createTestingModule({ - imports: [GlobalModule, CoreModule], - }).compile(); - apMfmService = app.get(ApMfmService); - }); - - describe('getNoteHtml', () => { - test('Do not provide _misskey_content for simple text', () => { - const note = { - text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com', - mentionedRemoteUsers: '[]', - }; - - const { content, noMisskeyContent } = apMfmService.getNoteHtml(note); - - assert.equal(noMisskeyContent, true, 'noMisskeyContent'); - assert.equal(content, '

テキスト @mention 🍊 ​:emoji:​ https://example.com

', 'content'); - }); - - test('Provide _misskey_content for MFM', () => { - const note = { - text: '$[tada foo]', - mentionedRemoteUsers: '[]', - }; - - const { content, noMisskeyContent } = apMfmService.getNoteHtml(note); - - assert.equal(noMisskeyContent, false, 'noMisskeyContent'); - assert.equal(content, '

foo

', 'content'); - }); - }); -}); diff --git a/packages/backend/test/unit/CaptchaService.ts b/packages/backend/test/unit/CaptchaService.ts deleted file mode 100644 index 51b70b05a1..0000000000 --- a/packages/backend/test/unit/CaptchaService.ts +++ /dev/null @@ -1,622 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { afterAll, beforeAll, beforeEach, describe, expect, jest } from '@jest/globals'; -import { Test, TestingModule } from '@nestjs/testing'; -import { Response } from 'node-fetch'; -import { - CaptchaError, - CaptchaErrorCode, - captchaErrorCodes, - CaptchaSaveResult, - CaptchaService, -} from '@/core/CaptchaService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { HttpRequestService } from '@/core/HttpRequestService.js'; -import { MetaService } from '@/core/MetaService.js'; -import { MiMeta } from '@/models/Meta.js'; -import { LoggerService } from '@/core/LoggerService.js'; - -describe('CaptchaService', () => { - let app: TestingModule; - let service: CaptchaService; - let httpRequestService: jest.Mocked; - let metaService: jest.Mocked; - - beforeAll(async () => { - app = await Test.createTestingModule({ - imports: [ - GlobalModule, - ], - providers: [ - CaptchaService, - LoggerService, - { - provide: HttpRequestService, useFactory: () => ({ send: jest.fn() }), - }, - { - provide: MetaService, useFactory: () => ({ - fetch: jest.fn(), - update: jest.fn(), - }), - }, - ], - }).compile(); - - app.enableShutdownHooks(); - - service = app.get(CaptchaService); - httpRequestService = app.get(HttpRequestService) as jest.Mocked; - metaService = app.get(MetaService) as jest.Mocked; - }); - - beforeEach(() => { - httpRequestService.send.mockClear(); - metaService.update.mockClear(); - metaService.fetch.mockClear(); - }); - - afterAll(async () => { - await app.close(); - }); - - function successMock(result: object) { - httpRequestService.send.mockResolvedValue({ - ok: true, - status: 200, - json: async () => (result), - } as Response); - } - - function failureHttpMock() { - httpRequestService.send.mockResolvedValue({ - ok: false, - status: 400, - } as Response); - } - - function failureVerificationMock(result: object) { - httpRequestService.send.mockResolvedValue({ - ok: true, - status: 200, - json: async () => (result), - } as Response); - } - - async function testCaptchaError(code: CaptchaErrorCode, test: () => Promise) { - try { - await test(); - expect(false).toBe(true); - } catch (e) { - expect(e instanceof CaptchaError).toBe(true); - - const _e = e as CaptchaError; - expect(_e.code).toBe(code); - } - } - - describe('verifyRecaptcha', () => { - test('success', async () => { - successMock({ success: true }); - await service.verifyRecaptcha('secret', 'response'); - }); - - test('noResponseProvided', async () => { - await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyRecaptcha('secret', null)); - }); - - test('requestFailed', async () => { - failureHttpMock(); - await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyRecaptcha('secret', 'response')); - }); - - test('verificationFailed', async () => { - failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] }); - await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyRecaptcha('secret', 'response')); - }); - }); - - describe('verifyHcaptcha', () => { - test('success', async () => { - successMock({ success: true }); - await service.verifyHcaptcha('secret', 'response'); - }); - - test('noResponseProvided', async () => { - await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyHcaptcha('secret', null)); - }); - - test('requestFailed', async () => { - failureHttpMock(); - await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyHcaptcha('secret', 'response')); - }); - - test('verificationFailed', async () => { - failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] }); - await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyHcaptcha('secret', 'response')); - }); - }); - - describe('verifyMcaptcha', () => { - const host = 'https://localhost'; - - test('success', async () => { - successMock({ valid: true }); - await service.verifyMcaptcha('secret', 'sitekey', host, 'response'); - }); - - test('noResponseProvided', async () => { - await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyMcaptcha('secret', 'sitekey', host, null)); - }); - - test('requestFailed', async () => { - failureHttpMock(); - await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyMcaptcha('secret', 'sitekey', host, 'response')); - }); - - test('verificationFailed', async () => { - failureVerificationMock({ valid: false }); - await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyMcaptcha('secret', 'sitekey', host, 'response')); - }); - }); - - describe('verifyTurnstile', () => { - test('success', async () => { - successMock({ success: true }); - await service.verifyTurnstile('secret', 'response'); - }); - - test('noResponseProvided', async () => { - await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyTurnstile('secret', null)); - }); - - test('requestFailed', async () => { - failureHttpMock(); - await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyTurnstile('secret', 'response')); - }); - - test('verificationFailed', async () => { - failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] }); - await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyTurnstile('secret', 'response')); - }); - }); - - describe('verifyTestcaptcha', () => { - test('success', async () => { - await service.verifyTestcaptcha('testcaptcha-passed'); - }); - - test('noResponseProvided', async () => { - await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyTestcaptcha(null)); - }); - - test('verificationFailed', async () => { - await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyTestcaptcha('testcaptcha-failed')); - }); - }); - - describe('get', () => { - function setupMeta(meta: Partial) { - metaService.fetch.mockResolvedValue(meta as MiMeta); - } - - test('values', async () => { - setupMeta({ - enableHcaptcha: false, - enableMcaptcha: false, - enableRecaptcha: false, - enableTurnstile: false, - enableTestcaptcha: false, - hcaptchaSiteKey: 'hcaptcha-sitekey', - hcaptchaSecretKey: 'hcaptcha-secret', - mcaptchaSitekey: 'mcaptcha-sitekey', - mcaptchaSecretKey: 'mcaptcha-secret', - mcaptchaInstanceUrl: 'https://localhost', - recaptchaSiteKey: 'recaptcha-sitekey', - recaptchaSecretKey: 'recaptcha-secret', - turnstileSiteKey: 'turnstile-sitekey', - turnstileSecretKey: 'turnstile-secret', - }); - - const result = await service.get(); - expect(result.provider).toBe('none'); - expect(result.hcaptcha.siteKey).toBe('hcaptcha-sitekey'); - expect(result.hcaptcha.secretKey).toBe('hcaptcha-secret'); - expect(result.mcaptcha.siteKey).toBe('mcaptcha-sitekey'); - expect(result.mcaptcha.secretKey).toBe('mcaptcha-secret'); - expect(result.mcaptcha.instanceUrl).toBe('https://localhost'); - expect(result.recaptcha.siteKey).toBe('recaptcha-sitekey'); - expect(result.recaptcha.secretKey).toBe('recaptcha-secret'); - expect(result.turnstile.siteKey).toBe('turnstile-sitekey'); - expect(result.turnstile.secretKey).toBe('turnstile-secret'); - }); - - describe('provider', () => { - test('none', async () => { - setupMeta({ - enableHcaptcha: false, - enableMcaptcha: false, - enableRecaptcha: false, - enableTurnstile: false, - enableTestcaptcha: false, - }); - - const result = await service.get(); - expect(result.provider).toBe('none'); - }); - - test('hcaptcha', async () => { - setupMeta({ - enableHcaptcha: true, - enableMcaptcha: false, - enableRecaptcha: false, - enableTurnstile: false, - enableTestcaptcha: false, - }); - - const result = await service.get(); - expect(result.provider).toBe('hcaptcha'); - }); - - test('mcaptcha', async () => { - setupMeta({ - enableHcaptcha: false, - enableMcaptcha: true, - enableRecaptcha: false, - enableTurnstile: false, - enableTestcaptcha: false, - }); - - const result = await service.get(); - expect(result.provider).toBe('mcaptcha'); - }); - - test('recaptcha', async () => { - setupMeta({ - enableHcaptcha: false, - enableMcaptcha: false, - enableRecaptcha: true, - enableTurnstile: false, - enableTestcaptcha: false, - }); - - const result = await service.get(); - expect(result.provider).toBe('recaptcha'); - }); - - test('turnstile', async () => { - setupMeta({ - enableHcaptcha: false, - enableMcaptcha: false, - enableRecaptcha: false, - enableTurnstile: true, - enableTestcaptcha: false, - }); - - const result = await service.get(); - expect(result.provider).toBe('turnstile'); - }); - - test('testcaptcha', async () => { - setupMeta({ - enableHcaptcha: false, - enableMcaptcha: false, - enableRecaptcha: false, - enableTurnstile: false, - enableTestcaptcha: true, - }); - - const result = await service.get(); - expect(result.provider).toBe('testcaptcha'); - }); - }); - }); - - describe('save', () => { - const host = 'https://localhost'; - - describe('[success] 検証に成功した時だけ保存できる+他のプロバイダの設定値を誤って更新しない', () => { - beforeEach(() => { - successMock({ success: true, valid: true }); - }); - - async function assertSuccess(promise: Promise, expectMeta: Partial) { - await expect(promise) - .resolves - .toStrictEqual({ success: true }); - const partialParams = metaService.update.mock.calls[0][0]; - expect(partialParams).toStrictEqual(expectMeta); - } - - test('none', async () => { - await assertSuccess( - service.save('none'), - { - enableHcaptcha: false, - enableMcaptcha: false, - enableRecaptcha: false, - enableTurnstile: false, - enableTestcaptcha: false, - }, - ); - }); - - test('hcaptcha', async () => { - await assertSuccess( - service.save('hcaptcha', { - sitekey: 'hcaptcha-sitekey', - secret: 'hcaptcha-secret', - captchaResult: 'hcaptcha-passed', - }), - { - enableHcaptcha: true, - enableMcaptcha: false, - enableRecaptcha: false, - enableTurnstile: false, - enableTestcaptcha: false, - hcaptchaSiteKey: 'hcaptcha-sitekey', - hcaptchaSecretKey: 'hcaptcha-secret', - }, - ); - }); - - test('mcaptcha', async () => { - await assertSuccess( - service.save('mcaptcha', { - sitekey: 'mcaptcha-sitekey', - secret: 'mcaptcha-secret', - instanceUrl: host, - captchaResult: 'mcaptcha-passed', - }), - { - enableHcaptcha: false, - enableMcaptcha: true, - enableRecaptcha: false, - enableTurnstile: false, - enableTestcaptcha: false, - mcaptchaSitekey: 'mcaptcha-sitekey', - mcaptchaSecretKey: 'mcaptcha-secret', - mcaptchaInstanceUrl: host, - }, - ); - }); - - test('recaptcha', async () => { - await assertSuccess( - service.save('recaptcha', { - sitekey: 'recaptcha-sitekey', - secret: 'recaptcha-secret', - captchaResult: 'recaptcha-passed', - }), - { - enableHcaptcha: false, - enableMcaptcha: false, - enableRecaptcha: true, - enableTurnstile: false, - enableTestcaptcha: false, - recaptchaSiteKey: 'recaptcha-sitekey', - recaptchaSecretKey: 'recaptcha-secret', - }, - ); - }); - - test('turnstile', async () => { - await assertSuccess( - service.save('turnstile', { - sitekey: 'turnstile-sitekey', - secret: 'turnstile-secret', - captchaResult: 'turnstile-passed', - }), - { - enableHcaptcha: false, - enableMcaptcha: false, - enableRecaptcha: false, - enableTurnstile: true, - enableTestcaptcha: false, - turnstileSiteKey: 'turnstile-sitekey', - turnstileSecretKey: 'turnstile-secret', - }, - ); - }); - - test('testcaptcha', async () => { - await assertSuccess( - service.save('testcaptcha', { - sitekey: 'testcaptcha-sitekey', - secret: 'testcaptcha-secret', - captchaResult: 'testcaptcha-passed', - }), - { - enableHcaptcha: false, - enableMcaptcha: false, - enableRecaptcha: false, - enableTurnstile: false, - enableTestcaptcha: true, - }, - ); - }); - }); - - describe('[failure] 検証に失敗した場合は保存できない+設定値の更新そのものが発生しない', () => { - async function assertFailure(code: CaptchaErrorCode, promise: Promise) { - const res = await promise; - expect(res.success).toBe(false); - if (!res.success) { - expect(res.error.code).toBe(code); - } - expect(metaService.update).not.toBeCalled(); - } - - describe('invalidParameters', () => { - test('hcaptcha', async () => { - await assertFailure( - captchaErrorCodes.invalidParameters, - service.save('hcaptcha', { - sitekey: 'hcaptcha-sitekey', - secret: 'hcaptcha-secret', - captchaResult: null, - }), - ); - }); - - test('mcaptcha', async () => { - await assertFailure( - captchaErrorCodes.invalidParameters, - service.save('mcaptcha', { - sitekey: 'mcaptcha-sitekey', - secret: 'mcaptcha-secret', - instanceUrl: host, - captchaResult: null, - }), - ); - }); - - test('recaptcha', async () => { - await assertFailure( - captchaErrorCodes.invalidParameters, - service.save('recaptcha', { - sitekey: 'recaptcha-sitekey', - secret: 'recaptcha-secret', - captchaResult: null, - }), - ); - }); - - test('turnstile', async () => { - await assertFailure( - captchaErrorCodes.invalidParameters, - service.save('turnstile', { - sitekey: 'turnstile-sitekey', - secret: 'turnstile-secret', - captchaResult: null, - }), - ); - }); - - test('testcaptcha', async () => { - await assertFailure( - captchaErrorCodes.invalidParameters, - service.save('testcaptcha', { - captchaResult: null, - }), - ); - }); - }); - - describe('requestFailed', () => { - beforeEach(() => { - failureHttpMock(); - }); - - test('hcaptcha', async () => { - await assertFailure( - captchaErrorCodes.requestFailed, - service.save('hcaptcha', { - sitekey: 'hcaptcha-sitekey', - secret: 'hcaptcha-secret', - captchaResult: 'hcaptcha-passed', - }), - ); - }); - - test('mcaptcha', async () => { - await assertFailure( - captchaErrorCodes.requestFailed, - service.save('mcaptcha', { - sitekey: 'mcaptcha-sitekey', - secret: 'mcaptcha-secret', - instanceUrl: host, - captchaResult: 'mcaptcha-passed', - }), - ); - }); - - test('recaptcha', async () => { - await assertFailure( - captchaErrorCodes.requestFailed, - service.save('recaptcha', { - sitekey: 'recaptcha-sitekey', - secret: 'recaptcha-secret', - captchaResult: 'recaptcha-passed', - }), - ); - }); - - test('turnstile', async () => { - await assertFailure( - captchaErrorCodes.requestFailed, - service.save('turnstile', { - sitekey: 'turnstile-sitekey', - secret: 'turnstile-secret', - captchaResult: 'turnstile-passed', - }), - ); - }); - - // testchapchaはrequestFailedがない - }); - - describe('verificationFailed', () => { - beforeEach(() => { - failureVerificationMock({ success: false, valid: false, 'error-codes': ['code01', 'code02'] }); - }); - - test('hcaptcha', async () => { - await assertFailure( - captchaErrorCodes.verificationFailed, - service.save('hcaptcha', { - sitekey: 'hcaptcha-sitekey', - secret: 'hcaptcha-secret', - captchaResult: 'hccaptcha-passed', - }), - ); - }); - - test('mcaptcha', async () => { - await assertFailure( - captchaErrorCodes.verificationFailed, - service.save('mcaptcha', { - sitekey: 'mcaptcha-sitekey', - secret: 'mcaptcha-secret', - instanceUrl: host, - captchaResult: 'mcaptcha-passed', - }), - ); - }); - - test('recaptcha', async () => { - await assertFailure( - captchaErrorCodes.verificationFailed, - service.save('recaptcha', { - sitekey: 'recaptcha-sitekey', - secret: 'recaptcha-secret', - captchaResult: 'recaptcha-passed', - }), - ); - }); - - test('turnstile', async () => { - await assertFailure( - captchaErrorCodes.verificationFailed, - service.save('turnstile', { - sitekey: 'turnstile-sitekey', - secret: 'turnstile-secret', - captchaResult: 'turnstile-passed', - }), - ); - }); - - test('testcaptcha', async () => { - await assertFailure( - captchaErrorCodes.verificationFailed, - service.save('testcaptcha', { - captchaResult: 'testcaptcha-failed', - }), - ); - }); - }); - }); - }); -}); diff --git a/packages/backend/test/unit/CustomEmojiService.ts b/packages/backend/test/unit/CustomEmojiService.ts deleted file mode 100644 index d6c73a2091..0000000000 --- a/packages/backend/test/unit/CustomEmojiService.ts +++ /dev/null @@ -1,817 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { afterEach, beforeAll, describe, test } from '@jest/globals'; -import { Test, TestingModule } from '@nestjs/testing'; -import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { IdService } from '@/core/IdService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import { DI } from '@/di-symbols.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { EmojisRepository } from '@/models/_.js'; -import { MiEmoji } from '@/models/Emoji.js'; - -describe('CustomEmojiService', () => { - let app: TestingModule; - let service: CustomEmojiService; - - let emojisRepository: EmojisRepository; - let idService: IdService; - - beforeAll(async () => { - app = await Test - .createTestingModule({ - imports: [ - GlobalModule, - ], - providers: [ - CustomEmojiService, - UtilityService, - IdService, - EmojiEntityService, - ModerationLogService, - GlobalEventService, - ], - }) - .compile(); - app.enableShutdownHooks(); - - service = app.get(CustomEmojiService); - emojisRepository = app.get(DI.emojisRepository); - idService = app.get(IdService); - }); - - describe('fetchEmojis', () => { - async function insert(data: Partial[]) { - for (const d of data) { - const id = idService.gen(); - await emojisRepository.insert({ - id: id, - updatedAt: new Date(), - ...d, - }); - } - } - - function call(params: Parameters['0']) { - return service.fetchEmojis( - params, - { - // テスト向けに - sortKeys: ['+id'], - }, - ); - } - - function defaultData(suffix: string, override?: Partial): Partial { - return { - name: `emoji${suffix}`, - host: null, - category: 'default', - originalUrl: `https://example.com/emoji${suffix}.png`, - publicUrl: `https://example.com/emoji${suffix}.png`, - type: 'image/png', - aliases: [`emoji${suffix}`], - license: 'CC0', - isSensitive: false, - localOnly: false, - roleIdsThatCanBeUsedThisEmojiAsReaction: [], - ...override, - }; - } - - afterEach(async () => { - await emojisRepository.createQueryBuilder().delete().execute(); - }); - - describe('単独', () => { - test('updatedAtFrom', async () => { - await insert([ - defaultData('001', { updatedAt: new Date('2021-01-01T00:00:00.000Z') }), - defaultData('002', { updatedAt: new Date('2021-01-02T00:00:00.000Z') }), - defaultData('003', { updatedAt: new Date('2021-01-03T00:00:00.000Z') }), - ]); - - const actual = await call({ - query: { - updatedAtFrom: '2021-01-02T00:00:00.000Z', - }, - }); - - expect(actual.allCount).toBe(2); - expect(actual.emojis[0].name).toBe('emoji002'); - expect(actual.emojis[1].name).toBe('emoji003'); - }); - - test('updatedAtTo', async () => { - await insert([ - defaultData('001', { updatedAt: new Date('2021-01-01T00:00:00.000Z') }), - defaultData('002', { updatedAt: new Date('2021-01-02T00:00:00.000Z') }), - defaultData('003', { updatedAt: new Date('2021-01-03T00:00:00.000Z') }), - ]); - - const actual = await call({ - query: { - updatedAtTo: '2021-01-02T00:00:00.000Z', - }, - }); - - expect(actual.allCount).toBe(2); - expect(actual.emojis[0].name).toBe('emoji001'); - expect(actual.emojis[1].name).toBe('emoji002'); - }); - - describe('name', () => { - test('single', async () => { - await insert([ - defaultData('001'), - defaultData('002'), - ]); - - const actual = await call({ - query: { - name: 'emoji001', - }, - }); - - expect(actual.allCount).toBe(1); - expect(actual.emojis[0].name).toBe('emoji001'); - }); - - test('multi', async () => { - await insert([ - defaultData('001'), - defaultData('002'), - ]); - - const actual = await call({ - query: { - name: 'emoji001 emoji002', - }, - }); - - expect(actual.allCount).toBe(2); - expect(actual.emojis[0].name).toBe('emoji001'); - expect(actual.emojis[1].name).toBe('emoji002'); - }); - - test('keyword', async () => { - await insert([ - defaultData('001'), - defaultData('002'), - defaultData('003', { name: 'em003' }), - ]); - - const actual = await call({ - query: { - name: 'oji', - }, - }); - - expect(actual.allCount).toBe(2); - expect(actual.emojis[0].name).toBe('emoji001'); - expect(actual.emojis[1].name).toBe('emoji002'); - }); - - test('escape', async () => { - await insert([ - defaultData('001'), - ]); - - const actual = await call({ - query: { - name: '%', - }, - }); - - expect(actual.allCount).toBe(0); - }); - }); - - describe('host', () => { - test('single', async () => { - await insert([ - defaultData('001', { host: 'example.com' }), - defaultData('002', { host: 'example.com' }), - defaultData('003', { host: '1.example.com' }), - defaultData('004', { host: '2.example.com' }), - ]); - - const actual = await call({ - query: { - host: 'example.com', - hostType: 'remote', - }, - }); - - expect(actual.allCount).toBe(4); - }); - - test('multi', async () => { - await insert([ - defaultData('001', { host: 'example.com' }), - defaultData('002', { host: 'example.com' }), - defaultData('003', { host: '1.example.com' }), - defaultData('004', { host: '2.example.com' }), - ]); - - const actual = await call({ - query: { - host: '1.example.com 2.example.com', - hostType: 'remote', - }, - }); - - expect(actual.allCount).toBe(2); - expect(actual.emojis[0].name).toBe('emoji003'); - expect(actual.emojis[1].name).toBe('emoji004'); - }); - - test('keyword', async () => { - await insert([ - defaultData('001', { host: 'example.com' }), - defaultData('002', { host: 'example.com' }), - defaultData('003', { host: '1.example.com' }), - defaultData('004', { host: '2.example.com' }), - ]); - - const actual = await call({ - query: { - host: 'example', - hostType: 'remote', - }, - }); - - expect(actual.allCount).toBe(4); - }); - - test('escape', async () => { - await insert([ - defaultData('001', { host: 'example.com' }), - ]); - - const actual = await call({ - query: { - host: '%', - hostType: 'remote', - }, - }); - - expect(actual.allCount).toBe(0); - }); - }); - - describe('uri', () => { - test('single', async () => { - await insert([ - defaultData('001', { uri: 'uri001' }), - defaultData('002', { uri: 'uri002' }), - defaultData('003', { uri: 'uri003' }), - ]); - - const actual = await call({ - query: { - uri: 'uri002', - }, - }); - - expect(actual.allCount).toBe(1); - expect(actual.emojis[0].name).toBe('emoji002'); - }); - - test('multi', async () => { - await insert([ - defaultData('001', { uri: 'uri001' }), - defaultData('002', { uri: 'uri002' }), - defaultData('003', { uri: 'uri003' }), - ]); - - const actual = await call({ - query: { - uri: 'uri001 uri003', - }, - }); - - expect(actual.allCount).toBe(2); - expect(actual.emojis[0].name).toBe('emoji001'); - expect(actual.emojis[1].name).toBe('emoji003'); - }); - - test('keyword', async () => { - await insert([ - defaultData('001', { uri: 'uri001' }), - defaultData('002', { uri: 'uri002' }), - defaultData('003', { uri: 'uri003' }), - ]); - - const actual = await call({ - query: { - uri: 'ri', - }, - }); - - expect(actual.allCount).toBe(3); - }); - - test('escape', async () => { - await insert([ - defaultData('001', { uri: 'uri001' }), - ]); - - const actual = await call({ - query: { - uri: '%', - }, - }); - - expect(actual.allCount).toBe(0); - }); - }); - - describe('publicUrl', () => { - test('single', async () => { - await insert([ - defaultData('001', { publicUrl: 'publicUrl001' }), - defaultData('002', { publicUrl: 'publicUrl002' }), - defaultData('003', { publicUrl: 'publicUrl003' }), - ]); - - const actual = await call({ - query: { - publicUrl: 'publicUrl002', - }, - }); - - expect(actual.allCount).toBe(1); - expect(actual.emojis[0].name).toBe('emoji002'); - }); - - test('multi', async () => { - await insert([ - defaultData('001', { publicUrl: 'publicUrl001' }), - defaultData('002', { publicUrl: 'publicUrl002' }), - defaultData('003', { publicUrl: 'publicUrl003' }), - ]); - - const actual = await call({ - query: { - publicUrl: 'publicUrl001 publicUrl003', - }, - }); - - expect(actual.allCount).toBe(2); - expect(actual.emojis[0].name).toBe('emoji001'); - expect(actual.emojis[1].name).toBe('emoji003'); - }); - - test('keyword', async () => { - await insert([ - defaultData('001', { publicUrl: 'publicUrl001' }), - defaultData('002', { publicUrl: 'publicUrl002' }), - defaultData('003', { publicUrl: 'publicUrl003' }), - ]); - - const actual = await call({ - query: { - publicUrl: 'Url', - }, - }); - - expect(actual.allCount).toBe(3); - }); - - test('escape', async () => { - await insert([ - defaultData('001', { publicUrl: 'publicUrl001' }), - ]); - - const actual = await call({ - query: { - publicUrl: '%', - }, - }); - - expect(actual.allCount).toBe(0); - }); - }); - - describe('type', () => { - test('single', async () => { - await insert([ - defaultData('001', { type: 'type001' }), - defaultData('002', { type: 'type002' }), - defaultData('003', { type: 'type003' }), - ]); - - const actual = await call({ - query: { - type: 'type002', - }, - }); - - expect(actual.allCount).toBe(1); - expect(actual.emojis[0].name).toBe('emoji002'); - }); - - test('multi', async () => { - await insert([ - defaultData('001', { type: 'type001' }), - defaultData('002', { type: 'type002' }), - defaultData('003', { type: 'type003' }), - ]); - - const actual = await call({ - query: { - type: 'type001 type003', - }, - }); - - expect(actual.allCount).toBe(2); - expect(actual.emojis[0].name).toBe('emoji001'); - expect(actual.emojis[1].name).toBe('emoji003'); - }); - - test('keyword', async () => { - await insert([ - defaultData('001', { type: 'type001' }), - defaultData('002', { type: 'type002' }), - defaultData('003', { type: 'type003' }), - ]); - - const actual = await call({ - query: { - type: 'pe', - }, - }); - - expect(actual.allCount).toBe(3); - }); - - test('escape', async () => { - await insert([ - defaultData('001', { type: 'type001' }), - ]); - - const actual = await call({ - query: { - type: '%', - }, - }); - - expect(actual.allCount).toBe(0); - }); - }); - - describe('aliases', () => { - test('single', async () => { - await insert([ - defaultData('001', { aliases: ['alias001', 'alias002'] }), - defaultData('002', { aliases: ['alias002'] }), - defaultData('003', { aliases: ['alias003'] }), - ]); - - const actual = await call({ - query: { - aliases: 'alias002', - }, - }); - - expect(actual.allCount).toBe(2); - expect(actual.emojis[0].name).toBe('emoji001'); - expect(actual.emojis[1].name).toBe('emoji002'); - }); - - test('multi', async () => { - await insert([ - defaultData('001', { aliases: ['alias001', 'alias002'] }), - defaultData('002', { aliases: ['alias002', 'alias004'] }), - defaultData('003', { aliases: ['alias003'] }), - defaultData('004', { aliases: ['alias004'] }), - ]); - - const actual = await call({ - query: { - aliases: 'alias001 alias004', - }, - }); - - expect(actual.allCount).toBe(3); - expect(actual.emojis[0].name).toBe('emoji001'); - expect(actual.emojis[1].name).toBe('emoji002'); - expect(actual.emojis[2].name).toBe('emoji004'); - }); - - test('keyword', async () => { - await insert([ - defaultData('001', { aliases: ['alias001', 'alias002'] }), - defaultData('002', { aliases: ['alias002', 'alias004'] }), - defaultData('003', { aliases: ['alias003'] }), - defaultData('004', { aliases: ['alias004'] }), - ]); - - const actual = await call({ - query: { - aliases: 'ias', - }, - }); - - expect(actual.allCount).toBe(4); - }); - - test('escape', async () => { - await insert([ - defaultData('001', { aliases: ['alias001', 'alias002'] }), - ]); - - const actual = await call({ - query: { - aliases: '%', - }, - }); - - expect(actual.allCount).toBe(0); - }); - }); - - describe('category', () => { - test('single', async () => { - await insert([ - defaultData('001', { category: 'category001' }), - defaultData('002', { category: 'category002' }), - defaultData('003', { category: 'category003' }), - ]); - - const actual = await call({ - query: { - category: 'category002', - }, - }); - - expect(actual.allCount).toBe(1); - expect(actual.emojis[0].name).toBe('emoji002'); - }); - - test('multi', async () => { - await insert([ - defaultData('001', { category: 'category001' }), - defaultData('002', { category: 'category002' }), - defaultData('003', { category: 'category003' }), - ]); - - const actual = await call({ - query: { - category: 'category001 category003', - }, - }); - - expect(actual.allCount).toBe(2); - expect(actual.emojis[0].name).toBe('emoji001'); - expect(actual.emojis[1].name).toBe('emoji003'); - }); - - test('keyword', async () => { - await insert([ - defaultData('001', { category: 'category001' }), - defaultData('002', { category: 'category002' }), - defaultData('003', { category: 'category003' }), - ]); - - const actual = await call({ - query: { - category: 'egory', - }, - }); - - expect(actual.allCount).toBe(3); - }); - - test('escape', async () => { - await insert([ - defaultData('001', { category: 'category001' }), - ]); - - const actual = await call({ - query: { - category: '%', - }, - }); - - expect(actual.allCount).toBe(0); - }); - }); - - describe('license', () => { - test('single', async () => { - await insert([ - defaultData('001', { license: 'license001' }), - defaultData('002', { license: 'license002' }), - defaultData('003', { license: 'license003' }), - ]); - - const actual = await call({ - query: { - license: 'license002', - }, - }); - - expect(actual.allCount).toBe(1); - expect(actual.emojis[0].name).toBe('emoji002'); - }); - - test('multi', async () => { - await insert([ - defaultData('001', { license: 'license001' }), - defaultData('002', { license: 'license002' }), - defaultData('003', { license: 'license003' }), - ]); - - const actual = await call({ - query: { - license: 'license001 license003', - }, - }); - - expect(actual.allCount).toBe(2); - expect(actual.emojis[0].name).toBe('emoji001'); - expect(actual.emojis[1].name).toBe('emoji003'); - }); - - test('keyword', async () => { - await insert([ - defaultData('001', { license: 'license001' }), - defaultData('002', { license: 'license002' }), - defaultData('003', { license: 'license003' }), - ]); - - const actual = await call({ - query: { - license: 'cense', - }, - }); - - expect(actual.allCount).toBe(3); - }); - - test('escape', async () => { - await insert([ - defaultData('001', { license: 'license001' }), - ]); - - const actual = await call({ - query: { - license: '%', - }, - }); - - expect(actual.allCount).toBe(0); - }); - }); - - describe('isSensitive', () => { - test('true', async () => { - await insert([ - defaultData('001', { isSensitive: true }), - defaultData('002', { isSensitive: false }), - defaultData('003', { isSensitive: true }), - ]); - - const actual = await call({ - query: { - isSensitive: true, - }, - }); - - expect(actual.allCount).toBe(2); - expect(actual.emojis[0].name).toBe('emoji001'); - expect(actual.emojis[1].name).toBe('emoji003'); - }); - - test('false', async () => { - await insert([ - defaultData('001', { isSensitive: true }), - defaultData('002', { isSensitive: false }), - defaultData('003', { isSensitive: true }), - ]); - - const actual = await call({ - query: { - isSensitive: false, - }, - }); - - expect(actual.allCount).toBe(1); - expect(actual.emojis[0].name).toBe('emoji002'); - }); - - test('null', async () => { - await insert([ - defaultData('001', { isSensitive: true }), - defaultData('002', { isSensitive: false }), - defaultData('003', { isSensitive: true }), - ]); - - const actual = await call({ - query: {}, - }); - - expect(actual.allCount).toBe(3); - }); - }); - - describe('localOnly', () => { - test('true', async () => { - await insert([ - defaultData('001', { localOnly: true }), - defaultData('002', { localOnly: false }), - defaultData('003', { localOnly: true }), - ]); - - const actual = await call({ - query: { - localOnly: true, - }, - }); - - expect(actual.allCount).toBe(2); - expect(actual.emojis[0].name).toBe('emoji001'); - expect(actual.emojis[1].name).toBe('emoji003'); - }); - - test('false', async () => { - await insert([ - defaultData('001', { localOnly: true }), - defaultData('002', { localOnly: false }), - defaultData('003', { localOnly: true }), - ]); - - const actual = await call({ - query: { - localOnly: false, - }, - }); - - expect(actual.allCount).toBe(1); - expect(actual.emojis[0].name).toBe('emoji002'); - }); - - test('null', async () => { - await insert([ - defaultData('001', { localOnly: true }), - defaultData('002', { localOnly: false }), - defaultData('003', { localOnly: true }), - ]); - - const actual = await call({ - query: {}, - }); - - expect(actual.allCount).toBe(3); - }); - }); - - describe('roleId', () => { - test('single', async () => { - await insert([ - defaultData('001', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role001'] }), - defaultData('002', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role002'] }), - defaultData('003', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role003'] }), - ]); - - const actual = await call({ - query: { - roleIds: ['role002'], - }, - }); - - expect(actual.allCount).toBe(1); - expect(actual.emojis[0].name).toBe('emoji002'); - }); - - test('multi', async () => { - await insert([ - defaultData('001', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role001'] }), - defaultData('002', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role002', 'role003'] }), - defaultData('003', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role003'] }), - defaultData('004', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role004'] }), - ]); - - const actual = await call({ - query: { - roleIds: ['role001', 'role003'], - }, - }); - - expect(actual.allCount).toBe(3); - expect(actual.emojis[0].name).toBe('emoji001'); - expect(actual.emojis[1].name).toBe('emoji002'); - expect(actual.emojis[2].name).toBe('emoji003'); - }); - }); - }); - }); -}); diff --git a/packages/backend/test/unit/DriveService.ts b/packages/backend/test/unit/DriveService.ts index 964c65ccaa..4065665579 100644 --- a/packages/backend/test/unit/DriveService.ts +++ b/packages/backend/test/unit/DriveService.ts @@ -1,18 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import { Test } from '@nestjs/testing'; -import { - DeleteObjectCommand, - DeleteObjectCommandOutput, - InvalidObjectState, - NoSuchKey, - S3Client, -} from '@aws-sdk/client-s3'; +import { DeleteObjectCommandOutput, DeleteObjectCommand, NoSuchKey, InvalidObjectState, S3Client } from '@aws-sdk/client-s3'; import { mockClient } from 'aws-sdk-client-mock'; import { GlobalModule } from '@/GlobalModule.js'; import { DriveService } from '@/core/DriveService.js'; @@ -45,7 +34,7 @@ describe('DriveService', () => { test('delete a file', async () => { s3Mock.on(DeleteObjectCommand) .resolves({} as DeleteObjectCommandOutput); - + await driveService.deleteObjectStorageFile('peace of the world'); }); diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts deleted file mode 100644 index 1e3605aafc..0000000000 --- a/packages/backend/test/unit/FetchInstanceMetadataService.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -process.env.NODE_ENV = 'test'; - -import { jest } from '@jest/globals'; -import { Test } from '@nestjs/testing'; -import { Redis } from 'ioredis'; -import type { TestingModule } from '@nestjs/testing'; -import { GlobalModule } from '@/GlobalModule.js'; -import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; -import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; -import { HttpRequestService } from '@/core/HttpRequestService.js'; -import { LoggerService } from '@/core/LoggerService.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import { IdService } from '@/core/IdService.js'; -import { DI } from '@/di-symbols.js'; - -function mockRedis() { - const hash = {} as any; - const set = jest.fn((key: string, value) => { - const ret = hash[key]; - hash[key] = value; - return ret; - }); - return set; -} - -describe('FetchInstanceMetadataService', () => { - let app: TestingModule; - let fetchInstanceMetadataService: jest.Mocked; - let federatedInstanceService: jest.Mocked; - let httpRequestService: jest.Mocked; - let redisClient: jest.Mocked; - - beforeEach(async () => { - app = await Test - .createTestingModule({ - imports: [ - GlobalModule, - ], - providers: [ - FetchInstanceMetadataService, - LoggerService, - UtilityService, - IdService, - ], - }) - .useMocker((token) => { - if (token === HttpRequestService) { - return { getJson: jest.fn(), getHtml: jest.fn(), send: jest.fn() }; - } else if (token === FederatedInstanceService) { - return { fetchOrRegister: jest.fn() }; - } else if (token === DI.redis) { - return mockRedis; - } - return null; - }) - .compile(); - - app.enableShutdownHooks(); - - fetchInstanceMetadataService = app.get(FetchInstanceMetadataService) as jest.Mocked; - federatedInstanceService = app.get(FederatedInstanceService) as jest.Mocked; - redisClient = app.get(DI.redis) as jest.Mocked; - httpRequestService = app.get(HttpRequestService) as jest.Mocked; - }); - - afterEach(async () => { - await app.close(); - }); - - test('Lock and update', async () => { - redisClient.set = mockRedis(); - const now = Date.now(); - federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any); - httpRequestService.getJson.mockImplementation(() => { throw Error(); }); - const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); - const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); - - await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); - expect(tryLockSpy).toHaveBeenCalledTimes(1); - expect(unlockSpy).toHaveBeenCalledTimes(1); - expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(1); - expect(httpRequestService.getJson).toHaveBeenCalled(); - }); - - test('Lock and don\'t update', async () => { - redisClient.set = mockRedis(); - const now = Date.now(); - federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any); - httpRequestService.getJson.mockImplementation(() => { throw Error(); }); - const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); - const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); - - await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); - expect(tryLockSpy).toHaveBeenCalledTimes(1); - expect(unlockSpy).toHaveBeenCalledTimes(1); - expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(1); - expect(httpRequestService.getJson).toHaveBeenCalledTimes(0); - }); - - test('Do nothing when lock not acquired', async () => { - redisClient.set = mockRedis(); - const now = Date.now(); - federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any); - httpRequestService.getJson.mockImplementation(() => { throw Error(); }); - await fetchInstanceMetadataService.tryLock('example.com'); - const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); - const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); - - await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); - expect(tryLockSpy).toHaveBeenCalledTimes(1); - expect(unlockSpy).toHaveBeenCalledTimes(0); - expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(0); - expect(httpRequestService.getJson).toHaveBeenCalledTimes(0); - }); - - test('Do when lock not acquired but forced', async () => { - redisClient.set = mockRedis(); - const now = Date.now(); - federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any); - httpRequestService.getJson.mockImplementation(() => { throw Error(); }); - await fetchInstanceMetadataService.tryLock('example.com'); - const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); - const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); - - await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true); - expect(tryLockSpy).toHaveBeenCalledTimes(0); - expect(unlockSpy).toHaveBeenCalledTimes(1); - expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(0); - expect(httpRequestService.getJson).toHaveBeenCalled(); - }); -}); diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts index 29bd03a201..f378184c74 100644 --- a/packages/backend/test/unit/FileInfoService.ts +++ b/packages/backend/test/unit/FileInfoService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; @@ -10,13 +5,12 @@ import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; -import { afterAll, beforeAll, describe, test } from '@jest/globals'; import { GlobalModule } from '@/GlobalModule.js'; -import { FileInfo, FileInfoService } from '@/core/FileInfoService.js'; +import { FileInfoService } from '@/core/FileInfoService.js'; //import { DI } from '@/di-symbols.js'; import { AiService } from '@/core/AiService.js'; -import { LoggerService } from '@/core/LoggerService.js'; import type { TestingModule } from '@nestjs/testing'; +import { describe, beforeAll, afterAll, test } from '@jest/globals'; import type { MockFunctionMetadata } from 'jest-mock'; const _filename = fileURLToPath(import.meta.url); @@ -28,15 +22,6 @@ const moduleMocker = new ModuleMocker(global); describe('FileInfoService', () => { let app: TestingModule; let fileInfoService: FileInfoService; - const strip = (fileInfo: FileInfo): Omit, 'warnings' | 'blurhash' | 'sensitive' | 'porn'> => { - const fi: Partial = fileInfo; - delete fi.warnings; - delete fi.sensitive; - delete fi.blurhash; - delete fi.porn; - - return fi; - } beforeAll(async () => { app = await Test.createTestingModule({ @@ -45,7 +30,6 @@ describe('FileInfoService', () => { ], providers: [ AiService, - LoggerService, FileInfoService, ], }) @@ -72,7 +56,11 @@ describe('FileInfoService', () => { test('Empty file', async () => { const path = `${resources}/emptyfile`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 0, md5: 'd41d8cd98f00b204e9800998ecf8427e', @@ -88,24 +76,32 @@ describe('FileInfoService', () => { describe('IMAGE', () => { test('Generic JPEG', async () => { - const path = `${resources}/192.jpg`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const path = `${resources}/Lenna.jpg`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { - size: 5131, - md5: '8c9ed0677dd2b8f9f7472c3af247e5e3', + size: 25360, + md5: '091b3f259662aa31e2ffef4519951168', type: { mime: 'image/jpeg', ext: 'jpg', }, - width: 192, - height: 192, + width: 512, + height: 512, orientation: undefined, }); }); - + test('Generic APNG', async () => { const path = `${resources}/anime.png`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 1868, md5: '08189c607bea3b952704676bb3c979e0', @@ -118,10 +114,14 @@ describe('FileInfoService', () => { orientation: undefined, }); }); - + test('Generic AGIF', async () => { const path = `${resources}/anime.gif`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 2248, md5: '32c47a11555675d9267aee1a86571e7e', @@ -134,10 +134,14 @@ describe('FileInfoService', () => { orientation: undefined, }); }); - + test('PNG with alpha', async () => { const path = `${resources}/with-alpha.png`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 3772, md5: 'f73535c3e1e27508885b69b10cf6e991', @@ -150,10 +154,14 @@ describe('FileInfoService', () => { orientation: undefined, }); }); - + test('Generic SVG', async () => { const path = `${resources}/image.svg`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 505, md5: 'b6f52b4b021e7b92cdd04509c7267965', @@ -166,11 +174,15 @@ describe('FileInfoService', () => { orientation: undefined, }); }); - + test('SVG with XML definition', async () => { // https://github.com/misskey-dev/misskey/issues/4413 const path = `${resources}/with-xml-def.svg`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 544, md5: '4b7a346cde9ccbeb267e812567e33397', @@ -183,10 +195,14 @@ describe('FileInfoService', () => { orientation: undefined, }); }); - + test('Dimension limit', async () => { const path = `${resources}/25000x25000.png`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 75933, md5: '268c5dde99e17cf8fe09f1ab3f97df56', @@ -199,10 +215,14 @@ describe('FileInfoService', () => { orientation: undefined, }); }); - + test('Rotate JPEG', async () => { const path = `${resources}/rotate.jpg`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 12624, md5: '68d5b2d8d1d1acbbce99203e3ec3857e', @@ -220,7 +240,11 @@ describe('FileInfoService', () => { describe('AUDIO', () => { test('MP3', async () => { const path = `${resources}/kick_gaba7.mp3`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; delete info.width; delete info.height; delete info.orientation; @@ -233,10 +257,14 @@ describe('FileInfoService', () => { }, }); }); - + test('WAV', async () => { const path = `${resources}/kick_gaba7.wav`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; delete info.width; delete info.height; delete info.orientation; @@ -249,10 +277,14 @@ describe('FileInfoService', () => { }, }); }); - + test('AAC', async () => { const path = `${resources}/kick_gaba7.aac`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; delete info.width; delete info.height; delete info.orientation; @@ -265,10 +297,14 @@ describe('FileInfoService', () => { }, }); }); - + test('FLAC', async () => { const path = `${resources}/kick_gaba7.flac`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; delete info.width; delete info.height; delete info.orientation; @@ -281,37 +317,28 @@ describe('FileInfoService', () => { }, }); }); - - test('MPEG-4 AUDIO (M4A)', async () => { - const path = `${resources}/kick_gaba7.m4a`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); - delete info.width; - delete info.height; - delete info.orientation; - assert.deepStrictEqual(info, { - size: 9817, - md5: '74c9279a4abe98789565f1dc1a541a42', - type: { - mime: 'audio/mp4', - ext: 'm4a', - }, - }); - }); - + + /* + * video/webmとして検出されてしまう test('WEBM AUDIO', async () => { const path = `${resources}/kick_gaba7.webm`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; delete info.width; delete info.height; delete info.orientation; assert.deepStrictEqual(info, { size: 8879, - md5: '53bc1adcb6acbbda67ff9bd484896438', + md5: '3350083dec312419cfdc06c16413aca7', type: { mime: 'audio/webm', ext: 'webm', }, }); }); + */ }); }); diff --git a/packages/backend/test/unit/FlashService.ts b/packages/backend/test/unit/FlashService.ts deleted file mode 100644 index 9e4bad147b..0000000000 --- a/packages/backend/test/unit/FlashService.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Test, TestingModule } from '@nestjs/testing'; -import { FlashService } from '@/core/FlashService.js'; -import { IdService } from '@/core/IdService.js'; -import { FlashsRepository, MiFlash, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { GlobalModule } from '@/GlobalModule.js'; - -describe('FlashService', () => { - let app: TestingModule; - let service: FlashService; - - // -------------------------------------------------------------------------------------- - - let flashsRepository: FlashsRepository; - let usersRepository: UsersRepository; - let userProfilesRepository: UserProfilesRepository; - let idService: IdService; - - // -------------------------------------------------------------------------------------- - - let root: MiUser; - let alice: MiUser; - let bob: MiUser; - - // -------------------------------------------------------------------------------------- - - async function createFlash(data: Partial) { - return flashsRepository.insert({ - id: idService.gen(), - updatedAt: new Date(), - userId: root.id, - title: 'title', - summary: 'summary', - script: 'script', - permissions: [], - likedCount: 0, - ...data, - }).then(x => flashsRepository.findOneByOrFail(x.identifiers[0])); - } - - async function createUser(data: Partial = {}) { - const user = await usersRepository - .insert({ - id: idService.gen(), - ...data, - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - - await userProfilesRepository.insert({ - userId: user.id, - }); - - return user; - } - - // -------------------------------------------------------------------------------------- - - beforeEach(async () => { - app = await Test.createTestingModule({ - imports: [ - GlobalModule, - ], - providers: [ - FlashService, - IdService, - ], - }).compile(); - - service = app.get(FlashService); - - flashsRepository = app.get(DI.flashsRepository); - usersRepository = app.get(DI.usersRepository); - userProfilesRepository = app.get(DI.userProfilesRepository); - idService = app.get(IdService); - - root = await createUser({ username: 'root', usernameLower: 'root' }); - alice = await createUser({ username: 'alice', usernameLower: 'alice' }); - bob = await createUser({ username: 'bob', usernameLower: 'bob' }); - }); - - afterEach(async () => { - await usersRepository.createQueryBuilder().delete().execute(); - await userProfilesRepository.createQueryBuilder().delete().execute(); - await flashsRepository.createQueryBuilder().delete().execute(); - }); - - afterAll(async () => { - await app.close(); - }); - - // -------------------------------------------------------------------------------------- - - describe('featured', () => { - test('should return featured flashes', async () => { - const flash1 = await createFlash({ likedCount: 1 }); - const flash2 = await createFlash({ likedCount: 2 }); - const flash3 = await createFlash({ likedCount: 3 }); - - const result = await service.featured({ - offset: 0, - limit: 10, - }); - - expect(result).toEqual([flash3, flash2, flash1]); - }); - - test('should return featured flashes public visibility only', async () => { - const flash1 = await createFlash({ likedCount: 1, visibility: 'public' }); - const flash2 = await createFlash({ likedCount: 2, visibility: 'public' }); - const flash3 = await createFlash({ likedCount: 3, visibility: 'private' }); - - const result = await service.featured({ - offset: 0, - limit: 10, - }); - - expect(result).toEqual([flash2, flash1]); - }); - - test('should return featured flashes with offset', async () => { - const flash1 = await createFlash({ likedCount: 1 }); - const flash2 = await createFlash({ likedCount: 2 }); - const flash3 = await createFlash({ likedCount: 3 }); - - const result = await service.featured({ - offset: 1, - limit: 10, - }); - - expect(result).toEqual([flash2, flash1]); - }); - - test('should return featured flashes with limit', async () => { - const flash1 = await createFlash({ likedCount: 1 }); - const flash2 = await createFlash({ likedCount: 2 }); - const flash3 = await createFlash({ likedCount: 3 }); - - const result = await service.featured({ - offset: 0, - limit: 2, - }); - - expect(result).toEqual([flash3, flash2]); - }); - }); -}); diff --git a/packages/backend/test/unit/MetaService.ts b/packages/backend/test/unit/MetaService.ts index 19c98eab3d..9efd8bbe70 100644 --- a/packages/backend/test/unit/MetaService.ts +++ b/packages/backend/test/unit/MetaService.ts @@ -1,18 +1,15 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import { jest } from '@jest/globals'; +import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; import { GlobalModule } from '@/GlobalModule.js'; +import type { MetasRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { CoreModule } from '@/core/CoreModule.js'; -import type { TestingModule } from '@nestjs/testing'; import type { DataSource } from 'typeorm'; +import type { TestingModule } from '@nestjs/testing'; describe('MetaService', () => { let app: TestingModule; diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts index 7350da3cae..5496738778 100644 --- a/packages/backend/test/unit/MfmService.ts +++ b/packages/backend/test/unit/MfmService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as assert from 'assert'; import * as mfm from 'mfm-js'; import { Test } from '@nestjs/testing'; @@ -24,25 +19,13 @@ describe('MfmService', () => { describe('toHtml', () => { test('br', () => { const input = 'foo\nbar\nbaz'; - const output = '

foo
bar
baz

'; + const output = '

foo
bar
baz

'; assert.equal(mfmService.toHtml(mfm.parse(input)), output); }); test('br alt', () => { const input = 'foo\r\nbar\rbaz'; - const output = '

foo
bar
baz

'; - assert.equal(mfmService.toHtml(mfm.parse(input)), output); - }); - - test('Do not generate unnecessary span', () => { - const input = 'foo $[tada bar]'; - const output = '

foo bar

'; - assert.equal(mfmService.toHtml(mfm.parse(input)), output); - }); - - test('escape', () => { - const input = '```\n

Hello, world!

\n```'; - const output = '

<p>Hello, world!</p>

'; + const output = '

foo
bar
baz

'; assert.equal(mfmService.toHtml(mfm.parse(input)), output); }); }); @@ -108,24 +91,6 @@ describe('MfmService', () => { assert.deepStrictEqual(mfmService.fromHtml('

a d

'), 'a d'); }); - test('ruby', () => { - assert.deepStrictEqual(mfmService.fromHtml('

a Misskey(ミスキー) b

'), 'a $[ruby Misskey ミスキー] b'); - assert.deepStrictEqual(mfmService.fromHtml('

a Misskey(ミスキー)Misskey(ミスキー) b

'), 'a $[ruby Misskey ミスキー]$[ruby Misskey ミスキー] b'); - }); - - test('ruby with spaces', () => { - assert.deepStrictEqual(mfmService.fromHtml('

a Miss key(ミスキー) b c

'), 'a Miss key(ミスキー) b c'); - assert.deepStrictEqual(mfmService.fromHtml('

a Misskey(ミス キー) b c

'), 'a Misskey(ミス キー) b c'); - assert.deepStrictEqual( - mfmService.fromHtml('

a Misskey(ミスキー)Misskey(ミス キー)Misskey(ミスキー) b

'), - 'a Misskey(ミスキー)Misskey(ミス キー)Misskey(ミスキー) b' - ); - }); - - test('ruby with other inline tags', () => { - assert.deepStrictEqual(mfmService.fromHtml('

a Misskey(ミスキー) b c

'), 'a **Misskey**(ミスキー) b c'); - }); - test('mention', () => { assert.deepStrictEqual(mfmService.fromHtml('

a @user d

'), 'a @user@example.com d'); }); diff --git a/packages/backend/test/unit/NoteCreateService.ts b/packages/backend/test/unit/NoteCreateService.ts deleted file mode 100644 index f2d4c8ffbb..0000000000 --- a/packages/backend/test/unit/NoteCreateService.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Test } from '@nestjs/testing'; - -import { CoreModule } from '@/core/CoreModule.js'; -import { NoteCreateService } from '@/core/NoteCreateService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { MiNote } from '@/models/Note.js'; -import { IPoll } from '@/models/Poll.js'; -import { MiDriveFile } from '@/models/DriveFile.js'; - -describe('NoteCreateService', () => { - let noteCreateService: NoteCreateService; - - beforeAll(async () => { - const app = await Test.createTestingModule({ - imports: [GlobalModule, CoreModule], - }).compile(); - noteCreateService = app.get(NoteCreateService); - }); - - describe('is-renote', () => { - const base: MiNote = { - id: 'some-note-id', - replyId: null, - reply: null, - renoteId: null, - renote: null, - threadId: null, - text: null, - name: null, - cw: null, - userId: 'some-user-id', - user: null, - localOnly: false, - reactionAcceptance: null, - renoteCount: 0, - repliesCount: 0, - clippedCount: 0, - reactions: {}, - visibility: 'public', - uri: null, - url: null, - fileIds: [], - attachedFileTypes: [], - visibleUserIds: [], - mentions: [], - mentionedRemoteUsers: '', - reactionAndUserPairCache: [], - emojis: [], - tags: [], - hasPoll: false, - channelId: null, - channel: null, - userHost: null, - replyUserId: null, - replyUserHost: null, - renoteUserId: null, - renoteUserHost: null, - }; - - const poll: IPoll = { - choices: ['kinoko', 'takenoko'], - multiple: false, - expiresAt: null, - }; - - const file: MiDriveFile = { - id: 'some-file-id', - userId: null, - user: null, - userHost: null, - md5: '', - name: '', - type: '', - size: 0, - comment: null, - blurhash: null, - properties: {}, - storedInternal: false, - url: '', - thumbnailUrl: null, - webpublicUrl: null, - webpublicType: null, - accessKey: null, - thumbnailAccessKey: null, - webpublicAccessKey: null, - uri: null, - src: null, - folderId: null, - folder: null, - isSensitive: false, - maybeSensitive: false, - maybePorn: false, - isLink: false, - requestHeaders: null, - requestIp: null, - }; - - test('note without renote should not be Renote', () => { - const note = { renote: null }; - expect(noteCreateService['isRenote'](note)).toBe(false); - }); - - test('note with renote should be Renote and not be Quote', () => { - const note = { renote: base }; - expect(noteCreateService['isRenote'](note)).toBe(true); - expect(noteCreateService['isQuote'](note)).toBe(false); - }); - - test('note with renote and text should be Quote', () => { - const note = { renote: base, text: 'some-text' }; - expect(noteCreateService['isRenote'](note)).toBe(true); - expect(noteCreateService['isQuote'](note)).toBe(true); - }); - - test('note with renote and cw should be Quote', () => { - const note = { renote: base, cw: 'some-cw' }; - expect(noteCreateService['isRenote'](note)).toBe(true); - expect(noteCreateService['isQuote'](note)).toBe(true); - }); - - test('note with renote and reply should be Quote', () => { - const note = { renote: base, reply: { ...base, id: 'another-note-id' } }; - expect(noteCreateService['isRenote'](note)).toBe(true); - expect(noteCreateService['isQuote'](note)).toBe(true); - }); - - test('note with renote and poll should be Quote', () => { - const note = { renote: base, poll }; - expect(noteCreateService['isRenote'](note)).toBe(true); - expect(noteCreateService['isQuote'](note)).toBe(true); - }); - - test('note with renote and non-empty files should be Quote', () => { - const note = { renote: base, files: [file] }; - expect(noteCreateService['isRenote'](note)).toBe(true); - expect(noteCreateService['isQuote'](note)).toBe(true); - }); - }); -}); diff --git a/packages/backend/test/unit/ReactionService.ts b/packages/backend/test/unit/ReactionService.ts index 1957f4544c..aa68f4117d 100644 --- a/packages/backend/test/unit/ReactionService.ts +++ b/packages/backend/test/unit/ReactionService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as assert from 'assert'; import { Test } from '@nestjs/testing'; @@ -90,45 +85,4 @@ describe('ReactionService', () => { assert.strictEqual(await reactionService.normalize('unknown'), '❤'); }); }); - - describe('convertLegacyReactions', () => { - test('空の入力に対しては何もしない', () => { - const input = {}; - assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input); - }); - - test('Unicode絵文字リアクションを変換してしまわない', () => { - const input = { '👍': 1, '🍮': 2 }; - assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input); - }); - - test('カスタム絵文字リアクションを変換してしまわない', () => { - const input = { ':like@.:': 1, ':pudding@example.tld:': 2 }; - assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input); - }); - - test('文字列によるレガシーなリアクションを変換する', () => { - const input = { 'like': 1, 'pudding': 2 }; - const output = { '👍': 1, '🍮': 2 }; - assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); - }); - - test('host部分が省略されたレガシーなカスタム絵文字リアクションを変換する', () => { - const input = { ':custom_emoji:': 1 }; - const output = { ':custom_emoji@.:': 1 }; - assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); - }); - - test('「0個のリアクション」情報を削除する', () => { - const input = { 'angry': 0 }; - const output = {}; - assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); - }); - - test('host部分の有無によりデコードすると同じ表記になるカスタム絵文字リアクションの個数情報を正しく足し合わせる', () => { - const input = { ':custom_emoji:': 1, ':custom_emoji@.:': 2 }; - const output = { ':custom_emoji@.:': 3 }; - assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); - }); - }); }); diff --git a/packages/backend/test/unit/RelayService.ts b/packages/backend/test/unit/RelayService.ts index 074430dd31..c2280142a6 100644 --- a/packages/backend/test/unit/RelayService.ts +++ b/packages/backend/test/unit/RelayService.ts @@ -1,23 +1,19 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import { jest } from '@jest/globals'; -import { Test } from '@nestjs/testing'; import { ModuleMocker } from 'jest-mock'; +import { Test } from '@nestjs/testing'; +import { GlobalModule } from '@/GlobalModule.js'; +import { RelayService } from '@/core/RelayService.js'; +import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; +import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { QueueService } from '@/core/QueueService.js'; +import { IdService } from '@/core/IdService.js'; +import type { RelaysRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import type { TestingModule } from '@nestjs/testing'; import type { MockFunctionMetadata } from 'jest-mock'; -import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { IdService } from '@/core/IdService.js'; -import { QueueService } from '@/core/QueueService.js'; -import { RelayService } from '@/core/RelayService.js'; -import { SystemAccountService } from '@/core/SystemAccountService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { UtilityService } from '@/core/UtilityService.js'; const moduleMocker = new ModuleMocker(global); @@ -25,6 +21,8 @@ describe('RelayService', () => { let app: TestingModule; let relayService: RelayService; let queueService: jest.Mocked; + let relaysRepository: RelaysRepository; + let userEntityService: UserEntityService; beforeAll(async () => { app = await Test.createTestingModule({ @@ -33,11 +31,10 @@ describe('RelayService', () => { ], providers: [ IdService, + CreateSystemUserService, ApRendererService, RelayService, UserEntityService, - SystemAccountService, - UtilityService, ], }) .useMocker((token) => { @@ -56,13 +53,15 @@ describe('RelayService', () => { relayService = app.get(RelayService); queueService = app.get(QueueService) as jest.Mocked; + relaysRepository = app.get(DI.relaysRepository); + userEntityService = app.get(UserEntityService); }); afterAll(async () => { await app.close(); }); - test('addRelay', async () => { + test('addRelay', async () => { const result = await relayService.addRelay('https://example.com'); expect(result.inbox).toBe('https://example.com'); @@ -73,7 +72,7 @@ describe('RelayService', () => { //expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor'); }); - test('listRelay', async () => { + test('listRelay', async () => { const result = await relayService.listRelay(); expect(result.length).toBe(1); @@ -81,13 +80,12 @@ describe('RelayService', () => { expect(result[0].status).toBe('requesting'); }); - test('removeRelay: succ', async () => { + test('removeRelay: succ', async () => { await relayService.removeRelay('https://example.com'); expect(queueService.deliver).toHaveBeenCalled(); expect(queueService.deliver.mock.lastCall![1]?.type).toBe('Undo'); - expect(typeof queueService.deliver.mock.lastCall![1]?.object).toBe('object'); - expect((queueService.deliver.mock.lastCall![1]?.object as any).type).toBe('Follow'); + expect(queueService.deliver.mock.lastCall![1]?.object.type).toBe('Follow'); expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com'); //expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor'); @@ -95,7 +93,7 @@ describe('RelayService', () => { expect(list.length).toBe(0); }); - test('removeRelay: fail', async () => { + test('removeRelay: fail', async () => { await expect(relayService.removeRelay('https://x.example.com')) .rejects.toThrow('relay not found'); }); diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 306836ea43..6979f23e0c 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -1,38 +1,22 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; -import { setTimeout } from 'node:timers/promises'; import { jest } from '@jest/globals'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; import * as lolex from '@sinonjs/fake-timers'; -import type { TestingModule } from '@nestjs/testing'; -import type { MockFunctionMetadata } from 'jest-mock'; import { GlobalModule } from '@/GlobalModule.js'; import { RoleService } from '@/core/RoleService.js'; -import { - MiMeta, - MiRole, - MiRoleAssignment, - MiUser, - RoleAssignmentsRepository, - RolesRepository, - UsersRepository, -} from '@/models/_.js'; +import type { Role, RolesRepository, RoleAssignmentsRepository, UsersRepository, User } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; -import { genAidx } from '@/misc/id/aidx.js'; +import { genAid } from '@/misc/id/aid.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; -import { NotificationService } from '@/core/NotificationService.js'; -import { RoleCondFormulaValue } from '@/models/Role.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { sleep } from '../utils.js'; +import type { TestingModule } from '@nestjs/testing'; +import type { MockFunctionMetadata } from 'jest-mock'; const moduleMocker = new ModuleMocker(global); @@ -42,64 +26,31 @@ describe('RoleService', () => { let usersRepository: UsersRepository; let rolesRepository: RolesRepository; let roleAssignmentsRepository: RoleAssignmentsRepository; - let meta: jest.Mocked; - let notificationService: jest.Mocked; + let metaService: jest.Mocked; let clock: lolex.InstalledClock; - async function createUser(data: Partial = {}) { + function createUser(data: Partial = {}) { const un = secureRndstr(16); - const x = await usersRepository.insert({ - id: genAidx(Date.now()), + return usersRepository.insert({ + id: genAid(new Date()), + createdAt: new Date(), username: un, usernameLower: un, ...data, - }); - return await usersRepository.findOneByOrFail(x.identifiers[0]); + }) + .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); } - async function createRoot(data: Partial = {}) { - const user = await createUser(data); - meta.rootUserId = user.id; - return user; - } - - async function createRole(data: Partial = {}) { - const x = await rolesRepository.insert({ - id: genAidx(Date.now()), + function createRole(data: Partial = {}) { + return rolesRepository.insert({ + id: genAid(new Date()), + createdAt: new Date(), updatedAt: new Date(), lastUsedAt: new Date(), - name: '', description: '', ...data, - }); - return await rolesRepository.findOneByOrFail(x.identifiers[0]); - } - - function createConditionalRole(condFormula: RoleCondFormulaValue, data: Partial = {}) { - return createRole({ - name: `[conditional] ${condFormula.type}`, - target: 'conditional', - condFormula: condFormula, - ...data, - }); - } - - async function assignRole(args: Partial) { - const id = genAidx(Date.now()); - const expiresAt = new Date(); - expiresAt.setDate(expiresAt.getDate() + 1); - - await roleAssignmentsRepository.insert({ - id, - expiresAt, - ...args, - }); - - return await roleAssignmentsRepository.findOneByOrFail({ id }); - } - - function aidx() { - return genAidx(Date.now()); + }) + .then(x => rolesRepository.findOneByOrFail(x.identifiers[0])); } beforeEach(async () => { @@ -117,17 +68,6 @@ describe('RoleService', () => { CacheService, IdService, GlobalEventService, - UserEntityService, - { - provide: NotificationService, - useFactory: () => ({ - createNotification: jest.fn(), - }), - }, - { - provide: NotificationService.name, - useExisting: NotificationService, - }, ], }) .useMocker((token) => { @@ -149,20 +89,17 @@ describe('RoleService', () => { rolesRepository = app.get(DI.rolesRepository); roleAssignmentsRepository = app.get(DI.roleAssignmentsRepository); - meta = app.get(DI.meta) as jest.Mocked; - notificationService = app.get(NotificationService) as jest.Mocked; - - await roleService.onModuleInit(); + metaService = app.get(MetaService) as jest.Mocked; }); afterEach(async () => { clock.uninstall(); await Promise.all([ - app.get(DI.metasRepository).createQueryBuilder().delete().execute(), - usersRepository.createQueryBuilder().delete().execute(), - rolesRepository.createQueryBuilder().delete().execute(), - roleAssignmentsRepository.createQueryBuilder().delete().execute(), + app.get(DI.metasRepository).delete({}), + usersRepository.delete({}), + rolesRepository.delete({}), + roleAssignmentsRepository.delete({}), ]); await app.close(); @@ -171,9 +108,11 @@ describe('RoleService', () => { describe('getUserPolicies', () => { test('instance default policies', async () => { const user = await createUser(); - meta.policies = { - canManageCustomEmojis: false, - }; + metaService.fetch.mockResolvedValue({ + policies: { + canManageCustomEmojis: false, + }, + } as any); const result = await roleService.getUserPolicies(user.id); @@ -182,9 +121,11 @@ describe('RoleService', () => { test('instance default policies 2', async () => { const user = await createUser(); - meta.policies = { - canManageCustomEmojis: true, - }; + metaService.fetch.mockResolvedValue({ + policies: { + canManageCustomEmojis: true, + }, + } as any); const result = await roleService.getUserPolicies(user.id); @@ -204,9 +145,11 @@ describe('RoleService', () => { }, }); await roleService.assign(user.id, role.id); - meta.policies = { - canManageCustomEmojis: false, - }; + metaService.fetch.mockResolvedValue({ + policies: { + canManageCustomEmojis: false, + }, + } as any); const result = await roleService.getUserPolicies(user.id); @@ -237,15 +180,59 @@ describe('RoleService', () => { }); await roleService.assign(user.id, role1.id); await roleService.assign(user.id, role2.id); - meta.policies = { - driveCapacityMb: 50, - }; + 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); + }); + test('expired role', async () => { const user = await createUser(); const role = await createRole({ @@ -259,9 +246,11 @@ describe('RoleService', () => { }, }); await roleService.assign(user.id, role.id, new Date(Date.now() + (1000 * 60 * 60 * 24))); - meta.policies = { - canManageCustomEmojis: false, - }; + metaService.fetch.mockResolvedValue({ + policies: { + canManageCustomEmojis: false, + }, + } as any); const result = await roleService.getUserPolicies(user.id); expect(result.canManageCustomEmojis).toBe(true); @@ -275,688 +264,10 @@ describe('RoleService', () => { // ストリーミング経由で反映されるまでちょっと待つ clock.uninstall(); - await setTimeout(100); + await sleep(100); const resultAfter25hAgain = await roleService.getUserPolicies(user.id); expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true); }); }); - - describe('getModeratorIds', () => { - test('includeAdmins = false, includeRoot = false, excludeExpire = false', async () => { - const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ - createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(), - ]); - - const role1 = await createRole({ name: 'admin', isAdministrator: true }); - const role2 = await createRole({ name: 'moderator', isModerator: true }); - const role3 = await createRole({ name: 'normal' }); - - await Promise.all([ - assignRole({ userId: adminUser1.id, roleId: role1.id }), - assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: modeUser1.id, roleId: role2.id }), - assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: normalUser1.id, roleId: role3.id }), - assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), - ]); - - const result = await roleService.getModeratorIds({ - includeAdmins: false, - includeRoot: false, - excludeExpire: false, - }); - expect(result).toEqual([modeUser1.id, modeUser2.id]); - }); - - test('includeAdmins = false, includeRoot = false, excludeExpire = true', async () => { - const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ - createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(), - ]); - - const role1 = await createRole({ name: 'admin', isAdministrator: true }); - const role2 = await createRole({ name: 'moderator', isModerator: true }); - const role3 = await createRole({ name: 'normal' }); - - await Promise.all([ - assignRole({ userId: adminUser1.id, roleId: role1.id }), - assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: modeUser1.id, roleId: role2.id }), - assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: normalUser1.id, roleId: role3.id }), - assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), - ]); - - const result = await roleService.getModeratorIds({ - includeAdmins: false, - includeRoot: false, - excludeExpire: true, - }); - expect(result).toEqual([modeUser1.id]); - }); - - test('includeAdmins = true, includeRoot = false, excludeExpire = false', async () => { - const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ - createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(), - ]); - - const role1 = await createRole({ name: 'admin', isAdministrator: true }); - const role2 = await createRole({ name: 'moderator', isModerator: true }); - const role3 = await createRole({ name: 'normal' }); - - await Promise.all([ - assignRole({ userId: adminUser1.id, roleId: role1.id }), - assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: modeUser1.id, roleId: role2.id }), - assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: normalUser1.id, roleId: role3.id }), - assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), - ]); - - const result = await roleService.getModeratorIds({ - includeAdmins: true, - includeRoot: false, - excludeExpire: false, - }); - expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id]); - }); - - test('includeAdmins = true, includeRoot = false, excludeExpire = true', async () => { - const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ - createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(), - ]); - - const role1 = await createRole({ name: 'admin', isAdministrator: true }); - const role2 = await createRole({ name: 'moderator', isModerator: true }); - const role3 = await createRole({ name: 'normal' }); - - await Promise.all([ - assignRole({ userId: adminUser1.id, roleId: role1.id }), - assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: modeUser1.id, roleId: role2.id }), - assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: normalUser1.id, roleId: role3.id }), - assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), - ]); - - const result = await roleService.getModeratorIds({ - includeAdmins: true, - includeRoot: false, - excludeExpire: true, - }); - expect(result).toEqual([adminUser1.id, modeUser1.id]); - }); - - test('includeAdmins = false, includeRoot = true, excludeExpire = false', async () => { - const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ - createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(), - ]); - - const role1 = await createRole({ name: 'admin', isAdministrator: true }); - const role2 = await createRole({ name: 'moderator', isModerator: true }); - const role3 = await createRole({ name: 'normal' }); - - await Promise.all([ - assignRole({ userId: adminUser1.id, roleId: role1.id }), - assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: modeUser1.id, roleId: role2.id }), - assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: normalUser1.id, roleId: role3.id }), - assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), - ]); - - const result = await roleService.getModeratorIds({ - includeAdmins: false, - includeRoot: true, - excludeExpire: false, - }); - expect(result).toEqual([modeUser1.id, modeUser2.id, rootUser.id]); - }); - - test('root has moderator role', async () => { - const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([ - createUser(), createUser(), createUser(), createRoot(), - ]); - - const role1 = await createRole({ name: 'admin', isAdministrator: true }); - const role2 = await createRole({ name: 'moderator', isModerator: true }); - const role3 = await createRole({ name: 'normal' }); - - await Promise.all([ - assignRole({ userId: adminUser1.id, roleId: role1.id }), - assignRole({ userId: modeUser1.id, roleId: role2.id }), - assignRole({ userId: rootUser.id, roleId: role2.id }), - assignRole({ userId: normalUser1.id, roleId: role3.id }), - ]); - - const result = await roleService.getModeratorIds({ - includeAdmins: false, - includeRoot: true, - excludeExpire: false, - }); - expect(result).toEqual([modeUser1.id, rootUser.id]); - }); - - test('root has administrator role', async () => { - const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([ - createUser(), createUser(), createUser(), createRoot(), - ]); - - const role1 = await createRole({ name: 'admin', isAdministrator: true }); - const role2 = await createRole({ name: 'moderator', isModerator: true }); - const role3 = await createRole({ name: 'normal' }); - - await Promise.all([ - assignRole({ userId: adminUser1.id, roleId: role1.id }), - assignRole({ userId: rootUser.id, roleId: role1.id }), - assignRole({ userId: modeUser1.id, roleId: role2.id }), - assignRole({ userId: normalUser1.id, roleId: role3.id }), - ]); - - const result = await roleService.getModeratorIds({ - includeAdmins: true, - includeRoot: true, - excludeExpire: false, - }); - expect(result).toEqual([adminUser1.id, modeUser1.id, rootUser.id]); - }); - - test('root has moderator role(expire)', async () => { - const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([ - createUser(), createUser(), createUser(), createRoot(), - ]); - - const role1 = await createRole({ name: 'admin', isAdministrator: true }); - const role2 = await createRole({ name: 'moderator', isModerator: true }); - const role3 = await createRole({ name: 'normal' }); - - await Promise.all([ - assignRole({ userId: adminUser1.id, roleId: role1.id }), - assignRole({ userId: modeUser1.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: rootUser.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: normalUser1.id, roleId: role3.id }), - ]); - - const result = await roleService.getModeratorIds({ - includeAdmins: false, - includeRoot: true, - excludeExpire: true, - }); - expect(result).toEqual([rootUser.id]); - }); - }); - - describe('conditional role', () => { - test('~かつ~', async () => { - const [user1, user2, user3, user4] = await Promise.all([ - createUser({ isBot: true, isCat: false, isSuspended: false }), - createUser({ isBot: false, isCat: true, isSuspended: false }), - createUser({ isBot: true, isCat: true, isSuspended: false }), - createUser({ isBot: false, isCat: false, isSuspended: true }), - ]); - const role1 = await createConditionalRole({ - id: aidx(), - type: 'isBot', - }); - const role2 = await createConditionalRole({ - id: aidx(), - type: 'isCat', - }); - const role3 = await createConditionalRole({ - id: aidx(), - type: 'isSuspended', - }); - const role4 = await createConditionalRole({ - id: aidx(), - type: 'and', - values: [role1.condFormula, role2.condFormula], - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - const actual4 = await roleService.getUserRoles(user4.id); - expect(actual1.some(r => r.id === role4.id)).toBe(false); - expect(actual2.some(r => r.id === role4.id)).toBe(false); - expect(actual3.some(r => r.id === role4.id)).toBe(true); - expect(actual4.some(r => r.id === role4.id)).toBe(false); - }); - - test('~または~', async () => { - const [user1, user2, user3, user4] = await Promise.all([ - createUser({ isBot: true, isCat: false, isSuspended: false }), - createUser({ isBot: false, isCat: true, isSuspended: false }), - createUser({ isBot: true, isCat: true, isSuspended: false }), - createUser({ isBot: false, isCat: false, isSuspended: true }), - ]); - const role1 = await createConditionalRole({ - id: aidx(), - type: 'isBot', - }); - const role2 = await createConditionalRole({ - id: aidx(), - type: 'isCat', - }); - const role3 = await createConditionalRole({ - id: aidx(), - type: 'isSuspended', - }); - const role4 = await createConditionalRole({ - id: aidx(), - type: 'or', - values: [role1.condFormula, role2.condFormula], - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - const actual4 = await roleService.getUserRoles(user4.id); - expect(actual1.some(r => r.id === role4.id)).toBe(true); - expect(actual2.some(r => r.id === role4.id)).toBe(true); - expect(actual3.some(r => r.id === role4.id)).toBe(true); - expect(actual4.some(r => r.id === role4.id)).toBe(false); - }); - - test('~ではない', async () => { - const [user1, user2, user3] = await Promise.all([ - createUser({ isBot: true, isCat: false, isSuspended: false }), - createUser({ isBot: false, isCat: true, isSuspended: false }), - createUser({ isBot: true, isCat: true, isSuspended: false }), - ]); - const role1 = await createConditionalRole({ - id: aidx(), - type: 'isBot', - }); - const role2 = await createConditionalRole({ - id: aidx(), - type: 'isCat', - }); - const role4 = await createConditionalRole({ - id: aidx(), - type: 'not', - value: role1.condFormula, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role4.id)).toBe(false); - expect(actual2.some(r => r.id === role4.id)).toBe(true); - expect(actual3.some(r => r.id === role4.id)).toBe(false); - }); - - test('マニュアルロールにアサイン済み', async () => { - const [user1, user2, role1] = await Promise.all([ - createUser(), - createUser(), - createRole({ - name: 'manual role', - }), - ]); - const role2 = await createConditionalRole({ - id: aidx(), - type: 'roleAssignedTo', - roleId: role1.id, - }); - await roleService.assign(user2.id, role1.id); - - const [u1role, u2role] = await Promise.all([ - roleService.getUserRoles(user1.id), - roleService.getUserRoles(user2.id), - ]); - expect(u1role.some(r => r.id === role2.id)).toBe(false); - expect(u2role.some(r => r.id === role2.id)).toBe(true); - }); - - test('ローカルユーザのみ', async () => { - const [user1, user2] = await Promise.all([ - createUser({ host: null }), - createUser({ host: 'example.com' }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'isLocal', - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - expect(actual1.some(r => r.id === role.id)).toBe(true); - expect(actual2.some(r => r.id === role.id)).toBe(false); - }); - - test('リモートユーザのみ', async () => { - const [user1, user2] = await Promise.all([ - createUser({ host: null }), - createUser({ host: 'example.com' }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'isRemote', - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - }); - - test('サスペンド済みユーザである', async () => { - const [user1, user2] = await Promise.all([ - createUser({ isSuspended: false }), - createUser({ isSuspended: true }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'isSuspended', - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - }); - - test('鍵アカウントユーザである', async () => { - const [user1, user2] = await Promise.all([ - createUser({ isLocked: false }), - createUser({ isLocked: true }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'isLocked', - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - }); - - test('botユーザである', async () => { - const [user1, user2] = await Promise.all([ - createUser({ isBot: false }), - createUser({ isBot: true }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'isBot', - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - }); - - test('猫である', async () => { - const [user1, user2] = await Promise.all([ - createUser({ isCat: false }), - createUser({ isCat: true }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'isCat', - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - }); - - test('「ユーザを見つけやすくする」が有効なアカウント', async () => { - const [user1, user2] = await Promise.all([ - createUser({ isExplorable: false }), - createUser({ isExplorable: true }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'isExplorable', - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - }); - - test('ユーザが作成されてから指定期間経過した', async () => { - const base = new Date(); - base.setMinutes(base.getMinutes() - 5); - - const d1 = new Date(base); - const d2 = new Date(base); - const d3 = new Date(base); - d1.setSeconds(d1.getSeconds() - 1); - d3.setSeconds(d3.getSeconds() + 1); - - const [user1, user2, user3] = await Promise.all([ - // 4:59 - createUser({ id: genAidx(d1.getTime()) }), - // 5:00 - createUser({ id: genAidx(d2.getTime()) }), - // 5:01 - createUser({ id: genAidx(d3.getTime()) }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'createdLessThan', - // 5 minutes - sec: 300, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(false); - expect(actual3.some(r => r.id === role.id)).toBe(true); - }); - - test('ユーザが作成されてから指定期間経っていない', async () => { - const base = new Date(); - base.setMinutes(base.getMinutes() - 5); - - const d1 = new Date(base); - const d2 = new Date(base); - const d3 = new Date(base); - d1.setSeconds(d1.getSeconds() - 1); - d3.setSeconds(d3.getSeconds() + 1); - - const [user1, user2, user3] = await Promise.all([ - // 4:59 - createUser({ id: genAidx(d1.getTime()) }), - // 5:00 - createUser({ id: genAidx(d2.getTime()) }), - // 5:01 - createUser({ id: genAidx(d3.getTime()) }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'createdMoreThan', - // 5 minutes - sec: 300, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(true); - expect(actual2.some(r => r.id === role.id)).toBe(false); - expect(actual3.some(r => r.id === role.id)).toBe(false); - }); - - test('フォロワー数が指定値以下', async () => { - const [user1, user2, user3] = await Promise.all([ - createUser({ followersCount: 99 }), - createUser({ followersCount: 100 }), - createUser({ followersCount: 101 }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'followersLessThanOrEq', - value: 100, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(true); - expect(actual2.some(r => r.id === role.id)).toBe(true); - expect(actual3.some(r => r.id === role.id)).toBe(false); - }); - - test('フォロワー数が指定値以下', async () => { - const [user1, user2, user3] = await Promise.all([ - createUser({ followersCount: 99 }), - createUser({ followersCount: 100 }), - createUser({ followersCount: 101 }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'followersMoreThanOrEq', - value: 100, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - expect(actual3.some(r => r.id === role.id)).toBe(true); - }); - - test('フォロー数が指定値以下', async () => { - const [user1, user2, user3] = await Promise.all([ - createUser({ followingCount: 99 }), - createUser({ followingCount: 100 }), - createUser({ followingCount: 101 }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'followingLessThanOrEq', - value: 100, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(true); - expect(actual2.some(r => r.id === role.id)).toBe(true); - expect(actual3.some(r => r.id === role.id)).toBe(false); - }); - - test('フォロー数が指定値以上', async () => { - const [user1, user2, user3] = await Promise.all([ - createUser({ followingCount: 99 }), - createUser({ followingCount: 100 }), - createUser({ followingCount: 101 }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'followingMoreThanOrEq', - value: 100, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - expect(actual3.some(r => r.id === role.id)).toBe(true); - }); - - test('ノート数が指定値以下', async () => { - const [user1, user2, user3] = await Promise.all([ - createUser({ notesCount: 9 }), - createUser({ notesCount: 10 }), - createUser({ notesCount: 11 }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'notesLessThanOrEq', - value: 10, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(true); - expect(actual2.some(r => r.id === role.id)).toBe(true); - expect(actual3.some(r => r.id === role.id)).toBe(false); - }); - - test('ノート数が指定値以上', async () => { - const [user1, user2, user3] = await Promise.all([ - createUser({ notesCount: 9 }), - createUser({ notesCount: 10 }), - createUser({ notesCount: 11 }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'notesMoreThanOrEq', - value: 10, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - expect(actual3.some(r => r.id === role.id)).toBe(true); - }); - }); - - describe('assign', () => { - test('公開ロールの場合は通知される', async () => { - const user = await createUser(); - const role = await createRole({ - isPublic: true, - name: 'a', - }); - - await roleService.assign(user.id, role.id); - - clock.uninstall(); - await setTimeout(100); - - const assignments = await roleAssignmentsRepository.find({ - where: { - userId: user.id, - roleId: role.id, - }, - }); - expect(assignments).toHaveLength(1); - - expect(notificationService.createNotification).toHaveBeenCalled(); - expect(notificationService.createNotification.mock.lastCall![0]).toBe(user.id); - expect(notificationService.createNotification.mock.lastCall![1]).toBe('roleAssigned'); - expect(notificationService.createNotification.mock.lastCall![2]).toEqual({ - roleId: role.id, - }); - }); - - test('非公開ロールの場合は通知されない', async () => { - const user = await createUser(); - const role = await createRole({ - isPublic: false, - name: 'a', - }); - - await roleService.assign(user.id, role.id); - - clock.uninstall(); - await setTimeout(100); - - const assignments = await roleAssignmentsRepository.find({ - where: { - userId: user.id, - roleId: role.id, - }, - }); - expect(assignments).toHaveLength(1); - - expect(notificationService.createNotification).not.toHaveBeenCalled(); - }); - }); }); diff --git a/packages/backend/test/unit/S3Service.ts b/packages/backend/test/unit/S3Service.ts index 151f3b826a..1dfa22afd2 100644 --- a/packages/backend/test/unit/S3Service.ts +++ b/packages/backend/test/unit/S3Service.ts @@ -1,23 +1,12 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import { Test } from '@nestjs/testing'; -import { - CompleteMultipartUploadCommand, - CreateMultipartUploadCommand, - PutObjectCommand, - S3Client, - UploadPartCommand, -} from '@aws-sdk/client-s3'; +import { UploadPartCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { mockClient } from 'aws-sdk-client-mock'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; -import { S3Service } from '@/core/S3Service.js'; -import { MiMeta } from '@/models/_.js'; +import { S3Service } from '@/core/S3Service'; +import { Meta } from '@/models'; import type { TestingModule } from '@nestjs/testing'; describe('S3Service', () => { @@ -46,7 +35,7 @@ describe('S3Service', () => { test('upload a file', async () => { s3Mock.on(PutObjectCommand).resolves({}); - await s3Service.upload({ objectStorageRegion: 'us-east-1' } as MiMeta, { + await s3Service.upload({ objectStorageRegion: 'us-east-1' } as Meta, { Bucket: 'fake', Key: 'fake', Body: 'x', @@ -58,7 +47,7 @@ describe('S3Service', () => { s3Mock.on(UploadPartCommand).resolves({ ETag: '1' }); s3Mock.on(CompleteMultipartUploadCommand).resolves({ Bucket: 'fake', Key: 'fake' }); - await s3Service.upload({} as MiMeta, { + await s3Service.upload({} as Meta, { Bucket: 'fake', Key: 'fake', Body: 'x'.repeat(8 * 1024 * 1024 + 1), // デフォルトpartSizeにしている 8 * 1024 * 1024 を越えるサイズ @@ -68,7 +57,7 @@ describe('S3Service', () => { test('upload a file error', async () => { s3Mock.on(PutObjectCommand).rejects({ name: 'Fake Error' }); - await expect(s3Service.upload({ objectStorageRegion: 'us-east-1' } as MiMeta, { + await expect(s3Service.upload({ objectStorageRegion: 'us-east-1' } as Meta, { Bucket: 'fake', Key: 'fake', Body: 'x', @@ -78,7 +67,7 @@ describe('S3Service', () => { test('upload a large file error', async () => { s3Mock.on(UploadPartCommand).rejects(); - await expect(s3Service.upload({} as MiMeta, { + await expect(s3Service.upload({} as Meta, { Bucket: 'fake', Key: 'fake', Body: 'x'.repeat(8 * 1024 * 1024 + 1), // デフォルトpartSizeにしている 8 * 1024 * 1024 を越えるサイズ diff --git a/packages/backend/test/unit/SigninWithPasskeyApiService.ts b/packages/backend/test/unit/SigninWithPasskeyApiService.ts deleted file mode 100644 index 0687ed8437..0000000000 --- a/packages/backend/test/unit/SigninWithPasskeyApiService.ts +++ /dev/null @@ -1,182 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { IncomingHttpHeaders } from 'node:http'; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals'; -import { Test, TestingModule } from '@nestjs/testing'; -import { FastifyReply, FastifyRequest } from 'fastify'; -import { AuthenticationResponseJSON } from '@simplewebauthn/types'; -import { HttpHeader } from 'fastify/types/utils.js'; -import { MockFunctionMetadata, ModuleMocker } from 'jest-mock'; -import { MiUser } from '@/models/User.js'; -import { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { DI } from '@/di-symbols.js'; -import { CoreModule } from '@/core/CoreModule.js'; -import { SigninWithPasskeyApiService } from '@/server/api/SigninWithPasskeyApiService.js'; -import { RateLimiterService } from '@/server/api/RateLimiterService.js'; -import { WebAuthnService } from '@/core/WebAuthnService.js'; -import { SigninService } from '@/server/api/SigninService.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; - -const moduleMocker = new ModuleMocker(global); - -class FakeLimiter { - public async limit() { - return; - } -} - -class FakeSigninService { - public signin(..._args: any): any { - return true; - } -} - -class DummyFastifyReply { - public statusCode: number; - code(num: number): void { - this.statusCode = num; - } - header(_key: HttpHeader, _value: any): void { - } -} -class DummyFastifyRequest { - public ip: string; - public body: {credential: any, context: string}; - public headers: IncomingHttpHeaders = { 'accept': 'application/json' }; - constructor(body?: any) { - this.ip = '0.0.0.0'; - this.body = body; - } -} - -type ApiFastifyRequestType = FastifyRequest<{ - Body: { - credential?: AuthenticationResponseJSON; - context?: string; - }; -}>; - -describe('SigninWithPasskeyApiService', () => { - let app: TestingModule; - let passkeyApiService: SigninWithPasskeyApiService; - let usersRepository: UsersRepository; - let userProfilesRepository: UserProfilesRepository; - let webAuthnService: WebAuthnService; - let idService: IdService; - let FakeWebauthnVerify: ()=>Promise; - - async function createUser(data: Partial = {}) { - const user = await usersRepository - .save({ - ...data, - }); - return user; - } - - async function createUserProfile(data: Partial = {}) { - const userProfile = await userProfilesRepository - .save({ ...data }, - ); - return userProfile; - } - - beforeAll(async () => { - app = await Test.createTestingModule({ - imports: [GlobalModule, CoreModule], - providers: [ - SigninWithPasskeyApiService, - { provide: RateLimiterService, useClass: FakeLimiter }, - { provide: SigninService, useClass: FakeSigninService }, - ], - }).useMocker((token) => { - if (typeof token === 'function') { - const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata; - const Mock = moduleMocker.generateFromMetadata(mockMetadata); - return new Mock(); - } - }).compile(); - passkeyApiService = app.get(SigninWithPasskeyApiService); - usersRepository = app.get(DI.usersRepository); - userProfilesRepository = app.get(DI.userProfilesRepository); - webAuthnService = app.get(WebAuthnService); - idService = app.get(IdService); - }); - - beforeEach(async () => { - const uid = idService.gen(); - FakeWebauthnVerify = async () => { - return uid; - }; - jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication').mockImplementation(FakeWebauthnVerify); - - const dummyUser = { - id: uid, username: uid, usernameLower: uid.toLowerCase(), uri: null, host: null, - }; - const dummyProfile = { - userId: uid, - password: 'qwerty', - usePasswordLessLogin: true, - }; - await createUser(dummyUser); - await createUserProfile(dummyProfile); - }); - - afterAll(async () => { - await app.close(); - }); - - describe('Get Passkey Options', () => { - it('Should return passkey Auth Options', async () => { - const req = new DummyFastifyRequest({}) as ApiFastifyRequestType; - const res = new DummyFastifyReply() as unknown as FastifyReply; - const res_body = await passkeyApiService.signin(req, res); - expect(res.statusCode).toBe(200); - expect((res_body as any).option).toBeDefined(); - expect(typeof (res_body as any).context).toBe('string'); - }); - }); - describe('Try Passkey Auth', () => { - it('Should Success', async () => { - const req = new DummyFastifyRequest({ context: 'auth-context', credential: { dummy: [] } }) as ApiFastifyRequestType; - const res = new DummyFastifyReply() as FastifyReply; - const res_body = await passkeyApiService.signin(req, res); - expect((res_body as any).signinResponse).toBeDefined(); - }); - - it('Should return 400 Without Auth Context', async () => { - const req = new DummyFastifyRequest({ credential: { dummy: [] } }) as ApiFastifyRequestType; - const res = new DummyFastifyReply() as FastifyReply; - const res_body = await passkeyApiService.signin(req, res); - expect(res.statusCode).toBe(400); - expect((res_body as any).error?.id).toStrictEqual('1658cc2e-4495-461f-aee4-d403cdf073c1'); - }); - - it('Should return 403 When Challenge Verify fail', async () => { - const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType; - const res = new DummyFastifyReply() as FastifyReply; - jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication') - .mockImplementation(async () => { - throw new IdentifiableError('THIS_ERROR_CODE_SHOULD_BE_FORWARDED'); - }); - const res_body = await passkeyApiService.signin(req, res); - expect(res.statusCode).toBe(403); - expect((res_body as any).error?.id).toStrictEqual('THIS_ERROR_CODE_SHOULD_BE_FORWARDED'); - }); - - it('Should return 403 When The user not Enabled Passwordless login', async () => { - const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType; - const res = new DummyFastifyReply() as FastifyReply; - const userId = await FakeWebauthnVerify(); - const data = { userId: userId, usePasswordLessLogin: false }; - await userProfilesRepository.update({ userId: userId }, data); - const res_body = await passkeyApiService.signin(req, res); - expect(res.statusCode).toBe(403); - expect((res_body as any).error?.id).toStrictEqual('2d84773e-f7b7-4d0b-8f72-bb69b584c912'); - }); - }); -}); diff --git a/packages/backend/test/unit/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts deleted file mode 100644 index 1128d83be1..0000000000 --- a/packages/backend/test/unit/SystemWebhookService.ts +++ /dev/null @@ -1,556 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { setTimeout } from 'node:timers/promises'; -import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals'; -import { Test, TestingModule } from '@nestjs/testing'; -import { randomString } from '../utils.js'; -import { MiUser } from '@/models/User.js'; -import { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js'; -import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; -import { QueueService } from '@/core/QueueService.js'; -import { LoggerService } from '@/core/LoggerService.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; - -describe('SystemWebhookService', () => { - let app: TestingModule; - let service: SystemWebhookService; - - // -------------------------------------------------------------------------------------- - - let usersRepository: UsersRepository; - let systemWebhooksRepository: SystemWebhooksRepository; - let idService: IdService; - let queueService: jest.Mocked; - - // -------------------------------------------------------------------------------------- - - let root: MiUser; - - // -------------------------------------------------------------------------------------- - - async function createUser(data: Partial = {}) { - return await usersRepository - .insert({ - id: idService.gen(), - ...data, - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - } - - async function createWebhook(data: Partial = {}) { - return systemWebhooksRepository - .insert({ - id: idService.gen(), - name: randomString(), - on: ['abuseReport'], - url: 'https://example.com', - secret: randomString(), - ...data, - }) - .then(x => systemWebhooksRepository.findOneByOrFail(x.identifiers[0])); - } - - // -------------------------------------------------------------------------------------- - - async function beforeAllImpl() { - app = await Test - .createTestingModule({ - imports: [ - GlobalModule, - ], - providers: [ - SystemWebhookService, - IdService, - LoggerService, - GlobalEventService, - { - provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }), - }, - { - provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }), - }, - ], - }) - .compile(); - - usersRepository = app.get(DI.usersRepository); - systemWebhooksRepository = app.get(DI.systemWebhooksRepository); - - service = app.get(SystemWebhookService); - idService = app.get(IdService); - queueService = app.get(QueueService) as jest.Mocked; - - app.enableShutdownHooks(); - } - - async function afterAllImpl() { - await app.close(); - } - - async function beforeEachImpl() { - root = await createUser({ username: 'root', usernameLower: 'root' }); - } - - async function afterEachImpl() { - await usersRepository.createQueryBuilder().delete().execute(); - await systemWebhooksRepository.createQueryBuilder().delete().execute(); - } - - // -------------------------------------------------------------------------------------- - - describe('アプリを毎回作り直す必要のないグループ', () => { - beforeAll(beforeAllImpl); - afterAll(afterAllImpl); - beforeEach(beforeEachImpl); - afterEach(afterEachImpl); - - describe('fetchSystemWebhooks', () => { - test('フィルタなし', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - const webhook2 = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - const webhook3 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - const webhook4 = await createWebhook({ - isActive: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchSystemWebhooks(); - expect(fetchedWebhooks).toEqual([webhook1, webhook2, webhook3, webhook4]); - }); - - test('activeのみ', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - const webhook2 = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - const webhook3 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - const webhook4 = await createWebhook({ - isActive: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchSystemWebhooks({ isActive: true }); - expect(fetchedWebhooks).toEqual([webhook1, webhook3]); - }); - - test('特定のイベントのみ', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - const webhook2 = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - const webhook3 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - const webhook4 = await createWebhook({ - isActive: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchSystemWebhooks({ on: ['abuseReport'] }); - expect(fetchedWebhooks).toEqual([webhook1, webhook2]); - }); - - test('activeな特定のイベントのみ', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - const webhook2 = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - const webhook3 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - const webhook4 = await createWebhook({ - isActive: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchSystemWebhooks({ on: ['abuseReport'], isActive: true }); - expect(fetchedWebhooks).toEqual([webhook1]); - }); - - test('ID指定', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - const webhook2 = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - const webhook3 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - const webhook4 = await createWebhook({ - isActive: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchSystemWebhooks({ ids: [webhook1.id, webhook4.id] }); - expect(fetchedWebhooks).toEqual([webhook1, webhook4]); - }); - - test('ID指定(他条件とANDになるか見たい)', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - const webhook2 = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - const webhook3 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - const webhook4 = await createWebhook({ - isActive: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchSystemWebhooks({ ids: [webhook1.id, webhook4.id], isActive: false }); - expect(fetchedWebhooks).toEqual([webhook4]); - }); - }); - - describe('createSystemWebhook', () => { - test('作成成功 ', async () => { - const params = { - isActive: true, - name: randomString(), - on: ['abuseReport'] as SystemWebhookEventType[], - url: 'https://example.com', - secret: randomString(), - }; - - const webhook = await service.createSystemWebhook(params, root); - expect(webhook).toMatchObject(params); - }); - }); - - describe('updateSystemWebhook', () => { - test('更新成功', async () => { - const webhook = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - - const params = { - id: webhook.id, - isActive: false, - name: randomString(), - on: ['abuseReport'] as SystemWebhookEventType[], - url: randomString(), - secret: randomString(), - }; - - const updatedWebhook = await service.updateSystemWebhook(params, root); - expect(updatedWebhook).toMatchObject(params); - }); - }); - - describe('deleteSystemWebhook', () => { - test('削除成功', async () => { - const webhook = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - - await service.deleteSystemWebhook(webhook.id, root); - - await expect(systemWebhooksRepository.findOneBy({ id: webhook.id })).resolves.toBeNull(); - }); - }); - }); - - describe('アプリを毎回作り直す必要があるグループ', () => { - beforeEach(async () => { - await beforeAllImpl(); - await beforeEachImpl(); - }); - - afterEach(async () => { - await afterEachImpl(); - await afterAllImpl(); - }); - - describe('enqueueSystemWebhook', () => { - test('キューに追加成功', async () => { - const webhook = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any); - - expect(queueService.systemWebhookDeliver).toHaveBeenCalledTimes(1); - expect(queueService.systemWebhookDeliver.mock.calls[0][0] as MiSystemWebhook).toEqual(webhook); - }); - - test('非アクティブなWebhookはキューに追加されない', async () => { - const webhook = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any); - - expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled(); - }); - - test('未許可のイベント種別が渡された場合はWebhookはキューに追加されない', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: [], - }); - const webhook2 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any); - - expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled(); - }); - - test('混在した時、有効かつ許可されたイベント種別のみ', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - const webhook2 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - const webhook3 = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - const webhook4 = await createWebhook({ - isActive: false, - on: ['abuseReportResolved'], - }); - await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any); - - expect(queueService.systemWebhookDeliver).toHaveBeenCalledTimes(1); - expect(queueService.systemWebhookDeliver.mock.calls[0][0] as MiSystemWebhook).toEqual(webhook1); - }); - - test('除外指定した場合は送信されない', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - const webhook2 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - - await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any, { excludes: [webhook2.id] }); - - expect(queueService.systemWebhookDeliver).toHaveBeenCalledTimes(1); - expect(queueService.systemWebhookDeliver.mock.calls[0][0] as MiSystemWebhook).toEqual(webhook1); - }); - }); - - describe('fetchActiveSystemWebhooks', () => { - describe('systemWebhookCreated', () => { - test('ActiveなWebhookが追加された時、キャッシュに追加されている', async () => { - const webhook = await service.createSystemWebhook( - { - isActive: true, - name: randomString(), - on: ['abuseReport'], - url: 'https://example.com', - secret: randomString(), - }, - root, - ); - - // redisでの配信経由で更新されるのでちょっと待つ - await setTimeout(500); - - const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); - expect(fetchedWebhooks).toEqual([webhook]); - }); - - test('NotActiveなWebhookが追加された時、キャッシュに追加されていない', async () => { - const webhook = await service.createSystemWebhook( - { - isActive: false, - name: randomString(), - on: ['abuseReport'], - url: 'https://example.com', - secret: randomString(), - }, - root, - ); - - // redisでの配信経由で更新されるのでちょっと待つ - await setTimeout(500); - - const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); - expect(fetchedWebhooks).toEqual([]); - }); - }); - - describe('systemWebhookUpdated', () => { - test('ActiveなWebhookが編集された時、キャッシュに反映されている', async () => { - const id = idService.gen(); - await createWebhook({ id }); - // キャッシュ作成 - const webhook1 = await service.fetchActiveSystemWebhooks(); - // 読み込まれていることをチェック - expect(webhook1.length).toEqual(1); - expect(webhook1[0].id).toEqual(id); - - const webhook2 = await service.updateSystemWebhook( - { - id, - isActive: true, - name: randomString(), - on: ['abuseReport'], - url: 'https://example.com', - secret: randomString(), - }, - root, - ); - - // redisでの配信経由で更新されるのでちょっと待つ - await setTimeout(500); - - const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); - expect(fetchedWebhooks).toEqual([webhook2]); - }); - - test('NotActiveなWebhookが編集された時、キャッシュに追加されない', async () => { - const id = idService.gen(); - await createWebhook({ id, isActive: false }); - // キャッシュ作成 - const webhook1 = await service.fetchActiveSystemWebhooks(); - // 読み込まれていないことをチェック - expect(webhook1.length).toEqual(0); - - const webhook2 = await service.updateSystemWebhook( - { - id, - isActive: false, - name: randomString(), - on: ['abuseReport'], - url: 'https://example.com', - secret: randomString(), - }, - root, - ); - - // redisでの配信経由で更新されるのでちょっと待つ - await setTimeout(500); - - const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); - expect(fetchedWebhooks.length).toEqual(0); - }); - - test('NotActiveなWebhookがActiveにされた時、キャッシュに追加されている', async () => { - const id = idService.gen(); - const baseWebhook = await createWebhook({ id, isActive: false }); - // キャッシュ作成 - const webhook1 = await service.fetchActiveSystemWebhooks(); - // 読み込まれていないことをチェック - expect(webhook1.length).toEqual(0); - - const webhook2 = await service.updateSystemWebhook( - { - ...baseWebhook, - isActive: true, - }, - root, - ); - - // redisでの配信経由で更新されるのでちょっと待つ - await setTimeout(500); - - const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); - expect(fetchedWebhooks).toEqual([webhook2]); - }); - - test('ActiveなWebhookがNotActiveにされた時、キャッシュから削除されている', async () => { - const id = idService.gen(); - const baseWebhook = await createWebhook({ id, isActive: true }); - // キャッシュ作成 - const webhook1 = await service.fetchActiveSystemWebhooks(); - // 読み込まれていることをチェック - expect(webhook1.length).toEqual(1); - expect(webhook1[0].id).toEqual(id); - - const webhook2 = await service.updateSystemWebhook( - { - ...baseWebhook, - isActive: false, - }, - root, - ); - - // redisでの配信経由で更新されるのでちょっと待つ - await setTimeout(500); - - const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); - expect(fetchedWebhooks.length).toEqual(0); - }); - }); - - describe('systemWebhookDeleted', () => { - test('キャッシュから削除されている', async () => { - const id = idService.gen(); - const baseWebhook = await createWebhook({ id, isActive: true }); - // キャッシュ作成 - const webhook1 = await service.fetchActiveSystemWebhooks(); - // 読み込まれていることをチェック - expect(webhook1.length).toEqual(1); - expect(webhook1[0].id).toEqual(id); - - const webhook2 = await service.deleteSystemWebhook( - id, - root, - ); - - // redisでの配信経由で更新されるのでちょっと待つ - await setTimeout(500); - - const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); - expect(fetchedWebhooks.length).toEqual(0); - }); - }); - }); - }); -}); diff --git a/packages/backend/test/unit/UserSearchService.ts b/packages/backend/test/unit/UserSearchService.ts deleted file mode 100644 index 75d3e58adc..0000000000 --- a/packages/backend/test/unit/UserSearchService.ts +++ /dev/null @@ -1,265 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Test, TestingModule } from '@nestjs/testing'; -import { describe, jest, test } from '@jest/globals'; -import { In } from 'typeorm'; -import { UserSearchService } from '@/core/UserSearchService.js'; -import { FollowingsRepository, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { DI } from '@/di-symbols.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; - -describe('UserSearchService', () => { - let app: TestingModule; - let service: UserSearchService; - - let usersRepository: UsersRepository; - let followingsRepository: FollowingsRepository; - let idService: IdService; - let userProfilesRepository: UserProfilesRepository; - - let root: MiUser; - let alice: MiUser; - let alyce: MiUser; - let alycia: MiUser; - let alysha: MiUser; - let alyson: MiUser; - let alyssa: MiUser; - let bob: MiUser; - let bobbi: MiUser; - let bobbie: MiUser; - let bobby: MiUser; - - async function createUser(data: Partial = {}) { - const user = await usersRepository - .insert({ - id: idService.gen(), - ...data, - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - - await userProfilesRepository.insert({ - userId: user.id, - }); - - return user; - } - - async function createFollowings(follower: MiUser, followees: MiUser[]) { - for (const followee of followees) { - await followingsRepository.insert({ - id: idService.gen(), - followerId: follower.id, - followeeId: followee.id, - }); - } - } - - async function setActive(users: MiUser[]) { - for (const user of users) { - await usersRepository.update(user.id, { - updatedAt: new Date(), - }); - } - } - - async function setInactive(users: MiUser[]) { - for (const user of users) { - await usersRepository.update(user.id, { - updatedAt: new Date(0), - }); - } - } - - async function setSuspended(users: MiUser[]) { - for (const user of users) { - await usersRepository.update(user.id, { - isSuspended: true, - }); - } - } - - beforeAll(async () => { - app = await Test - .createTestingModule({ - imports: [ - GlobalModule, - ], - providers: [ - UserSearchService, - { - provide: UserEntityService, useFactory: jest.fn(() => ({ - // とりあえずIDが返れば確認が出来るので - packMany: (value: any) => value, - })), - }, - IdService, - ], - }) - .compile(); - - await app.init(); - - usersRepository = app.get(DI.usersRepository); - userProfilesRepository = app.get(DI.userProfilesRepository); - followingsRepository = app.get(DI.followingsRepository); - - service = app.get(UserSearchService); - idService = app.get(IdService); - }); - - beforeEach(async () => { - root = await createUser({ username: 'root', usernameLower: 'root' }); - alice = await createUser({ username: 'Alice', usernameLower: 'alice' }); - alyce = await createUser({ username: 'Alyce', usernameLower: 'alyce' }); - alycia = await createUser({ username: 'Alycia', usernameLower: 'alycia' }); - alysha = await createUser({ username: 'Alysha', usernameLower: 'alysha' }); - alyson = await createUser({ username: 'Alyson', usernameLower: 'alyson', host: 'example.com' }); - alyssa = await createUser({ username: 'Alyssa', usernameLower: 'alyssa', host: 'example.com' }); - bob = await createUser({ username: 'Bob', usernameLower: 'bob' }); - bobbi = await createUser({ username: 'Bobbi', usernameLower: 'bobbi' }); - bobbie = await createUser({ username: 'Bobbie', usernameLower: 'bobbie', host: 'example.com' }); - bobby = await createUser({ username: 'Bobby', usernameLower: 'bobby', host: 'example.com' }); - }); - - afterEach(async () => { - await usersRepository.createQueryBuilder().delete().execute(); - }); - - afterAll(async () => { - await app.close(); - }); - - describe('searchByUsernameAndHost', () => { - test('フォロー中のアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { - await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - await setActive([alice, alyce, alyssa, bob, bobbi, bobbie, bobby]); - await setInactive([alycia, alysha, alyson]); - - const result = await service.searchByUsernameAndHost( - { username: 'al' }, - { limit: 100 }, - root, - ); - - // alycia, alysha, alysonは非アクティブなので後ろに行く - expect(result).toEqual([alice, alyce, alyssa, alycia, alysha, alyson].map(x => x.id)); - }); - - test('フォロー中の非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { - await createFollowings(root, [alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - - const result = await service.searchByUsernameAndHost( - { username: 'al' }, - { limit: 100 }, - root, - ); - - // alice, alyceはフォローしていないので後ろに行く - expect(result).toEqual([alycia, alysha, alyson, alyssa, alice, alyce].map(x => x.id)); - }); - - test('フォローしていないアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { - await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - await setInactive([alice, alyce, alycia]); - - const result = await service.searchByUsernameAndHost( - { username: 'al' }, - { limit: 100 }, - root, - ); - - // alice, alyce, alyciaは非アクティブなので後ろに行く - expect(result).toEqual([alysha, alyson, alyssa, alice, alyce, alycia].map(x => x.id)); - }); - - test('フォローしていない非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { - await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - - const result = await service.searchByUsernameAndHost( - { username: 'al' }, - { limit: 100 }, - root, - ); - - expect(result).toEqual([alice, alyce, alycia, alysha, alyson, alyssa].map(x => x.id)); - }); - - test('フォロー(アクティブ)、フォロー(非アクティブ)、非フォロー(アクティブ)、非フォロー(非アクティブ)混在時の優先順位度確認', async () => { - await createFollowings(root, [alyson, alyssa, bob, bobbi, bobbie]); - await setActive([root, alyssa, bob, bobbi, alyce, alycia]); - await setInactive([alyson, alice, alysha, bobbie, bobby]); - - const result = await service.searchByUsernameAndHost( - { }, - { limit: 100 }, - root, - ); - - // 見る用 - // const users = await usersRepository.findBy({ id: In(result) }).then(it => new Map(it.map(x => [x.id, x]))); - // console.log(result.map(x => users.get(x as any)).map(it => it?.username)); - - // フォローしててアクティブなので先頭: alyssa, bob, bobbi - // フォローしてて非アクティブなので次: alyson, bobbie - // フォローしてないけどアクティブなので次: alyce, alycia, root(アルファベット順的にここになる) - // フォローしてないし非アクティブなので最後: alice, alysha, bobby - expect(result).toEqual([alyssa, bob, bobbi, alyson, bobbie, alyce, alycia, root, alice, alysha, bobby].map(x => x.id)); - }); - - test('[非ログイン] アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { - await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - await setInactive([alice, alyce, alycia]); - - const result = await service.searchByUsernameAndHost( - { username: 'al' }, - { limit: 100 }, - ); - - // alice, alyce, alyciaは非アクティブなので後ろに行く - expect(result).toEqual([alysha, alyson, alyssa, alice, alyce, alycia].map(x => x.id)); - }); - - test('[非ログイン] 非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { - await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - - const result = await service.searchByUsernameAndHost( - { username: 'al' }, - { limit: 100 }, - ); - - expect(result).toEqual([alice, alyce, alycia, alysha, alyson, alyssa].map(x => x.id)); - }); - - test('フォロー中のアクティブユーザのうち、"al"から始まり"example.com"にいる人が全員ヒットする', async () => { - await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - - const result = await service.searchByUsernameAndHost( - { username: 'al', host: 'exam' }, - { limit: 100 }, - root, - ); - - expect(result).toEqual([alyson, alyssa].map(x => x.id)); - }); - - test('サスペンド済みユーザは出ない', async () => { - await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - await setSuspended([alice, alyce, alycia]); - - const result = await service.searchByUsernameAndHost( - { username: 'al' }, - { limit: 100 }, - root, - ); - - expect(result).toEqual([alysha, alyson, alyssa].map(x => x.id)); - }); - }); -}); diff --git a/packages/backend/test/unit/UserWebhookService.ts b/packages/backend/test/unit/UserWebhookService.ts deleted file mode 100644 index 928b9d3c2b..0000000000 --- a/packages/backend/test/unit/UserWebhookService.ts +++ /dev/null @@ -1,332 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals'; -import { Test, TestingModule } from '@nestjs/testing'; -import { randomString } from '../utils.js'; -import { MiUser } from '@/models/User.js'; -import { MiWebhook, UsersRepository, WebhooksRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; -import { QueueService } from '@/core/QueueService.js'; -import { LoggerService } from '@/core/LoggerService.js'; -import { UserWebhookService } from '@/core/UserWebhookService.js'; - -describe('UserWebhookService', () => { - let app: TestingModule; - let service: UserWebhookService; - - // -------------------------------------------------------------------------------------- - - let usersRepository: UsersRepository; - let userWebhooksRepository: WebhooksRepository; - let idService: IdService; - let queueService: jest.Mocked; - - // -------------------------------------------------------------------------------------- - - let root: MiUser; - - // -------------------------------------------------------------------------------------- - - async function createUser(data: Partial = {}) { - return await usersRepository - .insert({ - id: idService.gen(), - ...data, - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - } - - async function createWebhook(data: Partial = {}) { - return userWebhooksRepository - .insert({ - id: idService.gen(), - name: randomString(), - on: ['mention'], - url: 'https://example.com', - secret: randomString(), - userId: root.id, - ...data, - }) - .then(x => userWebhooksRepository.findOneByOrFail(x.identifiers[0])); - } - - // -------------------------------------------------------------------------------------- - - async function beforeAllImpl() { - app = await Test - .createTestingModule({ - imports: [ - GlobalModule, - ], - providers: [ - UserWebhookService, - IdService, - LoggerService, - GlobalEventService, - { - provide: QueueService, useFactory: () => ({ userWebhookDeliver: jest.fn() }), - }, - ], - }) - .compile(); - - usersRepository = app.get(DI.usersRepository); - userWebhooksRepository = app.get(DI.webhooksRepository); - - service = app.get(UserWebhookService); - idService = app.get(IdService); - queueService = app.get(QueueService) as jest.Mocked; - - app.enableShutdownHooks(); - } - - async function afterAllImpl() { - await app.close(); - } - - async function beforeEachImpl() { - root = await createUser({ username: 'root', usernameLower: 'root' }); - } - - async function afterEachImpl() { - await usersRepository.createQueryBuilder().delete().execute(); - await userWebhooksRepository.createQueryBuilder().delete().execute(); - } - - // -------------------------------------------------------------------------------------- - - describe('アプリを毎回作り直す必要のないグループ', () => { - beforeAll(beforeAllImpl); - afterAll(afterAllImpl); - beforeEach(beforeEachImpl); - afterEach(afterEachImpl); - - describe('fetchSystemWebhooks', () => { - test('フィルタなし', async () => { - const webhook1 = await createWebhook({ - active: true, - on: ['mention'], - }); - const webhook2 = await createWebhook({ - active: false, - on: ['mention'], - }); - const webhook3 = await createWebhook({ - active: true, - on: ['reply'], - }); - const webhook4 = await createWebhook({ - active: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchWebhooks(); - expect(fetchedWebhooks).toEqual([webhook1, webhook2, webhook3, webhook4]); - }); - - test('activeのみ', async () => { - const webhook1 = await createWebhook({ - active: true, - on: ['mention'], - }); - const webhook2 = await createWebhook({ - active: false, - on: ['mention'], - }); - const webhook3 = await createWebhook({ - active: true, - on: ['reply'], - }); - const webhook4 = await createWebhook({ - active: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchWebhooks({ isActive: true }); - expect(fetchedWebhooks).toEqual([webhook1, webhook3]); - }); - - test('特定のイベントのみ', async () => { - const webhook1 = await createWebhook({ - active: true, - on: ['mention'], - }); - const webhook2 = await createWebhook({ - active: false, - on: ['mention'], - }); - const webhook3 = await createWebhook({ - active: true, - on: ['reply'], - }); - const webhook4 = await createWebhook({ - active: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchWebhooks({ on: ['mention'] }); - expect(fetchedWebhooks).toEqual([webhook1, webhook2]); - }); - - test('activeな特定のイベントのみ', async () => { - const webhook1 = await createWebhook({ - active: true, - on: ['mention'], - }); - const webhook2 = await createWebhook({ - active: false, - on: ['mention'], - }); - const webhook3 = await createWebhook({ - active: true, - on: ['reply'], - }); - const webhook4 = await createWebhook({ - active: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchWebhooks({ on: ['mention'], isActive: true }); - expect(fetchedWebhooks).toEqual([webhook1]); - }); - - test('ID指定', async () => { - const webhook1 = await createWebhook({ - active: true, - on: ['mention'], - }); - const webhook2 = await createWebhook({ - active: false, - on: ['mention'], - }); - const webhook3 = await createWebhook({ - active: true, - on: ['reply'], - }); - const webhook4 = await createWebhook({ - active: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchWebhooks({ ids: [webhook1.id, webhook4.id] }); - expect(fetchedWebhooks).toEqual([webhook1, webhook4]); - }); - - test('ID指定(他条件とANDになるか見たい)', async () => { - const webhook1 = await createWebhook({ - active: true, - on: ['mention'], - }); - const webhook2 = await createWebhook({ - active: false, - on: ['mention'], - }); - const webhook3 = await createWebhook({ - active: true, - on: ['reply'], - }); - const webhook4 = await createWebhook({ - active: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchWebhooks({ ids: [webhook1.id, webhook4.id], isActive: false }); - expect(fetchedWebhooks).toEqual([webhook4]); - }); - }); - }); - - describe('アプリを毎回作り直す必要があるグループ', () => { - beforeEach(async () => { - await beforeAllImpl(); - await beforeEachImpl(); - }); - - afterEach(async () => { - await afterEachImpl(); - await afterAllImpl(); - }); - - describe('enqueueUserWebhook', () => { - test('キューに追加成功', async () => { - const webhook = await createWebhook({ - active: true, - on: ['note'], - }); - await service.enqueueUserWebhook(webhook.userId, 'note', { foo: 'bar' } as any); - - expect(queueService.userWebhookDeliver).toHaveBeenCalledTimes(1); - expect(queueService.userWebhookDeliver.mock.calls[0][0] as MiWebhook).toEqual(webhook); - }); - - test('非アクティブなWebhookはキューに追加されない', async () => { - const webhook = await createWebhook({ - active: false, - on: ['note'], - }); - await service.enqueueUserWebhook(webhook.userId, 'note', { foo: 'bar' } as any); - - expect(queueService.userWebhookDeliver).not.toHaveBeenCalled(); - }); - - test('未許可のイベント種別が渡された場合はWebhookはキューに追加されない', async () => { - const webhook1 = await createWebhook({ - active: true, - on: [], - }); - const webhook2 = await createWebhook({ - active: true, - on: ['note'], - }); - await service.enqueueUserWebhook(webhook1.userId, 'renote', { foo: 'bar' } as any); - await service.enqueueUserWebhook(webhook2.userId, 'renote', { foo: 'bar' } as any); - - expect(queueService.userWebhookDeliver).not.toHaveBeenCalled(); - }); - - test('ユーザIDが異なるWebhookはキューに追加されない', async () => { - const webhook = await createWebhook({ - active: true, - on: ['note'], - }); - await service.enqueueUserWebhook(idService.gen(), 'note', { foo: 'bar' } as any); - - expect(queueService.userWebhookDeliver).not.toHaveBeenCalled(); - }); - - test('混在した時、有効かつ許可されたイベント種別のみ', async () => { - const userId = root.id; - const webhook1 = await createWebhook({ - userId, - active: true, - on: ['note'], - }); - const webhook2 = await createWebhook({ - userId, - active: true, - on: ['renote'], - }); - const webhook3 = await createWebhook({ - userId, - active: false, - on: ['note'], - }); - const webhook4 = await createWebhook({ - userId, - active: false, - on: ['renote'], - }); - await service.enqueueUserWebhook(userId, 'note', { foo: 'bar' } as any); - - expect(queueService.userWebhookDeliver).toHaveBeenCalledTimes(1); - expect(queueService.userWebhookDeliver.mock.calls[0][0] as MiWebhook).toEqual(webhook1); - }); - }); - }); -}); diff --git a/packages/backend/test/unit/WebhookTestService.ts b/packages/backend/test/unit/WebhookTestService.ts deleted file mode 100644 index 0e965021c2..0000000000 --- a/packages/backend/test/unit/WebhookTestService.ts +++ /dev/null @@ -1,231 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Test, TestingModule } from '@nestjs/testing'; -import { beforeAll, describe, jest } from '@jest/globals'; -import { WebhookTestService } from '@/core/WebhookTestService.js'; -import { UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { DI } from '@/di-symbols.js'; -import { QueueService } from '@/core/QueueService.js'; -import { CustomEmojiService } from '@/core/CustomEmojiService.js'; - -describe('WebhookTestService', () => { - let app: TestingModule; - let service: WebhookTestService; - - // -------------------------------------------------------------------------------------- - - let usersRepository: UsersRepository; - let userProfilesRepository: UserProfilesRepository; - let queueService: jest.Mocked; - let userWebhookService: jest.Mocked; - let systemWebhookService: jest.Mocked; - let idService: IdService; - - let root: MiUser; - let alice: MiUser; - - async function createUser(data: Partial = {}) { - const user = await usersRepository - .insert({ - id: idService.gen(), - ...data, - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - - await userProfilesRepository.insert({ - userId: user.id, - }); - - return user; - } - - // -------------------------------------------------------------------------------------- - - beforeAll(async () => { - app = await Test.createTestingModule({ - imports: [ - GlobalModule, - ], - providers: [ - WebhookTestService, - IdService, - { - provide: CustomEmojiService, useFactory: () => ({ - populateEmojis: jest.fn(), - }), - }, - { - provide: QueueService, useFactory: () => ({ - systemWebhookDeliver: jest.fn(), - userWebhookDeliver: jest.fn(), - }), - }, - { - provide: UserWebhookService, useFactory: () => ({ - fetchWebhooks: jest.fn(), - }), - }, - { - provide: SystemWebhookService, useFactory: () => ({ - fetchSystemWebhooks: jest.fn(), - }), - }, - ], - }).compile(); - - usersRepository = app.get(DI.usersRepository); - userProfilesRepository = app.get(DI.userProfilesRepository); - - service = app.get(WebhookTestService); - idService = app.get(IdService); - queueService = app.get(QueueService) as jest.Mocked; - userWebhookService = app.get(UserWebhookService) as jest.Mocked; - systemWebhookService = app.get(SystemWebhookService) as jest.Mocked; - - app.enableShutdownHooks(); - }); - - beforeEach(async () => { - root = await createUser({ username: 'root', usernameLower: 'root' }); - alice = await createUser({ username: 'alice', usernameLower: 'alice' }); - - userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([ - { id: 'dummy-webhook', active: true, userId: alice.id } as MiWebhook, - ])); - systemWebhookService.fetchSystemWebhooks.mockReturnValue(Promise.resolve([ - { id: 'dummy-webhook', isActive: true } as MiSystemWebhook, - ])); - }); - - afterEach(async () => { - queueService.systemWebhookDeliver.mockClear(); - queueService.userWebhookDeliver.mockClear(); - userWebhookService.fetchWebhooks.mockClear(); - systemWebhookService.fetchSystemWebhooks.mockClear(); - - await usersRepository.createQueryBuilder().delete().execute(); - await userProfilesRepository.createQueryBuilder().delete().execute(); - }); - - afterAll(async () => { - await app.close(); - }); - - // -------------------------------------------------------------------------------------- - - describe('testUserWebhook', () => { - test('note', async () => { - await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'note' }, alice); - - const calls = queueService.userWebhookDeliver.mock.calls[0]; - expect((calls[0] as any).id).toBe('dummy-webhook'); - expect(calls[1]).toBe('note'); - expect((calls[2] as UserWebhookPayload<'note'>).note.id).toBe('dummy-note-1'); - }); - - test('reply', async () => { - await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'reply' }, alice); - - const calls = queueService.userWebhookDeliver.mock.calls[0]; - expect((calls[0] as any).id).toBe('dummy-webhook'); - expect(calls[1]).toBe('reply'); - expect((calls[2] as UserWebhookPayload<'reply'>).note.id).toBe('dummy-reply-1'); - }); - - test('renote', async () => { - await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'renote' }, alice); - - const calls = queueService.userWebhookDeliver.mock.calls[0]; - expect((calls[0] as any).id).toBe('dummy-webhook'); - expect(calls[1]).toBe('renote'); - expect((calls[2] as UserWebhookPayload<'renote'>).note.id).toBe('dummy-renote-1'); - }); - - test('mention', async () => { - await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'mention' }, alice); - - const calls = queueService.userWebhookDeliver.mock.calls[0]; - expect((calls[0] as any).id).toBe('dummy-webhook'); - expect(calls[1]).toBe('mention'); - expect((calls[2] as UserWebhookPayload<'mention'>).note.id).toBe('dummy-mention-1'); - }); - - test('follow', async () => { - await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'follow' }, alice); - - const calls = queueService.userWebhookDeliver.mock.calls[0]; - expect((calls[0] as any).id).toBe('dummy-webhook'); - expect(calls[1]).toBe('follow'); - expect((calls[2] as UserWebhookPayload<'follow'>).user.id).toBe('dummy-user-1'); - }); - - test('followed', async () => { - await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'followed' }, alice); - - const calls = queueService.userWebhookDeliver.mock.calls[0]; - expect((calls[0] as any).id).toBe('dummy-webhook'); - expect(calls[1]).toBe('followed'); - expect((calls[2] as UserWebhookPayload<'followed'>).user.id).toBe('dummy-user-2'); - }); - - test('unfollow', async () => { - await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'unfollow' }, alice); - - const calls = queueService.userWebhookDeliver.mock.calls[0]; - expect((calls[0] as any).id).toBe('dummy-webhook'); - expect(calls[1]).toBe('unfollow'); - expect((calls[2] as UserWebhookPayload<'unfollow'>).user.id).toBe('dummy-user-3'); - }); - - describe('NoSuchWebhookError', () => { - test('user not match', async () => { - userWebhookService.fetchWebhooks.mockClear(); - userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([ - { id: 'dummy-webhook', active: true } as MiWebhook, - ])); - - await expect(service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'note' }, root)) - .rejects.toThrow(WebhookTestService.NoSuchWebhookError); - }); - }); - }); - - describe('testSystemWebhook', () => { - test('abuseReport', async () => { - await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'abuseReport' }); - - const calls = queueService.systemWebhookDeliver.mock.calls[0]; - expect((calls[0] as any).id).toBe('dummy-webhook'); - expect(calls[1]).toBe('abuseReport'); - expect((calls[2] as any).id).toBe('dummy-abuse-report1'); - expect((calls[2] as any).resolved).toBe(false); - }); - - test('abuseReportResolved', async () => { - await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'abuseReportResolved' }); - - const calls = queueService.systemWebhookDeliver.mock.calls[0]; - expect((calls[0] as any).id).toBe('dummy-webhook'); - expect(calls[1]).toBe('abuseReportResolved'); - expect((calls[2] as any).id).toBe('dummy-abuse-report1'); - expect((calls[2] as any).resolved).toBe(true); - }); - - test('userCreated', async () => { - await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'userCreated' }); - - const calls = queueService.systemWebhookDeliver.mock.calls[0]; - expect((calls[0] as any).id).toBe('dummy-webhook'); - expect(calls[1]).toBe('userCreated'); - expect((calls[2] as any).id).toBe('dummy-user-1'); - }); - }); -}); diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index c6e09bdda2..7cd740a2fa 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -1,47 +1,26 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import * as fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; import { Test } from '@nestjs/testing'; import { jest } from '@jest/globals'; -import { MockResolver } from '../misc/mock-resolver.js'; -import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js'; -import type { MiRemoteUser } from '@/models/User.js'; -import { ApImageService } from '@/core/activitypub/models/ApImageService.js'; import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; -import { JsonLdService } from '@/core/activitypub/JsonLdService.js'; -import { CONTEXT } from '@/core/activitypub/misc/contexts.js'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { LoggerService } from '@/core/LoggerService.js'; -import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; +import type { IActor } from '@/core/activitypub/type.js'; +import { Note } from '@/models/index.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; -import { DownloadService } from '@/core/DownloadService.js'; -import { genAidx } from '@/misc/id/aidx.js'; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); +import { MockResolver } from '../misc/mock-resolver.js'; const host = 'https://host1.test'; -type NonTransientIActor = IActor & { id: string }; -type NonTransientIPost = IPost & { id: string }; - -function createRandomActor({ actorHost = host } = {}): NonTransientIActor { +function createRandomActor(): IActor & { id: string } { const preferredUsername = secureRndstr(8); - const actorId = `${actorHost}/users/${preferredUsername.toLowerCase()}`; + const actorId = `${host}/users/${preferredUsername.toLowerCase()}`; return { '@context': 'https://www.w3.org/ns/activitystreams', @@ -53,113 +32,28 @@ function createRandomActor({ actorHost = host } = {}): NonTransientIActor { }; } -function createRandomNote(actor: NonTransientIActor): NonTransientIPost { - const id = secureRndstr(8); - const noteId = `${new URL(actor.id).origin}/notes/${id}`; - - return { - id: noteId, - type: 'Note', - attributedTo: actor.id, - content: 'test test foo', - }; -} - -function createRandomNotes(actor: NonTransientIActor, length: number): NonTransientIPost[] { - return new Array(length).fill(null).map(() => createRandomNote(actor)); -} - -function createRandomFeaturedCollection(actor: NonTransientIActor, length: number): ICollection { - const items = createRandomNotes(actor, length); - - return { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Collection', - id: actor.outbox as string, - totalItems: items.length, - items, - }; -} - -async function createRandomRemoteUser( - resolver: MockResolver, - personService: ApPersonService, -): Promise { - const actor = createRandomActor(); - resolver.register(actor.id, actor); - - return await personService.createPerson(actor.id, resolver); -} - describe('ActivityPub', () => { - let userProfilesRepository: UserProfilesRepository; - let imageService: ApImageService; let noteService: ApNoteService; let personService: ApPersonService; let rendererService: ApRendererService; - let jsonLdService: JsonLdService; let resolver: MockResolver; - const metaInitial = { - cacheRemoteFiles: true, - cacheRemoteSensitiveFiles: true, - enableFanoutTimeline: true, - enableFanoutTimelineDbFallback: true, - perUserHomeTimelineCacheMax: 100, - perLocalUserUserTimelineCacheMax: 100, - perRemoteUserUserTimelineCacheMax: 100, - blockedHosts: [] as string[], - sensitiveWords: [] as string[], - prohibitedWords: [] as string[], - } as MiMeta; - const meta = { ...metaInitial }; - - function updateMeta(newMeta: Partial): void { - for (const key in meta) { - delete (meta as any)[key]; - } - Object.assign(meta, newMeta); - } - - beforeAll(async () => { + beforeEach(async () => { const app = await Test.createTestingModule({ imports: [GlobalModule, CoreModule], - }) - .overrideProvider(DownloadService).useValue({ - async downloadUrl(url: string, path: string): Promise<{ filename: string }> { - if (url.endsWith('.png')) { - fs.copyFileSync( - _dirname + '/../resources/hw.png', - path, - ); - } - return { - filename: 'dummy.tmp', - }; - }, - }) - .overrideProvider(DI.meta).useFactory({ factory: () => meta }) - .compile(); + }).compile(); await app.init(); app.enableShutdownHooks(); - userProfilesRepository = app.get(DI.userProfilesRepository); - noteService = app.get(ApNoteService); personService = app.get(ApPersonService); rendererService = app.get(ApRendererService); - imageService = app.get(ApImageService); - jsonLdService = app.get(JsonLdService); resolver = new MockResolver(await app.resolve(LoggerService)); // Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error const federatedInstanceService = app.get(FederatedInstanceService); - jest.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => { })); - }); - - beforeEach(() => { - resolver.clear(); + jest.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => {})); }); describe('Parse minimum object', () => { @@ -175,7 +69,7 @@ describe('ActivityPub', () => { }; test('Minimum Actor', async () => { - resolver.register(actor.id, actor); + resolver._register(actor.id, actor); const user = await personService.createPerson(actor.id, resolver); @@ -185,10 +79,10 @@ describe('ActivityPub', () => { }); test('Minimum Note', async () => { - resolver.register(actor.id, actor); - resolver.register(post.id, post); + resolver._register(actor.id, actor); + resolver._register(post.id, post); - const note = await noteService.createNote(post.id, undefined, resolver, true); + const note = await noteService.createNote(post.id, resolver, true); assert.deepStrictEqual(note?.uri, post.id); assert.deepStrictEqual(note.visibility, 'public'); @@ -203,7 +97,7 @@ describe('ActivityPub', () => { name: secureRndstr(129), }; - resolver.register(actor.id, actor); + resolver._register(actor.id, actor); const user = await personService.createPerson(actor.id, resolver); @@ -216,7 +110,7 @@ describe('ActivityPub', () => { name: '', }; - resolver.register(actor.id, actor); + resolver._register(actor.id, actor); const user = await personService.createPerson(actor.id, resolver); @@ -224,269 +118,12 @@ describe('ActivityPub', () => { }); }); - describe('Collection visibility', () => { - test('Public following/followers', async () => { - const actor = createRandomActor(); - actor.following = { - id: `${actor.id}/following`, - type: 'OrderedCollection', - totalItems: 0, - first: `${actor.id}/following?page=1`, - }; - actor.followers = `${actor.id}/followers`; - - resolver.register(actor.id, actor); - resolver.register(actor.followers, { - id: actor.followers, - type: 'OrderedCollection', - totalItems: 0, - first: `${actor.followers}?page=1`, - }); - - const user = await personService.createPerson(actor.id, resolver); - const userProfile = await userProfilesRepository.findOneByOrFail({ userId: user.id }); - - assert.deepStrictEqual(userProfile.followingVisibility, 'public'); - assert.deepStrictEqual(userProfile.followersVisibility, 'public'); - }); - - test('Private following/followers', async () => { - const actor = createRandomActor(); - actor.following = { - id: `${actor.id}/following`, - type: 'OrderedCollection', - totalItems: 0, - // first: … - }; - actor.followers = `${actor.id}/followers`; - - resolver.register(actor.id, actor); - //resolver.register(actor.followers, { … }); - - const user = await personService.createPerson(actor.id, resolver); - const userProfile = await userProfilesRepository.findOneByOrFail({ userId: user.id }); - - assert.deepStrictEqual(userProfile.followingVisibility, 'private'); - assert.deepStrictEqual(userProfile.followersVisibility, 'private'); - }); - }); - describe('Renderer', () => { test('Render an announce with visibility: followers', () => { - rendererService.renderAnnounce('https://example.com/notes/00example', { - id: genAidx(Date.now()), + rendererService.renderAnnounce(null, { + createdAt: new Date(0), visibility: 'followers', - } as MiNote); - }); - }); - - describe('Featured', () => { - test('Fetch featured notes from IActor', async () => { - const actor = createRandomActor(); - actor.featured = `${actor.id}/collections/featured`; - - const featured = createRandomFeaturedCollection(actor, 5); - - resolver.register(actor.id, actor); - resolver.register(actor.featured, featured); - - await personService.createPerson(actor.id, resolver); - - // All notes in `featured` are same-origin, no need to fetch notes again - assert.deepStrictEqual(resolver.remoteGetTrials(), [actor.id, actor.featured]); - - // Created notes without resolving anything - for (const item of featured.items as IPost[]) { - const note = await noteService.fetchNote(item); - assert.ok(note); - assert.strictEqual(note.text, 'test test foo'); - assert.strictEqual(note.uri, item.id); - } - }); - - test('Fetch featured notes from IActor pointing to another remote server', async () => { - const actor1 = createRandomActor(); - actor1.featured = `${actor1.id}/collections/featured`; - const actor2 = createRandomActor({ actorHost: 'https://host2.test' }); - - const actor2Note = createRandomNote(actor2); - const featured = createRandomFeaturedCollection(actor1, 0); - (featured.items as IPost[]).push({ - ...actor2Note, - content: 'test test bar', // fraud! - }); - - resolver.register(actor1.id, actor1); - resolver.register(actor1.featured, featured); - resolver.register(actor2.id, actor2); - resolver.register(actor2Note.id, actor2Note); - - await personService.createPerson(actor1.id, resolver); - - // actor2Note is from a different server and needs to be fetched again - assert.deepStrictEqual( - resolver.remoteGetTrials(), - [actor1.id, actor1.featured, actor2Note.id, actor2.id], - ); - - const note = await noteService.fetchNote(actor2Note.id); - assert.ok(note); - - // Reflects the original content instead of the fraud - assert.strictEqual(note.text, 'test test foo'); - assert.strictEqual(note.uri, actor2Note.id); - }); - - test('Fetch a note that is a featured note of the attributed actor', async () => { - const actor = createRandomActor(); - actor.featured = `${actor.id}/collections/featured`; - - const featured = createRandomFeaturedCollection(actor, 5); - const firstNote = (featured.items as NonTransientIPost[])[0]; - - resolver.register(actor.id, actor); - resolver.register(actor.featured, featured); - resolver.register(firstNote.id, firstNote); - - const note = await noteService.createNote(firstNote.id as string, undefined, resolver); - assert.strictEqual(note?.uri, firstNote.id); - }); - }); - - describe('Images', () => { - test('Create images', async () => { - const imageObject: IApDocument = { - type: 'Document', - mediaType: 'image/png', - url: 'http://host1.test/foo.png', - name: '', - }; - const driveFile = await imageService.createImage( - await createRandomRemoteUser(resolver, personService), - imageObject, - ); - assert.ok(driveFile && !driveFile.isLink); - - const sensitiveImageObject: IApDocument = { - type: 'Document', - mediaType: 'image/png', - url: 'http://host1.test/bar.png', - name: '', - sensitive: true, - }; - const sensitiveDriveFile = await imageService.createImage( - await createRandomRemoteUser(resolver, personService), - sensitiveImageObject, - ); - assert.ok(sensitiveDriveFile && !sensitiveDriveFile.isLink); - }); - - test('cacheRemoteFiles=false disables caching', async () => { - updateMeta({ ...metaInitial, cacheRemoteFiles: false }); - - const imageObject: IApDocument = { - type: 'Document', - mediaType: 'image/png', - url: 'http://host1.test/foo.png', - name: '', - }; - const driveFile = await imageService.createImage( - await createRandomRemoteUser(resolver, personService), - imageObject, - ); - assert.ok(driveFile && driveFile.isLink); - - const sensitiveImageObject: IApDocument = { - type: 'Document', - mediaType: 'image/png', - url: 'http://host1.test/bar.png', - name: '', - sensitive: true, - }; - const sensitiveDriveFile = await imageService.createImage( - await createRandomRemoteUser(resolver, personService), - sensitiveImageObject, - ); - assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink); - }); - - test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => { - updateMeta({ ...metaInitial, cacheRemoteSensitiveFiles: false }); - - const imageObject: IApDocument = { - type: 'Document', - mediaType: 'image/png', - url: 'http://host1.test/foo.png', - name: '', - }; - const driveFile = await imageService.createImage( - await createRandomRemoteUser(resolver, personService), - imageObject, - ); - assert.ok(driveFile && !driveFile.isLink); - - const sensitiveImageObject: IApDocument = { - type: 'Document', - mediaType: 'image/png', - url: 'http://host1.test/bar.png', - name: '', - sensitive: true, - }; - const sensitiveDriveFile = await imageService.createImage( - await createRandomRemoteUser(resolver, personService), - sensitiveImageObject, - ); - assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink); - }); - - test('Link is not an attachment files', async () => { - const linkObject: IObject = { - type: 'Link', - href: 'https://example.com/', - }; - const driveFile = await imageService.createImage( - await createRandomRemoteUser(resolver, personService), - linkObject, - ); - assert.strictEqual(driveFile, null); - }); - }); - - describe('JSON-LD', () => { - test('Compaction', async () => { - const jsonLd = jsonLdService.use(); - - const object = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - { - _misskey_quote: 'https://misskey-hub.net/ns#_misskey_quote', - unknown: 'https://example.org/ns#unknown', - undefined: null, - }, - ], - id: 'https://example.com/notes/42', - type: 'Note', - attributedTo: 'https://example.com/users/1', - to: ['https://www.w3.org/ns/activitystreams#Public'], - content: 'test test foo', - _misskey_quote: 'https://example.com/notes/1', - unknown: 'test test bar', - undefined: 'test test baz', - }; - const compacted = await jsonLd.compact(object); - - assert.deepStrictEqual(compacted, { - '@context': CONTEXT, - id: 'https://example.com/notes/42', - type: 'Note', - attributedTo: 'https://example.com/users/1', - to: 'as:Public', - content: 'test test foo', - _misskey_quote: 'https://example.com/notes/1', - 'https://example.org/ns#unknown': 'test test bar', - // undefined: 'test test baz', - }); + } as Note); }); }); }); diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts index f8b2a697f2..98f352e1c6 100644 --- a/packages/backend/test/unit/ap-request.ts +++ b/packages/backend/test/unit/ap-request.ts @@ -1,15 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as assert from 'assert'; import httpSignature from '@peertube/http-signature'; import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; -import { assertActivityMatchesUrl, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; -import { IObject } from '@/core/activitypub/type.js'; export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { return { @@ -26,10 +19,6 @@ export const buildParsedSignature = (signingString: string, signature: string, a }; }; -function cartesianProduct(a: T[], b: U[]): [T, U][] { - return a.flatMap(a => b.map(b => [a, b] as [T, U])); -} - describe('ap-request', () => { test('createSignedPost with verify', async () => { const keypair = await genRsaKeyPair(); @@ -64,108 +53,4 @@ describe('ap-request', () => { const result = httpSignature.verifySignature(parsed, keypair.publicKey); assert.deepStrictEqual(result, true); }); - - test('rejects non matching domain', () => { - assert.doesNotThrow(() => assertActivityMatchesUrl( - 'https://alice.example.com/abc', - { id: 'https://alice.example.com/abc' } as IObject, - 'https://alice.example.com/abc', - FetchAllowSoftFailMask.Strict, - ), 'validation should pass base case'); - assert.throws(() => assertActivityMatchesUrl( - 'https://alice.example.com/abc', - { id: 'https://bob.example.com/abc' } as IObject, - 'https://alice.example.com/abc', - FetchAllowSoftFailMask.Any, - ), 'validation should fail no matter what if the response URL is inconsistent with the object ID'); - - assert.doesNotThrow(() => assertActivityMatchesUrl( - 'https://alice.example.com/abc#test', - { id: 'https://alice.example.com/abc' } as IObject, - 'https://alice.example.com/abc', - FetchAllowSoftFailMask.Strict, - ), 'validation should pass with hash in request URL'); - - // fix issues like threads - // https://github.com/misskey-dev/misskey/issues/15039 - const withOrWithoutWWW = [ - 'https://alice.example.com/abc', - 'https://www.alice.example.com/abc', - ]; - - cartesianProduct( - cartesianProduct( - withOrWithoutWWW, - withOrWithoutWWW, - ), - withOrWithoutWWW, - ).forEach(([[a, b], c]) => { - assert.doesNotThrow(() => assertActivityMatchesUrl( - a, - { id: b } as IObject, - c, - FetchAllowSoftFailMask.Strict, - ), 'validation should pass with or without www. subdomain'); - }); - }); - - test('cross origin lookup', () => { - assert.doesNotThrow(() => assertActivityMatchesUrl( - 'https://alice.example.com/abc', - { id: 'https://bob.example.com/abc' } as IObject, - 'https://bob.example.com/abc', - FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId, - ), 'validation should pass if the response is otherwise consistent and cross-origin is allowed'); - assert.throws(() => assertActivityMatchesUrl( - 'https://alice.example.com/abc', - { id: 'https://bob.example.com/abc' } as IObject, - 'https://bob.example.com/abc', - FetchAllowSoftFailMask.Strict, - ), 'validation should fail if the response is otherwise consistent and cross-origin is not allowed'); - }); - - test('rejects non-canonical ID', () => { - assert.throws(() => assertActivityMatchesUrl( - 'https://alice.example.com/@alice', - { id: 'https://alice.example.com/users/alice' } as IObject, - 'https://alice.example.com/users/alice', - FetchAllowSoftFailMask.Strict, - ), 'throws if the response ID did not exactly match the expected ID'); - assert.doesNotThrow(() => assertActivityMatchesUrl( - 'https://alice.example.com/@alice', - { id: 'https://alice.example.com/users/alice' } as IObject, - 'https://alice.example.com/users/alice', - FetchAllowSoftFailMask.NonCanonicalId, - ), 'does not throw if non-canonical ID is allowed'); - }); - - test('origin relaxed alignment', () => { - assert.doesNotThrow(() => assertActivityMatchesUrl( - 'https://alice.example.com/abc', - { id: 'https://ap.alice.example.com/abc' } as IObject, - 'https://ap.alice.example.com/abc', - FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId, - ), 'validation should pass if response is a subdomain of the expected origin'); - assert.throws(() => assertActivityMatchesUrl( - 'https://alice.multi-tenant.example.com/abc', - { id: 'https://alice.multi-tenant.example.com/abc' } as IObject, - 'https://bob.multi-tenant.example.com/abc', - FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId, - ), 'validation should fail if response is a disjoint domain of the expected origin'); - assert.throws(() => assertActivityMatchesUrl( - 'https://alice.example.com/abc', - { id: 'https://ap.alice.example.com/abc' } as IObject, - 'https://ap.alice.example.com/abc', - FetchAllowSoftFailMask.Strict, - ), 'throws if relaxed origin is forbidden'); - }); - - test('resist HTTP downgrade', () => { - assert.throws(() => assertActivityMatchesUrl( - 'https://alice.example.com/abc', - { id: 'https://alice.example.com/abc' } as IObject, - 'http://alice.example.com/abc', - FetchAllowSoftFailMask.Strict, - ), 'throws if HTTP downgrade is detected'); - }); }); diff --git a/packages/backend/test/unit/chart.ts b/packages/backend/test/unit/chart.ts index 9dedd3a79d..5ac4cc18a2 100644 --- a/packages/backend/test/unit/chart.ts +++ b/packages/backend/test/unit/chart.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; @@ -18,7 +13,7 @@ import { entity as TestGroupedChartEntity } from '@/core/chart/charts/entities/t import { entity as TestUniqueChartEntity } from '@/core/chart/charts/entities/test-unique.js'; import { entity as TestIntersectionChartEntity } from '@/core/chart/charts/entities/test-intersection.js'; import { loadConfig } from '@/config.js'; -import type { AppLockService } from '@/core/AppLockService.js'; +import type { AppLockService } from '@/core/AppLockService'; import Logger from '@/logger.js'; describe('Chart', () => { @@ -480,16 +475,16 @@ describe('Chart', () => { await testIntersectionChart.addA('bob'); await testIntersectionChart.addB('carol'); await testIntersectionChart.save(); - + const chartHours = await testIntersectionChart.getChart('hour', 3, null); const chartDays = await testIntersectionChart.getChart('day', 3, null); - + assert.deepStrictEqual(chartHours, { a: [2, 0, 0], b: [1, 0, 0], aAndB: [0, 0, 0], }); - + assert.deepStrictEqual(chartDays, { a: [2, 0, 0], b: [1, 0, 0], @@ -503,16 +498,16 @@ describe('Chart', () => { await testIntersectionChart.addB('carol'); await testIntersectionChart.addB('alice'); await testIntersectionChart.save(); - + const chartHours = await testIntersectionChart.getChart('hour', 3, null); const chartDays = await testIntersectionChart.getChart('day', 3, null); - + assert.deepStrictEqual(chartHours, { a: [2, 0, 0], b: [2, 0, 0], aAndB: [1, 0, 0], }); - + assert.deepStrictEqual(chartDays, { a: [2, 0, 0], b: [2, 0, 0], diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts deleted file mode 100644 index ca6a639be8..0000000000 --- a/packages/backend/test/unit/entities/UserEntityService.ts +++ /dev/null @@ -1,532 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Test, TestingModule } from '@nestjs/testing'; -import type { MiUser } from '@/models/User.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { CoreModule } from '@/core/CoreModule.js'; -import { secureRndstr } from '@/misc/secure-rndstr.js'; -import { genAidx } from '@/misc/id/aidx.js'; -import { - BlockingsRepository, - FollowingsRepository, FollowRequestsRepository, - MiUserProfile, MutingsRepository, RenoteMutingsRepository, - UserMemoRepository, - UserProfilesRepository, - UsersRepository, -} from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; -import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; -import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { PageEntityService } from '@/core/entities/PageEntityService.js'; -import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import { AnnouncementService } from '@/core/AnnouncementService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; -import { IdService } from '@/core/IdService.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import { MetaService } from '@/core/MetaService.js'; -import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; -import { CacheService } from '@/core/CacheService.js'; -import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; -import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; -import { ApImageService } from '@/core/activitypub/models/ApImageService.js'; -import { ApMfmService } from '@/core/activitypub/ApMfmService.js'; -import { MfmService } from '@/core/MfmService.js'; -import { HashtagService } from '@/core/HashtagService.js'; -import UsersChart from '@/core/chart/charts/users.js'; -import { ChartLoggerService } from '@/core/chart/ChartLoggerService.js'; -import InstanceChart from '@/core/chart/charts/instance.js'; -import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js'; -import { AccountMoveService } from '@/core/AccountMoveService.js'; -import { ReactionService } from '@/core/ReactionService.js'; -import { NotificationService } from '@/core/NotificationService.js'; -import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; -import { ChatService } from '@/core/ChatService.js'; - -process.env.NODE_ENV = 'test'; - -describe('UserEntityService', () => { - describe('pack/packMany', () => { - let app: TestingModule; - let service: UserEntityService; - let usersRepository: UsersRepository; - let userProfileRepository: UserProfilesRepository; - let userMemosRepository: UserMemoRepository; - let followingRepository: FollowingsRepository; - let followingRequestRepository: FollowRequestsRepository; - let blockingRepository: BlockingsRepository; - let mutingRepository: MutingsRepository; - let renoteMutingsRepository: RenoteMutingsRepository; - - async function createUser(userData: Partial = {}, profileData: Partial = {}) { - const un = secureRndstr(16); - const user = await usersRepository - .insert({ - ...userData, - id: genAidx(Date.now()), - username: un, - usernameLower: un.toLowerCase(), - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - - await userProfileRepository.insert({ - ...profileData, - userId: user.id, - }); - - return user; - } - - async function memo(writer: MiUser, target: MiUser, memo: string) { - await userMemosRepository.insert({ - id: genAidx(Date.now()), - userId: writer.id, - targetUserId: target.id, - memo, - }); - } - - async function follow(follower: MiUser, followee: MiUser) { - await followingRepository.insert({ - id: genAidx(Date.now()), - followerId: follower.id, - followeeId: followee.id, - }); - } - - async function requestFollow(requester: MiUser, requestee: MiUser) { - await followingRequestRepository.insert({ - id: genAidx(Date.now()), - followerId: requester.id, - followeeId: requestee.id, - }); - } - - async function block(blocker: MiUser, blockee: MiUser) { - await blockingRepository.insert({ - id: genAidx(Date.now()), - blockerId: blocker.id, - blockeeId: blockee.id, - }); - } - - async function mute(mutant: MiUser, mutee: MiUser) { - await mutingRepository.insert({ - id: genAidx(Date.now()), - muterId: mutant.id, - muteeId: mutee.id, - }); - } - - async function muteRenote(mutant: MiUser, mutee: MiUser) { - await renoteMutingsRepository.insert({ - id: genAidx(Date.now()), - muterId: mutant.id, - muteeId: mutee.id, - }); - } - - function randomIntRange(weight = 10) { - return [...Array(Math.floor(Math.random() * weight))].map((it, idx) => idx); - } - - beforeAll(async () => { - const services = [ - UserEntityService, - ApPersonService, - NoteEntityService, - PageEntityService, - CustomEmojiService, - AnnouncementService, - RoleService, - FederatedInstanceService, - IdService, - AvatarDecorationService, - UtilityService, - EmojiEntityService, - ModerationLogService, - GlobalEventService, - DriveFileEntityService, - MetaService, - FetchInstanceMetadataService, - CacheService, - ApResolverService, - ApNoteService, - ApImageService, - ApMfmService, - MfmService, - HashtagService, - UsersChart, - ChartLoggerService, - InstanceChart, - ApLoggerService, - AccountMoveService, - ReactionService, - ReactionsBufferingService, - NotificationService, - ChatService, - ]; - - app = await Test.createTestingModule({ - imports: [GlobalModule, CoreModule], - providers: [ - ...services, - ...services.map(x => ({ provide: x.name, useExisting: x })), - ], - }).compile(); - await app.init(); - app.enableShutdownHooks(); - - service = app.get(UserEntityService); - usersRepository = app.get(DI.usersRepository); - userProfileRepository = app.get(DI.userProfilesRepository); - userMemosRepository = app.get(DI.userMemosRepository); - followingRepository = app.get(DI.followingsRepository); - followingRequestRepository = app.get(DI.followRequestsRepository); - blockingRepository = app.get(DI.blockingsRepository); - mutingRepository = app.get(DI.mutingsRepository); - renoteMutingsRepository = app.get(DI.renoteMutingsRepository); - }); - - afterAll(async () => { - await app.close(); - }); - - test('UserLite', async() => { - const me = await createUser(); - const who = await createUser(); - - await memo(me, who, 'memo'); - - const actual = await service.pack(who, me, { schema: 'UserLite' }) as any; - // no detail - expect(actual.memo).toBeUndefined(); - // no detail and me - expect(actual.birthday).toBeUndefined(); - // no detail and me - expect(actual.achievements).toBeUndefined(); - }); - - test('UserDetailedNotMe', async() => { - const me = await createUser(); - const who = await createUser({}, { birthday: '2000-01-01' }); - - await memo(me, who, 'memo'); - - const actual = await service.pack(who, me, { schema: 'UserDetailedNotMe' }) as any; - // is detail - expect(actual.memo).toBe('memo'); - // is detail - expect(actual.birthday).toBe('2000-01-01'); - // no detail and me - expect(actual.achievements).toBeUndefined(); - }); - - test('MeDetailed', async() => { - const achievements = [{ name: 'iLoveMisskey' as const, unlockedAt: new Date().getTime() }]; - const me = await createUser({}, { - birthday: '2000-01-01', - achievements: achievements, - }); - await memo(me, me, 'memo'); - - const actual = await service.pack(me, me, { schema: 'MeDetailed' }) as any; - // is detail - expect(actual.memo).toBe('memo'); - // is detail - expect(actual.birthday).toBe('2000-01-01'); - // is detail and me - expect(actual.achievements).toEqual(achievements); - }); - - describe('packManyによるpreloadがある時、preloadが無い時とpackの結果が同じになるか見たい', () => { - test('no-preload', async() => { - const me = await createUser(); - // meがフォローしてる人たち - const followeeMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of followeeMe) { - await follow(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(true); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meをフォローしてる人たち - const followerMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of followerMe) { - await follow(who, me); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(true); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meがフォローリクエストを送った人たち - const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of requestsFromYou) { - await requestFollow(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(true); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meにフォローリクエストを送った人たち - const requestsToYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of requestsToYou) { - await requestFollow(who, me); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(true); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meがブロックしてる人たち - const blockingYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of blockingYou) { - await block(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(true); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meをブロックしてる人たち - const blockingMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of blockingMe) { - await block(who, me); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(true); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meがミュートしてる人たち - const muters = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of muters) { - await mute(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(true); - expect(actual.isRenoteMuted).toBe(false); - } - - // meがリノートミュートしてる人たち - const renoteMuters = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of renoteMuters) { - await muteRenote(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(true); - } - }); - - test('preload', async() => { - const me = await createUser(); - - { - // meがフォローしてる人たち - const followeeMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of followeeMe) { - await follow(me, who); - } - const actualList = await service.packMany(followeeMe, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(true); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meをフォローしてる人たち - const followerMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of followerMe) { - await follow(who, me); - } - const actualList = await service.packMany(followerMe, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(true); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meがフォローリクエストを送った人たち - const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of requestsFromYou) { - await requestFollow(me, who); - } - const actualList = await service.packMany(requestsFromYou, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(true); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meにフォローリクエストを送った人たち - const requestsToYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of requestsToYou) { - await requestFollow(who, me); - } - const actualList = await service.packMany(requestsToYou, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(true); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meがブロックしてる人たち - const blockingYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of blockingYou) { - await block(me, who); - } - const actualList = await service.packMany(blockingYou, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(true); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meをブロックしてる人たち - const blockingMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of blockingMe) { - await block(who, me); - } - const actualList = await service.packMany(blockingMe, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(true); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meがミュートしてる人たち - const muters = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of muters) { - await mute(me, who); - } - const actualList = await service.packMany(muters, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(true); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meがリノートミュートしてる人たち - const renoteMuters = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of renoteMuters) { - await muteRenote(me, who); - } - const actualList = await service.packMany(renoteMuters, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(true); - } - } - }); - }); - }); -}); diff --git a/packages/backend/test/unit/extract-mentions.ts b/packages/backend/test/unit/extract-mentions.ts index 3403387e30..66d32be1c5 100644 --- a/packages/backend/test/unit/extract-mentions.ts +++ b/packages/backend/test/unit/extract-mentions.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as assert from 'assert'; import { parse } from 'mfm-js'; diff --git a/packages/backend/test/unit/misc/check-word-mute.ts b/packages/backend/test/unit/misc/check-word-mute.ts index eb0ca0f6cf..7ab838bdee 100644 --- a/packages/backend/test/unit/misc/check-word-mute.ts +++ b/packages/backend/test/unit/misc/check-word-mute.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { checkWordMute } from '@/misc/check-word-mute.js'; describe(checkWordMute, () => { diff --git a/packages/backend/test/unit/misc/correct-filename.ts b/packages/backend/test/unit/misc/correct-filename.ts deleted file mode 100644 index c76fb4c494..0000000000 --- a/packages/backend/test/unit/misc/correct-filename.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { correctFilename } from '@/misc/correct-filename.js'; - -describe(correctFilename, () => { - it('no ext to null', () => { - expect(correctFilename('test', null)).toBe('test.unknown'); - }); - it('no ext to jpg', () => { - expect(correctFilename('test', 'jpg')).toBe('test.jpg'); - }); - it('jpg to webp', () => { - expect(correctFilename('test.jpg', 'webp')).toBe('test.jpg.webp'); - }); - it('jpg to .webp', () => { - expect(correctFilename('test.jpg', '.webp')).toBe('test.jpg.webp'); - }); - it('jpeg to jpg', () => { - expect(correctFilename('test.jpeg', 'jpg')).toBe('test.jpeg'); - }); - it('JPEG to jpg', () => { - expect(correctFilename('test.JPEG', 'jpg')).toBe('test.JPEG'); - }); - it('jpg to jpg', () => { - expect(correctFilename('test.jpg', 'jpg')).toBe('test.jpg'); - }); - it('JPG to jpg', () => { - expect(correctFilename('test.JPG', 'jpg')).toBe('test.JPG'); - }); - it('tiff to tif', () => { - expect(correctFilename('test.tiff', 'tif')).toBe('test.tiff'); - }); - it('skip gz', () => { - expect(correctFilename('test.unitypackage', 'gz')).toBe('test.unitypackage'); - }); - it('skip text file', () => { - expect(correctFilename('test.txt', null)).toBe('test.txt'); - }); - it('unknown', () => { - expect(correctFilename('test.hoge', null)).toBe('test.hoge'); - }); - test('non ascii with space', () => { - expect(correctFilename('ファイル 名前', 'jpg')).toBe('ファイル 名前.jpg'); - }); -}); diff --git a/packages/backend/test/unit/misc/id.ts b/packages/backend/test/unit/misc/id.ts index d14efb10a6..ecd0e60a31 100644 --- a/packages/backend/test/unit/misc/id.ts +++ b/packages/backend/test/unit/misc/id.ts @@ -1,57 +1,44 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ulid } from 'ulid'; -import { describe, expect, test } from '@jest/globals'; import { aidRegExp, genAid, parseAid } from '@/misc/id/aid.js'; -import { aidxRegExp, genAidx, parseAidx } from '@/misc/id/aidx.js'; import { genMeid, meidRegExp, parseMeid } from '@/misc/id/meid.js'; import { genMeidg, meidgRegExp, parseMeidg } from '@/misc/id/meidg.js'; import { genObjectId, objectIdRegExp, parseObjectId } from '@/misc/id/object-id.js'; -import { parseUlid, ulidRegExp } from '@/misc/id/ulid.js'; +import { ulidRegExp, parseUlid } from '@/misc/id/ulid.js'; +import { ulid } from 'ulid'; +import { describe, test, expect } from '@jest/globals'; describe('misc:id', () => { - test('aid', () => { - const date = Date.now(); - const gotAid = genAid(date); - expect(gotAid).toMatch(aidRegExp); - expect(parseAid(gotAid).date.getTime()).toBe(date); - }); + test('aid', () => { + const date = new Date(); + const gotAid = genAid(date); + expect(gotAid).toMatch(aidRegExp); + expect(parseAid(gotAid).date.getTime()).toBe(date.getTime()); + }); - test('aidx', () => { - const date = Date.now(); - const gotAidx = genAidx(date); - expect(gotAidx).toMatch(aidxRegExp); - expect(parseAidx(gotAidx).date.getTime()).toBe(date); - }); + test('meid', () => { + const date = new Date(); + const gotMeid = genMeid(date); + expect(gotMeid).toMatch(meidRegExp); + expect(parseMeid(gotMeid).date.getTime()).toBe(date.getTime()); + }); - test('meid', () => { - const date = Date.now(); - const gotMeid = genMeid(date); - expect(gotMeid).toMatch(meidRegExp); - expect(parseMeid(gotMeid).date.getTime()).toBe(date); - }); + test('meidg', () => { + const date = new Date(); + const gotMeidg = genMeidg(date); + expect(gotMeidg).toMatch(meidgRegExp); + expect(parseMeidg(gotMeidg).date.getTime()).toBe(date.getTime()); + }); - test('meidg', () => { - const date = Date.now(); - const gotMeidg = genMeidg(date); - expect(gotMeidg).toMatch(meidgRegExp); - expect(parseMeidg(gotMeidg).date.getTime()).toBe(date); - }); + test('objectid', () => { + const date = new Date(); + const gotObjectId = genObjectId(date); + expect(gotObjectId).toMatch(objectIdRegExp); + expect(Math.floor(parseObjectId(gotObjectId).date.getTime() / 1000)).toBe(Math.floor(date.getTime() / 1000)); + }); - test('objectid', () => { - const date = Date.now(); - const gotObjectId = genObjectId(date); - expect(gotObjectId).toMatch(objectIdRegExp); - expect(Math.floor(parseObjectId(gotObjectId).date.getTime() / 1000)).toBe(Math.floor(date / 1000)); - }); - - test('ulid', () => { - const date = Date.now(); - const gotUlid = ulid(date); - expect(gotUlid).toMatch(ulidRegExp); - expect(parseUlid(gotUlid).date.getTime()).toBe(date); - }); + test('ulid', () => { + const date = new Date(); + const gotUlid = ulid(date.getTime()); + expect(gotUlid).toMatch(ulidRegExp); + expect(parseUlid(gotUlid).date.getTime()).toBe(date.getTime()); + }); }); diff --git a/packages/backend/test/unit/misc/is-renote.ts b/packages/backend/test/unit/misc/is-renote.ts deleted file mode 100644 index 0b713e8bf6..0000000000 --- a/packages/backend/test/unit/misc/is-renote.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { isQuote, isRenote } from '@/misc/is-renote.js'; -import { MiNote } from '@/models/Note.js'; - -const base: MiNote = { - id: 'some-note-id', - replyId: null, - reply: null, - renoteId: null, - renote: null, - threadId: null, - text: null, - name: null, - cw: null, - userId: 'some-user-id', - user: null, - localOnly: false, - reactionAcceptance: null, - renoteCount: 0, - repliesCount: 0, - clippedCount: 0, - reactions: {}, - visibility: 'public', - uri: null, - url: null, - fileIds: [], - attachedFileTypes: [], - visibleUserIds: [], - mentions: [], - mentionedRemoteUsers: '', - reactionAndUserPairCache: [], - emojis: [], - tags: [], - hasPoll: false, - channelId: null, - channel: null, - userHost: null, - replyUserId: null, - replyUserHost: null, - renoteUserId: null, - renoteUserHost: null, -}; - -describe('misc:is-renote', () => { - test('note without renoteId should not be Renote', () => { - expect(isRenote(base)).toBe(false); - }); - - test('note with renoteId should be Renote and not be Quote', () => { - const note: MiNote = { ...base, renoteId: 'some-renote-id' }; - expect(isRenote(note)).toBe(true); - expect(isQuote(note as any)).toBe(false); - }); - - test('note with renoteId and text should be Quote', () => { - const note: MiNote = { ...base, renoteId: 'some-renote-id', text: 'some-text' }; - expect(isRenote(note)).toBe(true); - expect(isQuote(note as any)).toBe(true); - }); - - test('note with renoteId and cw should be Quote', () => { - const note: MiNote = { ...base, renoteId: 'some-renote-id', cw: 'some-cw' }; - expect(isRenote(note)).toBe(true); - expect(isQuote(note as any)).toBe(true); - }); - - test('note with renoteId and replyId should be Quote', () => { - const note: MiNote = { ...base, renoteId: 'some-renote-id', replyId: 'some-reply-id' }; - expect(isRenote(note)).toBe(true); - expect(isQuote(note as any)).toBe(true); - }); - - test('note with renoteId and poll should be Quote', () => { - const note: MiNote = { ...base, renoteId: 'some-renote-id', hasPoll: true }; - expect(isRenote(note)).toBe(true); - expect(isQuote(note as any)).toBe(true); - }); - - test('note with renoteId and non-empty fileIds should be Quote', () => { - const note: MiNote = { ...base, renoteId: 'some-renote-id', fileIds: ['some-file-id'] }; - expect(isRenote(note)).toBe(true); - expect(isQuote(note as any)).toBe(true); - }); -}); diff --git a/packages/backend/test/unit/misc/loader.ts b/packages/backend/test/unit/misc/loader.ts deleted file mode 100644 index 2cf54e1555..0000000000 --- a/packages/backend/test/unit/misc/loader.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { DebounceLoader } from '@/misc/loader.js'; - -class Mock { - loadCountByKey = new Map(); - load = async (key: number): Promise => { - const count = this.loadCountByKey.get(key); - if (typeof count === 'undefined') { - this.loadCountByKey.set(key, 1); - } else { - this.loadCountByKey.set(key, count + 1); - } - return key * 2; - }; - reset() { - this.loadCountByKey.clear(); - } -} - -describe(DebounceLoader, () => { - describe('single request', () => { - it('loads once', async () => { - const mock = new Mock(); - const loader = new DebounceLoader(mock.load); - expect(await loader.load(7)).toBe(14); - expect(mock.loadCountByKey.size).toBe(1); - expect(mock.loadCountByKey.get(7)).toBe(1); - }); - }); - - describe('two duplicated requests at same time', () => { - it('loads once', async () => { - const mock = new Mock(); - const loader = new DebounceLoader(mock.load); - const [v1, v2] = await Promise.all([ - loader.load(7), - loader.load(7), - ]); - expect(v1).toBe(14); - expect(v2).toBe(14); - expect(mock.loadCountByKey.size).toBe(1); - expect(mock.loadCountByKey.get(7)).toBe(1); - }); - }); - - describe('two different requests at same time', () => { - it('loads twice', async () => { - const mock = new Mock(); - const loader = new DebounceLoader(mock.load); - const [v1, v2] = await Promise.all([ - loader.load(7), - loader.load(13), - ]); - expect(v1).toBe(14); - expect(v2).toBe(26); - expect(mock.loadCountByKey.size).toBe(2); - expect(mock.loadCountByKey.get(7)).toBe(1); - expect(mock.loadCountByKey.get(13)).toBe(1); - }); - }); - - describe('non-continuous same two requests', () => { - it('loads twice', async () => { - const mock = new Mock(); - const loader = new DebounceLoader(mock.load); - expect(await loader.load(7)).toBe(14); - expect(mock.loadCountByKey.size).toBe(1); - expect(mock.loadCountByKey.get(7)).toBe(1); - mock.reset(); - expect(await loader.load(7)).toBe(14); - expect(mock.loadCountByKey.size).toBe(1); - expect(mock.loadCountByKey.get(7)).toBe(1); - }); - }); - - describe('non-continuous different two requests', () => { - it('loads twice', async () => { - const mock = new Mock(); - const loader = new DebounceLoader(mock.load); - expect(await loader.load(7)).toBe(14); - expect(mock.loadCountByKey.size).toBe(1); - expect(mock.loadCountByKey.get(7)).toBe(1); - mock.reset(); - expect(await loader.load(13)).toBe(26); - expect(mock.loadCountByKey.size).toBe(1); - expect(mock.loadCountByKey.get(13)).toBe(1); - }); - }); -}); diff --git a/packages/backend/test/unit/misc/others.ts b/packages/backend/test/unit/misc/others.ts index 3bc134a2b8..c476aef33b 100644 --- a/packages/backend/test/unit/misc/others.ts +++ b/packages/backend/test/unit/misc/others.ts @@ -1,19 +1,42 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { describe, expect, test } from '@jest/globals'; +import { describe, test, expect } from '@jest/globals'; import { contentDisposition } from '@/misc/content-disposition.js'; +import { correctFilename } from '@/misc/correct-filename.js'; describe('misc:content-disposition', () => { - test('inline', () => { - expect(contentDisposition('inline', 'foo bar')).toBe('inline; filename=\"foo_bar\"; filename*=UTF-8\'\'foo%20bar'); - }); - test('attachment', () => { - expect(contentDisposition('attachment', 'foo bar')).toBe('attachment; filename=\"foo_bar\"; filename*=UTF-8\'\'foo%20bar'); - }); - test('non ascii', () => { - expect(contentDisposition('attachment', 'ファイル名')).toBe('attachment; filename=\"_____\"; filename*=UTF-8\'\'%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D'); - }); + test('inline', () => { + expect(contentDisposition('inline', 'foo bar')).toBe('inline; filename=\"foo_bar\"; filename*=UTF-8\'\'foo%20bar'); + }); + test('attachment', () => { + expect(contentDisposition('attachment', 'foo bar')).toBe('attachment; filename=\"foo_bar\"; filename*=UTF-8\'\'foo%20bar'); + }); + test('non ascii', () => { + expect(contentDisposition('attachment', 'ファイル名')).toBe('attachment; filename=\"_____\"; filename*=UTF-8\'\'%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D'); + }); +}); + +describe('misc:correct-filename', () => { + test('simple', () => { + expect(correctFilename('filename', 'jpg')).toBe('filename.jpg'); + }); + test('with same ext', () => { + expect(correctFilename('filename.jpg', 'jpg')).toBe('filename.jpg'); + }); + test('.ext', () => { + expect(correctFilename('filename.jpg', '.jpg')).toBe('filename.jpg'); + }); + test('with different ext', () => { + expect(correctFilename('filename.webp', 'jpg')).toBe('filename.webp.jpg'); + }); + test('non ascii with space', () => { + expect(correctFilename('ファイル 名前', 'jpg')).toBe('ファイル 名前.jpg'); + }); + test('jpeg', () => { + expect(correctFilename('filename.jpeg', 'jpg')).toBe('filename.jpeg'); + }); + test('tiff', () => { + expect(correctFilename('filename.tiff', 'tif')).toBe('filename.tiff'); + }); + test('null ext', () => { + expect(correctFilename('filename', null)).toBe('filename.unknown'); + }); }); diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts deleted file mode 100644 index 211846eef2..0000000000 --- a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts +++ /dev/null @@ -1,383 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { jest } from '@jest/globals'; -import { Test, TestingModule } from '@nestjs/testing'; -import * as lolex from '@sinonjs/fake-timers'; -import { addHours, addSeconds, subDays, subHours, subSeconds } from 'date-fns'; -import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; -import { MiSystemWebhook, MiUser, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { MetaService } from '@/core/MetaService.js'; -import { DI } from '@/di-symbols.js'; -import { QueueLoggerService } from '@/queue/QueueLoggerService.js'; -import { EmailService } from '@/core/EmailService.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { AnnouncementService } from '@/core/AnnouncementService.js'; -import { SystemWebhookEventType } from '@/models/SystemWebhook.js'; - -const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0)); - -describe('CheckModeratorsActivityProcessorService', () => { - let app: TestingModule; - let clock: lolex.InstalledClock; - let service: CheckModeratorsActivityProcessorService; - - // -------------------------------------------------------------------------------------- - - let usersRepository: UsersRepository; - let userProfilesRepository: UserProfilesRepository; - let idService: IdService; - let roleService: jest.Mocked; - let announcementService: jest.Mocked; - let emailService: jest.Mocked; - let systemWebhookService: jest.Mocked; - - let systemWebhook1: MiSystemWebhook; - let systemWebhook2: MiSystemWebhook; - let systemWebhook3: MiSystemWebhook; - - // -------------------------------------------------------------------------------------- - - async function createUser(data: Partial = {}, profile: Partial = {}): Promise { - const id = idService.gen(); - const user = await usersRepository - .insert({ - id: id, - username: `user_${id}`, - usernameLower: `user_${id}`.toLowerCase(), - ...data, - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - - await userProfilesRepository.insert({ - userId: user.id, - ...profile, - }); - - return user; - } - - function crateSystemWebhook(data: Partial = {}): MiSystemWebhook { - return { - id: idService.gen(), - isActive: true, - updatedAt: new Date(), - latestSentAt: null, - latestStatus: null, - name: 'test', - url: 'https://example.com', - secret: 'test', - on: [], - ...data, - }; - } - - function mockModeratorRole(users: MiUser[]) { - roleService.getModerators.mockReset(); - roleService.getModerators.mockResolvedValue(users); - } - - // -------------------------------------------------------------------------------------- - - beforeAll(async () => { - app = await Test - .createTestingModule({ - imports: [ - GlobalModule, - ], - providers: [ - CheckModeratorsActivityProcessorService, - IdService, - { - provide: RoleService, useFactory: () => ({ getModerators: jest.fn() }), - }, - { - provide: MetaService, useFactory: () => ({ fetch: jest.fn() }), - }, - { - provide: AnnouncementService, useFactory: () => ({ create: jest.fn() }), - }, - { - provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }), - }, - { - provide: SystemWebhookService, useFactory: () => ({ - fetchActiveSystemWebhooks: jest.fn(), - enqueueSystemWebhook: jest.fn(), - }), - }, - { - provide: QueueLoggerService, useFactory: () => ({ - logger: ({ - createSubLogger: () => ({ - info: jest.fn(), - warn: jest.fn(), - succ: jest.fn(), - }), - }), - }), - }, - ], - }) - .compile(); - - usersRepository = app.get(DI.usersRepository); - userProfilesRepository = app.get(DI.userProfilesRepository); - - service = app.get(CheckModeratorsActivityProcessorService); - idService = app.get(IdService); - roleService = app.get(RoleService) as jest.Mocked; - announcementService = app.get(AnnouncementService) as jest.Mocked; - emailService = app.get(EmailService) as jest.Mocked; - systemWebhookService = app.get(SystemWebhookService) as jest.Mocked; - - app.enableShutdownHooks(); - }); - - beforeEach(async () => { - clock = lolex.install({ - now: new Date(baseDate), - shouldClearNativeTimers: true, - }); - - systemWebhook1 = crateSystemWebhook({ on: ['inactiveModeratorsWarning'] }); - systemWebhook2 = crateSystemWebhook({ on: ['inactiveModeratorsWarning', 'inactiveModeratorsInvitationOnlyChanged'] }); - systemWebhook3 = crateSystemWebhook({ on: ['abuseReport'] }); - - emailService.sendEmail.mockReturnValue(Promise.resolve()); - announcementService.create.mockReturnValue(Promise.resolve({} as never)); - systemWebhookService.fetchActiveSystemWebhooks.mockResolvedValue([systemWebhook1, systemWebhook2, systemWebhook3]); - systemWebhookService.enqueueSystemWebhook.mockReturnValue(Promise.resolve({} as never)); - }); - - afterEach(async () => { - clock.uninstall(); - await usersRepository.createQueryBuilder().delete().execute(); - await userProfilesRepository.createQueryBuilder().delete().execute(); - roleService.getModerators.mockReset(); - announcementService.create.mockReset(); - emailService.sendEmail.mockReset(); - systemWebhookService.enqueueSystemWebhook.mockReset(); - }); - - afterAll(async () => { - await app.close(); - }); - - // -------------------------------------------------------------------------------------- - - describe('evaluateModeratorsInactiveDays', () => { - test('[isModeratorsInactive] inactiveなモデレーターがいても他のモデレーターがアクティブなら"運営が非アクティブ"としてみなされない', async () => { - const [user1, user2, user3, user4] = await Promise.all([ - // 期限よりも1秒新しいタイミングでアクティブ化(セーフ) - createUser({ lastActiveDate: subDays(addSeconds(baseDate, 1), 7) }), - // 期限ちょうどにアクティブ化(セーフ) - createUser({ lastActiveDate: subDays(baseDate, 7) }), - // 期限よりも1秒古いタイミングでアクティブ化(アウト) - createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }), - // 対象外 - createUser({ lastActiveDate: null }), - ]); - - mockModeratorRole([user1, user2, user3, user4]); - - const result = await service.evaluateModeratorsInactiveDays(); - expect(result.isModeratorsInactive).toBe(false); - expect(result.inactiveModerators).toEqual([user3]); - }); - - test('[isModeratorsInactive] 全員非アクティブなら"運営が非アクティブ"としてみなされる', async () => { - const [user1, user2] = await Promise.all([ - // 期限よりも1秒古いタイミングでアクティブ化(アウト) - createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }), - // 対象外 - createUser({ lastActiveDate: null }), - ]); - - mockModeratorRole([user1, user2]); - - const result = await service.evaluateModeratorsInactiveDays(); - expect(result.isModeratorsInactive).toBe(true); - expect(result.inactiveModerators).toEqual([user1]); - }); - - test('[remainingTime] 猶予まで24時間ある場合、猶予1日として計算される', async () => { - const [user1, user2] = await Promise.all([ - createUser({ lastActiveDate: subDays(baseDate, 8) }), - // 猶予はこのユーザ基準で計算される想定。 - // 期限まで残り24時間->猶予1日として計算されるはずである - createUser({ lastActiveDate: subDays(baseDate, 6) }), - ]); - - mockModeratorRole([user1, user2]); - - const result = await service.evaluateModeratorsInactiveDays(); - expect(result.isModeratorsInactive).toBe(false); - expect(result.inactiveModerators).toEqual([user1]); - expect(result.remainingTime.asDays).toBe(1); - expect(result.remainingTime.asHours).toBe(24); - }); - - test('[remainingTime] 猶予まで25時間ある場合、猶予1日として計算される', async () => { - const [user1, user2] = await Promise.all([ - createUser({ lastActiveDate: subDays(baseDate, 8) }), - // 猶予はこのユーザ基準で計算される想定。 - // 期限まで残り25時間->猶予1日として計算されるはずである - createUser({ lastActiveDate: subDays(addHours(baseDate, 1), 6) }), - ]); - - mockModeratorRole([user1, user2]); - - const result = await service.evaluateModeratorsInactiveDays(); - expect(result.isModeratorsInactive).toBe(false); - expect(result.inactiveModerators).toEqual([user1]); - expect(result.remainingTime.asDays).toBe(1); - expect(result.remainingTime.asHours).toBe(25); - }); - - test('[remainingTime] 猶予まで23時間ある場合、猶予0日として計算される', async () => { - const [user1, user2] = await Promise.all([ - createUser({ lastActiveDate: subDays(baseDate, 8) }), - // 猶予はこのユーザ基準で計算される想定。 - // 期限まで残り23時間->猶予0日として計算されるはずである - createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 6) }), - ]); - - mockModeratorRole([user1, user2]); - - const result = await service.evaluateModeratorsInactiveDays(); - expect(result.isModeratorsInactive).toBe(false); - expect(result.inactiveModerators).toEqual([user1]); - expect(result.remainingTime.asDays).toBe(0); - expect(result.remainingTime.asHours).toBe(23); - }); - - test('[remainingTime] 期限ちょうどの場合、猶予0日として計算される', async () => { - const [user1, user2] = await Promise.all([ - createUser({ lastActiveDate: subDays(baseDate, 8) }), - // 猶予はこのユーザ基準で計算される想定。 - // 期限ちょうど->猶予0日として計算されるはずである - createUser({ lastActiveDate: subDays(baseDate, 7) }), - ]); - - mockModeratorRole([user1, user2]); - - const result = await service.evaluateModeratorsInactiveDays(); - expect(result.isModeratorsInactive).toBe(false); - expect(result.inactiveModerators).toEqual([user1]); - expect(result.remainingTime.asDays).toBe(0); - expect(result.remainingTime.asHours).toBe(0); - }); - - test('[remainingTime] 期限より1時間超過している場合、猶予-1日として計算される', async () => { - const [user1, user2] = await Promise.all([ - createUser({ lastActiveDate: subDays(baseDate, 8) }), - // 猶予はこのユーザ基準で計算される想定。 - // 期限より1時間超過->猶予-1日として計算されるはずである - createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 7) }), - ]); - - mockModeratorRole([user1, user2]); - - const result = await service.evaluateModeratorsInactiveDays(); - expect(result.isModeratorsInactive).toBe(true); - expect(result.inactiveModerators).toEqual([user1, user2]); - expect(result.remainingTime.asDays).toBe(-1); - expect(result.remainingTime.asHours).toBe(-1); - }); - - test('[remainingTime] 期限より25時間超過している場合、猶予-2日として計算される', async () => { - const [user1, user2] = await Promise.all([ - createUser({ lastActiveDate: subDays(baseDate, 10) }), - // 猶予はこのユーザ基準で計算される想定。 - // 期限より1時間超過->猶予-1日として計算されるはずである - createUser({ lastActiveDate: subDays(subHours(baseDate, 25), 7) }), - ]); - - mockModeratorRole([user1, user2]); - - const result = await service.evaluateModeratorsInactiveDays(); - expect(result.isModeratorsInactive).toBe(true); - expect(result.inactiveModerators).toEqual([user1, user2]); - expect(result.remainingTime.asDays).toBe(-2); - expect(result.remainingTime.asHours).toBe(-25); - }); - }); - - describe('notifyInactiveModeratorsWarning', () => { - test('[notification + mail] 通知はモデレータ全員に発信され、メールはメールアドレスが存在+認証済みの場合のみ', async () => { - const [user1, user2, user3, user4, root] = await Promise.all([ - createUser({}, { email: 'user1@example.com', emailVerified: true }), - createUser({}, { email: 'user2@example.com', emailVerified: false }), - createUser({}, { email: null, emailVerified: false }), - createUser({}, { email: 'user4@example.com', emailVerified: true }), - createUser({}, { email: 'root@example.com', emailVerified: true }), - ]); - - mockModeratorRole([user1, user2, user3, root]); - await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 }); - - expect(emailService.sendEmail).toHaveBeenCalledTimes(2); - expect(emailService.sendEmail.mock.calls[0][0]).toBe('user1@example.com'); - expect(emailService.sendEmail.mock.calls[1][0]).toBe('root@example.com'); - }); - - test('[systemWebhook] "inactiveModeratorsWarning"が有効なSystemWebhookに対して送信される', async () => { - const [user1] = await Promise.all([ - createUser({}, { email: 'user1@example.com', emailVerified: true }), - ]); - - mockModeratorRole([user1]); - await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 }); - - // typeとactiveによる絞り込みが機能しているかはSystemWebhookServiceのテストで確認する. - // ここでは呼び出されているか、typeが正しいかのみを確認する - expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1); - expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0] as SystemWebhookEventType).toEqual('inactiveModeratorsWarning'); - }); - }); - - describe('notifyChangeToInvitationOnly', () => { - test('[notification + mail] 通知はモデレータ全員に発信され、メールはメールアドレスが存在+認証済みの場合のみ', async () => { - const [user1, user2, user3, user4, root] = await Promise.all([ - createUser({}, { email: 'user1@example.com', emailVerified: true }), - createUser({}, { email: 'user2@example.com', emailVerified: false }), - createUser({}, { email: null, emailVerified: false }), - createUser({}, { email: 'user4@example.com', emailVerified: true }), - createUser({}, { email: 'root@example.com', emailVerified: true }), - ]); - - mockModeratorRole([user1, user2, user3, root]); - await service.notifyChangeToInvitationOnly(); - - expect(announcementService.create).toHaveBeenCalledTimes(4); - expect(announcementService.create.mock.calls[0][0].userId).toBe(user1.id); - expect(announcementService.create.mock.calls[1][0].userId).toBe(user2.id); - expect(announcementService.create.mock.calls[2][0].userId).toBe(user3.id); - expect(announcementService.create.mock.calls[3][0].userId).toBe(root.id); - - expect(emailService.sendEmail).toHaveBeenCalledTimes(2); - expect(emailService.sendEmail.mock.calls[0][0]).toBe('user1@example.com'); - expect(emailService.sendEmail.mock.calls[1][0]).toBe('root@example.com'); - }); - - test('[systemWebhook] "inactiveModeratorsInvitationOnlyChanged"が有効なSystemWebhookに対して送信される', async () => { - const [user1] = await Promise.all([ - createUser({}, { email: 'user1@example.com', emailVerified: true }), - ]); - - mockModeratorRole([user1]); - await service.notifyChangeToInvitationOnly(); - - // typeとactiveによる絞り込みが機能しているかはSystemWebhookServiceのテストで確認する. - // ここでは呼び出されているか、typeが正しいかのみを確認する - expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1); - expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0] as SystemWebhookEventType).toEqual('inactiveModeratorsInvitationOnlyChanged'); - }); - }); -}); diff --git a/packages/backend/test/unit/server/api/drive/files/create.ts b/packages/backend/test/unit/server/api/drive/files/create.ts deleted file mode 100644 index e86b818ca5..0000000000 --- a/packages/backend/test/unit/server/api/drive/files/create.ts +++ /dev/null @@ -1,216 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Test, TestingModule } from '@nestjs/testing'; -import { FastifyInstance } from 'fastify'; -import request from 'supertest'; -import { randomString } from '../../../../../utils.js'; -import { CoreModule } from '@/core/CoreModule.js'; -import { RoleService } from '@/core/RoleService.js'; -import { DI } from '@/di-symbols.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { DriveFoldersRepository, MiDriveFolder, MiRole, UserProfilesRepository, UsersRepository } from '@/models/_.js'; -import { MiUser } from '@/models/User.js'; -import { ServerModule } from '@/server/ServerModule.js'; -import { ServerService } from '@/server/ServerService.js'; -import { IdService } from '@/core/IdService.js'; - -// TODO: uploadableFileTypes で許可されていないファイルが弾かれるかのテスト - -describe('/drive/files/create', () => { - let module: TestingModule; - let server: FastifyInstance; - let roleService: RoleService; - let idService: IdService; - - let root: MiUser; - let role_tinyAttachment: MiRole; - let role_imageOnly: MiRole; - let role_allowAllTypes: MiRole; - - let folder: MiDriveFolder; - - beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [GlobalModule, CoreModule, ServerModule], - }).compile(); - module.enableShutdownHooks(); - - const serverService = module.get(ServerService); - await serverService.launch(); - server = serverService.fastify; - - idService = module.get(IdService); - - const usersRepository = module.get(DI.usersRepository); - await usersRepository.createQueryBuilder().delete().execute(); - root = await usersRepository.insert({ - id: idService.gen(), - username: 'root', - usernameLower: 'root', - token: '1234567890123456', - }).then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - - const userProfilesRepository = module.get(DI.userProfilesRepository); - await userProfilesRepository.createQueryBuilder().delete().execute(); - await userProfilesRepository.insert({ - userId: root.id, - }); - - const driveFoldersRepository = module.get(DI.driveFoldersRepository); - folder = await driveFoldersRepository.insertOne({ - id: idService.gen(), - name: 'root-folder', - parentId: null, - userId: root.id, - }); - - roleService = module.get(RoleService); - role_imageOnly = await roleService.create({ - name: 'test-role001', - description: 'Test role001 description', - target: 'manual', - policies: { - uploadableFileTypes: { - useDefault: false, - priority: 1, - value: ['image/png'], - }, - }, - }); - role_allowAllTypes = await roleService.create({ - name: 'test-role002', - description: 'Test role002 description', - target: 'manual', - policies: { - uploadableFileTypes: { - useDefault: false, - priority: 1, - value: ['*/*'], - }, - }, - }); - role_tinyAttachment = await roleService.create({ - name: 'test-role003', - description: 'Test role003 description', - target: 'manual', - policies: { - maxFileSizeMb: { - useDefault: false, - priority: 1, - // 10byte - value: 10 / 1024 / 1024, - }, - }, - }); - }); - - beforeEach(async () => { - await roleService.unassign(root.id, role_tinyAttachment.id).catch(() => { - }); - await roleService.unassign(root.id, role_imageOnly.id).catch(() => { - }); - await roleService.unassign(root.id, role_allowAllTypes.id).catch(() => { - }); - }); - - afterAll(async () => { - await server.close(); - await module.close(); - }); - - async function postFile(props: { - name: string, - comment: string, - isSensitive: boolean, - force: boolean, - fileContent: Buffer | string, - }) { - const { name, comment, isSensitive, force, fileContent } = props; - - return await request(server.server) - .post('/api/drive/files/create') - .set('Content-Type', 'multipart/form-data') - .attach('file', fileContent) - .field('name', name) - .field('comment', comment) - .field('isSensitive', isSensitive) - .field('force', force) - .field('folderId', folder.id) - .field('i', root.token ?? ''); - } - - test('200 ok (all types allowed)', async () => { - await roleService.assign(root.id, role_allowAllTypes.id); - - const name = randomString(); - const comment = randomString(); - const result = await postFile({ - name: name, - comment: comment, - isSensitive: true, - force: true, - fileContent: Buffer.from('a'.repeat(1000 * 1000)), - }); - expect(result.statusCode).toBe(200); - expect(result.body.name).toBe(name + '.unknown'); - expect(result.body.comment).toBe(comment); - expect(result.body.isSensitive).toBe(true); - expect(result.body.folderId).toBe(folder.id); - }); - - test('400 when not allowed type', async () => { - await roleService.assign(root.id, role_imageOnly.id); - - const name = randomString(); - const comment = randomString(); - const result = await postFile({ - name: name, - comment: comment, - isSensitive: true, - force: true, - fileContent: Buffer.from('a'.repeat(10)), - }); - expect(result.statusCode).toBe(400); - expect(result.body.error.code).toBe('UNALLOWED_FILE_TYPE'); - }); - - test('200 ok (with size limited role)', async () => { - await roleService.assign(root.id, role_allowAllTypes.id); - await roleService.assign(root.id, role_tinyAttachment.id); - - const name = randomString(); - const comment = randomString(); - const result = await postFile({ - name: name, - comment: comment, - isSensitive: true, - force: true, - fileContent: Buffer.from('a'.repeat(10)), - }); - expect(result.statusCode).toBe(200); - expect(result.body.name).toBe(name + '.unknown'); - expect(result.body.comment).toBe(comment); - expect(result.body.isSensitive).toBe(true); - expect(result.body.folderId).toBe(folder.id); - }); - - test('413 too large', async () => { - await roleService.assign(root.id, role_allowAllTypes.id); - await roleService.assign(root.id, role_tinyAttachment.id); - - const name = randomString(); - const comment = randomString(); - const result = await postFile({ - name: name, - comment: comment, - isSensitive: true, - force: true, - fileContent: Buffer.from('a'.repeat(11)), - }); - expect(result.statusCode).toBe(413); - expect(result.body.error.code).toBe('MAX_FILE_SIZE_EXCEEDED'); - }); -}); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 7eecf8bb0d..37c1474be4 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -1,86 +1,66 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import * as assert from 'node:assert'; import { readFile } from 'node:fs/promises'; -import { basename, isAbsolute } from 'node:path'; -import { randomUUID } from 'node:crypto'; +import { isAbsolute, basename } from 'node:path'; import { inspect } from 'node:util'; import WebSocket, { ClientOptions } from 'ws'; -import fetch, { File, RequestInit, type Headers } from 'node-fetch'; +import fetch, { Blob, File, RequestInit } from 'node-fetch'; import { DataSource } from 'typeorm'; import { JSDOM } from 'jsdom'; -import { type Response } from 'node-fetch'; -import Fastify from 'fastify'; +import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { entities } from '../src/postgres.js'; import { loadConfig } from '../src/config.js'; import type * as misskey from 'misskey-js'; -import { DEFAULT_POLICIES } from '@/core/RoleService.js'; -import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; -import { ApiError } from '@/server/api/error.js'; -export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js'; +export { server as startServer } from '@/boot/common.js'; -export interface UserToken { +interface UserToken { token: string; bearer?: boolean; } -export type SystemWebhookPayload = { - server: string; - hookId: string; - eventId: string; - createdAt: string; - type: string; - body: any; -}; - const config = loadConfig(); export const port = config.port; -export const origin = config.url; -export const host = new URL(config.url).host; -export const WEBHOOK_HOST = 'http://localhost:15080'; -export const WEBHOOK_PORT = 15080; +export const cookie = (me: UserToken): string => { + return `token=${me.token};`; +}; -export type ApiRequest = { - endpoint: E, - parameters: P, +export const api = async (endpoint: string, params: any, me?: UserToken) => { + const normalized = endpoint.replace(/^\//, ''); + return await request(`api/${normalized}`, params, me); +}; + +export type ApiRequest = { + endpoint: string, + parameters: object, user: UserToken | undefined, }; -export const successfulApiCall = async (request: ApiRequest, assertion: { +export const successfulApiCall = async (request: ApiRequest, assertion: { status?: number, -} = {}): Promise> => { +} = {}): Promise => { const { endpoint, parameters, user } = request; const res = await api(endpoint, parameters, user); const status = assertion.status ?? (res.body == null ? 204 : 200); assert.strictEqual(res.status, status, inspect(res.body, { depth: 5, colors: true })); - - return res.body as misskey.api.SwitchCaseResponseType; + return res.body; }; -export const failedApiCall = async (request: ApiRequest, assertion: { +export const failedApiCall = async (request: ApiRequest, assertion: { status: number, code: string, id: string -}): Promise => { +}): Promise => { const { endpoint, parameters, user } = request; const { status, code, id } = assertion; const res = await api(endpoint, parameters, user); assert.strictEqual(res.status, status, inspect(res.body)); - assert.ok(res.body); - assert.strictEqual(castAsError(res.body as any).error.code, code, inspect(res.body)); - assert.strictEqual(castAsError(res.body as any).error.id, id, inspect(res.body)); + assert.strictEqual(res.body.error.code, code, inspect(res.body)); + assert.strictEqual(res.body.error.id, id, inspect(res.body)); + return res.body; }; -export const api = async (path: E, params: P, me?: UserToken): Promise<{ - status: number, - headers: Headers, - body: misskey.api.SwitchCaseResponseType -}> => { +const request = async (path: string, params: any, me?: UserToken): Promise<{ status: number, headers: Headers, body: any }> => { const bodyAuth: Record = {}; const headers: Record = { 'Content-Type': 'application/json', @@ -92,7 +72,7 @@ export const api = async + ? await res.json() : null; return { status: res.status, headers: res.headers, - // FIXME: removing this non-null assertion: requires better typing around empty response. - body: body!, + body, }; }; @@ -115,31 +94,9 @@ export const relativeFetch = async (path: string, init?: RequestInit | undefined return await fetch(new URL(path, `http://127.0.0.1:${port}/`).toString(), init); }; -export function randomString(chars = 'abcdefghijklmnopqrstuvwxyz0123456789', length = 16) { - let randomString = ''; - for (let i = 0; i < length; i++) { - randomString += chars[Math.floor(Math.random() * chars.length)]; - } - return randomString; -} - -/** - * @brief プロミスにタイムアウト追加 - * @param p 待ち対象プロミス - * @param timeout 待機ミリ秒 - */ -function timeoutPromise(p: Promise, timeout: number): Promise { - return Promise.race([ - p, - new Promise((reject) => { - setTimeout(() => { reject(new Error('timed out')); }, timeout); - }) as never, - ]); -} - export const signup = async (params?: Partial): Promise> => { const q = Object.assign({ - username: randomString(), + username: 'test', password: 'test', }, params); @@ -148,27 +105,17 @@ export const signup = async (params?: Partial => { +export const post = async (user: UserToken, params?: misskey.Endpoints['notes/create']['req']): Promise => { const q = params; const res = await api('notes/create', q, user); - // FIXME: the return type should reflect this fact. - return (res.body ? res.body.createdNote : null)!; -}; - -export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => { - const res = await api('miauth/gen-token', { - session: randomUUID(), - permission: permissions, - }, user); - - return (res.body as misskey.entities.MiauthGenTokenResponse).token; + return res.body ? res.body.createdNote : null; }; // 非公開ノートをAPI越しに見たときのノート NoteEntityService.ts -export const hiddenNote = (note: misskey.entities.Note): misskey.entities.Note => { - const temp: misskey.entities.Note = { +export const hiddenNote = (note: any): any => { + const temp = { ...note, fileIds: [], files: [], @@ -181,22 +128,21 @@ export const hiddenNote = (note: misskey.entities.Note): misskey.entities.Note = return temp; }; -export const react = async (user: UserToken, note: misskey.entities.Note, reaction: string): Promise => { +export const react = async (user: UserToken, note: any, reaction: string): Promise => { await api('notes/reactions/create', { noteId: note.id, reaction: reaction, }, user); }; -export const userList = async (user: UserToken, userList: Partial = {}): Promise => { +export const userList = async (user: UserToken, userList: any = {}): Promise => { const res = await api('users/lists/create', { name: 'test', - ...userList, }, user); return res.body; }; -export const page = async (user: UserToken, page: Partial = {}): Promise => { +export const page = async (user: UserToken, page: any = {}): Promise => { const res = await api('pages/create', { alignCenter: false, content: [ @@ -207,7 +153,7 @@ export const page = async (user: UserToken, page: Partial }, ], eyeCatchingImageId: null, - font: 'sans-serif' as any, + font: 'sans-serif', hideTitleWhenPinned: false, name: '1678594845072', script: '', @@ -219,7 +165,7 @@ export const page = async (user: UserToken, page: Partial return res.body; }; -export const play = async (user: UserToken, play: Partial = {}): Promise => { +export const play = async (user: UserToken, play: any = {}): Promise => { const res = await api('flash/create', { permissions: [], script: 'test', @@ -230,7 +176,7 @@ export const play = async (user: UserToken, play: Partial = {}): Promise => { +export const clip = async (user: UserToken, clip: any = {}): Promise => { const res = await api('clips/create', { description: null, isPublic: true, @@ -240,18 +186,18 @@ export const clip = async (user: UserToken, clip: Partial return res.body; }; -export const galleryPost = async (user: UserToken, galleryPost: Partial = {}): Promise => { +export const galleryPost = async (user: UserToken, channel: any = {}): Promise => { const res = await api('gallery/posts/create', { description: null, fileIds: [], isSensitive: false, title: 'test', - ...galleryPost, + ...channel, }, user); return res.body; }; -export const channel = async (user: UserToken, channel: Partial = {}): Promise => { +export const channel = async (user: UserToken, channel: any = {}): Promise => { const res = await api('channels/create', { bannerId: null, description: null, @@ -261,7 +207,7 @@ export const channel = async (user: UserToken, channel: Partial = {}, policies: any = {}): Promise => { +export const role = async (user: UserToken, role: any = {}, policies: any = {}): Promise => { const res = await api('admin/roles/create', { asBadge: false, canEditMembersByModerator: false, @@ -269,7 +215,7 @@ export const role = async (user: UserToken, role: Partial condFormula: { id: 'ebef1684-672d-49b6-ad82-1b3ec3784f85', type: 'isRemote', - } as any, + }, description: '', displayOrder: 0, iconUrl: null, @@ -304,13 +250,9 @@ interface UploadOptions { * Upload file * @param user User */ -export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ - status: number, - headers: Headers, - body: misskey.entities.DriveFile | null -}> => { +export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ status: number, headers: Headers, body: misskey.Endpoints['drive/files/create']['res'] | null }> => { const absPath = path == null - ? new URL('resources/192.jpg', import.meta.url) + ? new URL('resources/Lenna.jpg', import.meta.url) : isAbsolute(path.toString()) ? new URL(path) : new URL(path, new URL('resources/', import.meta.url)); @@ -337,6 +279,7 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO }); const body = res.status !== 204 ? await res.json() as misskey.Endpoints['drive/files/create']['res'] : null; + return { status: res.status, headers: res.headers, @@ -344,16 +287,15 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO }; }; -export const uploadUrl = async (user: UserToken, url: string): Promise => { +export const uploadUrl = async (user: UserToken, url: string) => { + let file: any; const marker = Math.random().toString(); - const catcher = makeStreamCatcher( - user, - 'main', - (msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker, - (msg) => msg.body.file, - 60 * 1000, - ); + const ws = await connectStream(user, 'main', (msg) => { + if (msg.type === 'urlUploadFinished' && msg.body.marker === marker) { + file = msg.body.file; + } + }); await api('drive/files/upload-from-url', { url, @@ -361,10 +303,13 @@ export const uploadUrl = async (user: UserToken, url: string): Promise(user: UserToken, channel: C, listener: (message: Record) => any, params?: misskey.Channels[C]['params']): Promise { +export function connectStream(user: UserToken, channel: string, listener: (message: Record) => any, params?: any): Promise { return new Promise((res, rej) => { const url = new URL(`ws://127.0.0.1:${port}/streaming`); const options: ClientOptions = {}; @@ -399,7 +344,7 @@ export function connectStream(user: UserToken, }); } -export const waitFire = async (user: UserToken, channel: C, trgr: () => any, cond: (msg: Record) => boolean, params?: misskey.Channels[C]['params']) => { +export const waitFire = async (user: UserToken, channel: string, trgr: () => any, cond: (msg: Record) => boolean, params?: any) => { return new Promise(async (res, rej) => { let timer: NodeJS.Timeout | null = null; @@ -433,42 +378,13 @@ export const waitFire = async (user: UserToken }); }; -/** - * @brief WebSocketストリームから特定条件の通知を拾うプロミスを生成 - * @param user ユーザー認証情報 - * @param channel チャンネル - * @param cond 条件 - * @param extractor 取り出し処理 - * @param timeout ミリ秒タイムアウト - * @returns 時間内に正常に処理できた場合に通知からextractorを通した値を得る - */ -export function makeStreamCatcher( - user: UserToken, - channel: keyof misskey.Channels, - cond: (message: Record) => boolean, - extractor: (message: Record) => T, - timeout = 60 * 1000): Promise { - let ws: WebSocket; - const p = new Promise(async (resolve) => { - ws = await connectStream(user, channel, (msg) => { - if (cond(msg)) { - resolve(extractor(msg)); - } - }); - }).finally(() => { - ws.close(); - }); - - return timeoutPromise(p, timeout); -} - export type SimpleGetResponse = { status: number, body: any | JSDOM | null, type: string | null, location: string | null }; -export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined, bodyExtractor: (res: Response) => Promise = _ => Promise.resolve(null)): Promise => { +export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined): Promise => { const res = await relativeFetch(path, { headers: { Accept: accept, @@ -485,18 +401,10 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde 'text/html; charset=utf-8', ]; - if (res.ok && ( - accept.startsWith('application/activity+json') || - (accept.startsWith('application/ld+json') && accept.includes('https://www.w3.org/ns/activitystreams')) - )) { - // validateContentTypeSetAsActivityPubのテストを兼ねる - validateContentTypeSetAsActivityPub(res); - } - const body = - jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : - htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : - await bodyExtractor(res); + jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : + htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : + null; return { status: res.status, @@ -537,15 +445,14 @@ export async function testPaginationConsistency id + ':' + createdAt), expected.map(({ id, createdAt }) => id + ':' + createdAt)); } - */ // 3. untilId指定+limitで取得してつなぎ合わせた結果が期待通りになっていること if (ordering === 'desc') { @@ -574,7 +480,7 @@ export async function testPaginationConsistency id + ':' + createdAt), @@ -618,73 +524,10 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) { return db; } -export async function sendEnvUpdateRequest(params: { key: string, value?: string }) { - const res = await fetch( - `http://localhost:${port + 1000}/env`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(params), - }, - ); - - if (res.status !== 200) { - throw new Error('server env update failed.'); - } -} - -export async function sendEnvResetRequest() { - const res = await fetch( - `http://localhost:${port + 1000}/env-reset`, - { - method: 'POST', - body: JSON.stringify({}), - }, - ); - - if (res.status !== 200) { - throw new Error('server env update failed.'); - } -} - -// 与えられた値を強制的にエラーとみなす。この関数は型安全性を破壊するため、異常系のアサーション以外で用いられるべきではない。 -// FIXME(misskey-js): misskey-jsがエラー情報を公開するようになったらこの関数を廃止する -export function castAsError(obj: Record): { error: ApiError } { - return obj as { error: ApiError }; -} - -export async function captureWebhook(postAction: () => Promise, port = WEBHOOK_PORT): Promise { - const fastify = Fastify(); - - let timeoutHandle: NodeJS.Timeout | null = null; - const result = await new Promise(async (resolve, reject) => { - fastify.all('/', async (req, res) => { - timeoutHandle && clearTimeout(timeoutHandle); - - const body = JSON.stringify(req.body); - res.status(200).send('ok'); - await fastify.close(); - resolve(body); - }); - - await fastify.listen({ port }); - - timeoutHandle = setTimeout(async () => { - await fastify.close(); - reject(new Error('timeout')); - }, 3000); - - try { - await postAction(); - } catch (e) { - await fastify.close(); - reject(e); - } +export function sleep(msec: number) { + return new Promise(res => { + setTimeout(() => { + res(); + }, msec); }); - - await fastify.close(); - - return JSON.parse(result) as T; } diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index 2b15a5cc7a..93944a68d5 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -10,8 +10,8 @@ "declaration": false, "sourceMap": false, "target": "ES2022", - "module": "nodenext", - "moduleResolution": "nodenext", + "module": "ESNext", + "moduleResolution": "node16", "allowSyntheticDefaultImports": true, "removeComments": false, "noLib": false, @@ -33,9 +33,8 @@ "node" ], "typeRoots": [ - "./src/@types", "./node_modules/@types", - "./node_modules" + "./src/@types" ], "lib": [ "esnext" diff --git a/packages/backend/scripts/watch.mjs b/packages/backend/watch.mjs similarity index 82% rename from packages/backend/scripts/watch.mjs rename to packages/backend/watch.mjs index a0ccea3b16..9c9d2dbd86 100644 --- a/packages/backend/scripts/watch.mjs +++ b/packages/backend/watch.mjs @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { execa } from 'execa'; (async () => { diff --git a/packages/frontend-embed/.gitignore b/packages/frontend-embed/.gitignore deleted file mode 100644 index 1aa0ac14e8..0000000000 --- a/packages/frontend-embed/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/storybook-static diff --git a/packages/frontend-embed/@types/global.d.ts b/packages/frontend-embed/@types/global.d.ts deleted file mode 100644 index 8a067a78ec..0000000000 --- a/packages/frontend-embed/@types/global.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -type FIXME = any; - -declare const _LANGS_: string[][]; -declare const _VERSION_: string; -declare const _ENV_: string; -declare const _DEV_: boolean; -declare const _PERF_PREFIX_: string; - -// for dev-mode -declare const _LANGS_FULL_: string[][]; - -// TagCanvas -interface Window { - TagCanvas: any; -} diff --git a/packages/frontend-embed/@types/theme.d.ts b/packages/frontend-embed/@types/theme.d.ts deleted file mode 100644 index 6ac1037493..0000000000 --- a/packages/frontend-embed/@types/theme.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -declare module '@@/themes/*.json5' { - import { Theme } from '@/theme.js'; - - const theme: Theme; - - export default theme; -} diff --git a/packages/frontend-embed/assets/dummy.png b/packages/frontend-embed/assets/dummy.png deleted file mode 100644 index 39332b0c1b..0000000000 Binary files a/packages/frontend-embed/assets/dummy.png and /dev/null differ diff --git a/packages/frontend-embed/eslint.config.js b/packages/frontend-embed/eslint.config.js deleted file mode 100644 index 2aef311e2e..0000000000 --- a/packages/frontend-embed/eslint.config.js +++ /dev/null @@ -1,93 +0,0 @@ -import globals from 'globals'; -import tsParser from '@typescript-eslint/parser'; -import parser from 'vue-eslint-parser'; -import pluginVue from 'eslint-plugin-vue'; -import pluginMisskey from '@misskey-dev/eslint-plugin'; -import sharedConfig from '../shared/eslint.config.js'; - -export default [ - ...sharedConfig, - { - files: ['src/**/*.vue'], - ...pluginMisskey.configs.typescript, - }, - ...pluginVue.configs['flat/recommended'], - { - files: ['src/**/*.{ts,vue}'], - languageOptions: { - globals: { - ...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])), - ...globals.browser, - - // Node.js - module: false, - require: false, - __dirname: false, - - // Misskey - _DEV_: false, - _LANGS_: false, - _VERSION_: false, - _ENV_: false, - _PERF_PREFIX_: false, - }, - parser, - parserOptions: { - extraFileExtensions: ['.vue'], - parser: tsParser, - project: ['./tsconfig.json'], - sourceType: 'module', - tsconfigRootDir: import.meta.dirname, - }, - }, - rules: { - '@typescript-eslint/no-empty-interface': ['error', { - allowSingleExtends: true, - }], - 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], - // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため - // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため - 'id-denylist': ['error', 'window', 'e'], - 'no-shadow': ['warn'], - 'vue/attributes-order': ['error', { - alphabetical: false, - }], - 'vue/no-use-v-if-with-v-for': ['error', { - allowUsingIterationVar: false, - }], - 'vue/no-ref-as-operand': 'error', - 'vue/no-multi-spaces': ['error', { - ignoreProperties: false, - }], - 'vue/no-v-html': 'warn', - 'vue/order-in-components': 'error', - 'vue/html-indent': ['warn', 'tab', { - attribute: 1, - baseIndent: 0, - closeBracket: 0, - alignAttributesVertically: true, - ignores: [], - }], - 'vue/html-closing-bracket-spacing': ['warn', { - startTag: 'never', - endTag: 'never', - selfClosingTag: 'never', - }], - 'vue/multi-word-component-names': 'warn', - 'vue/require-v-for-key': 'warn', - 'vue/no-unused-components': 'warn', - 'vue/no-unused-vars': 'warn', - 'vue/no-dupe-keys': 'warn', - 'vue/valid-v-for': 'warn', - 'vue/return-in-computed-property': 'warn', - 'vue/no-setup-props-reactivity-loss': 'warn', - 'vue/max-attributes-per-line': 'off', - 'vue/html-self-closing': 'off', - 'vue/singleline-html-element-content-newline': 'off', - 'vue/v-on-event-hyphenation': ['error', 'never', { - autofix: true, - }], - 'vue/attribute-hyphenation': ['error', 'never'], - }, - }, -]; diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json deleted file mode 100644 index f2489f1b80..0000000000 --- a/packages/frontend-embed/package.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "name": "frontend-embed", - "private": true, - "type": "module", - "scripts": { - "watch": "vite", - "build": "vite build", - "typecheck": "vue-tsc --noEmit", - "eslint": "eslint --quiet \"src/**/*.{ts,vue}\"", - "lint": "pnpm typecheck && pnpm eslint" - }, - "dependencies": { - "@discordapp/twemoji": "15.1.0", - "@rollup/plugin-json": "6.1.0", - "@rollup/plugin-replace": "6.0.2", - "@rollup/pluginutils": "5.1.4", - "@twemoji/parser": "15.1.1", - "@vitejs/plugin-vue": "5.2.4", - "@vue/compiler-sfc": "3.5.16", - "astring": "1.9.0", - "buraha": "0.0.1", - "estree-walker": "3.0.3", - "icons-subsetter": "workspace:*", - "frontend-shared": "workspace:*", - "json5": "2.2.3", - "mfm-js": "0.24.0", - "misskey-js": "workspace:*", - "punycode.js": "2.3.1", - "rollup": "4.41.1", - "sass": "1.89.0", - "shiki": "3.4.2", - "tinycolor2": "1.6.0", - "tsc-alias": "1.8.16", - "tsconfig-paths": "4.2.0", - "typescript": "5.8.3", - "uuid": "11.1.0", - "vite": "6.3.5", - "vue": "3.5.16" - }, - "devDependencies": { - "@misskey-dev/summaly": "5.2.1", - "@tabler/icons-webfont": "3.33.0", - "@testing-library/vue": "8.1.0", - "@types/estree": "1.0.7", - "@types/micromatch": "4.0.9", - "@types/node": "22.15.28", - "@types/punycode.js": "npm:@types/punycode@2.1.4", - "@types/tinycolor2": "1.4.6", - "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.33.0", - "@typescript-eslint/parser": "8.33.0", - "@vitest/coverage-v8": "3.1.4", - "@vue/runtime-core": "3.5.16", - "acorn": "8.14.1", - "cross-env": "7.0.3", - "eslint-plugin-import": "2.31.0", - "eslint-plugin-vue": "10.1.0", - "fast-glob": "3.3.3", - "happy-dom": "17.5.6", - "intersection-observer": "0.12.2", - "micromatch": "4.0.8", - "msw": "2.8.6", - "nodemon": "3.1.10", - "prettier": "3.5.3", - "start-server-and-test": "2.0.12", - "vite-plugin-turbosnap": "1.0.3", - "vue-component-type-helpers": "2.2.10", - "vue-eslint-parser": "10.1.3", - "vue-tsc": "2.2.10" - } -} diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts deleted file mode 100644 index 459b283e23..0000000000 --- a/packages/frontend-embed/src/boot.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// https://vitejs.dev/config/build-options.html#build-modulepreload -import 'vite/modulepreload-polyfill'; - -if (import.meta.env.DEV) { - await import('@tabler/icons-webfont/dist/tabler-icons.scss'); -} else { - await import('icons-subsetter/built/tabler-icons-frontendEmbed.css'); -} - -import '@/style.scss'; -import { createApp, defineAsyncComponent } from 'vue'; -import defaultLightTheme from '@@/themes/l-light.json5'; -import defaultDarkTheme from '@@/themes/d-dark.json5'; -import { MediaProxy } from '@@/js/media-proxy.js'; -import { applyTheme, assertIsTheme } from '@/theme.js'; -import { fetchCustomEmojis } from '@/custom-emojis.js'; -import { DI } from '@/di.js'; -import { serverMetadata } from '@/server-metadata.js'; -import { url, version, locale, lang, updateLocale } from '@@/js/config.js'; -import { parseEmbedParams } from '@@/js/embed-page.js'; -import { postMessageToParentWindow, setIframeId } from '@/post-message.js'; -import { serverContext } from '@/server-context.js'; -import { i18n, updateI18n } from '@/i18n.js'; - -import type { Theme } from '@/theme.js'; - -console.log('Misskey Embed'); - -//#region Embedパラメータの取得・パース -const params = new URLSearchParams(location.search); -const embedParams = parseEmbedParams(params); -if (_DEV_) console.log(embedParams); -//#endregion - -//#region テーマ -function parseThemeOrNull(theme: string | null): Theme | null { - if (theme == null) return null; - try { - const parsed = JSON.parse(theme); - if (assertIsTheme(parsed)) { - return parsed; - } else { - return null; - } - } catch (err) { - return null; - } -} - -const lightTheme = parseThemeOrNull(serverMetadata.defaultLightTheme) ?? defaultLightTheme; -const darkTheme = parseThemeOrNull(serverMetadata.defaultDarkTheme) ?? defaultDarkTheme; - -if (embedParams.colorMode === 'dark') { - applyTheme(darkTheme); -} else if (embedParams.colorMode === 'light') { - applyTheme(lightTheme); -} else { - if (window.matchMedia('(prefers-color-scheme: dark)').matches) { - applyTheme(darkTheme); - } else { - applyTheme(lightTheme); - } - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => { - if (mql.matches) { - applyTheme(darkTheme); - } else { - applyTheme(lightTheme); - } - }); -} -//#endregion - -//#region Detect language & fetch translations -const localeVersion = localStorage.getItem('localeVersion'); -const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null); -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); - localStorage.setItem('locale', newLocale); - localStorage.setItem('localeVersion', version); - updateLocale(parsedNewLocale); - updateI18n(parsedNewLocale); - } -} -//#endregion - -// サイズの制限 -document.documentElement.style.maxWidth = '500px'; - -// iframeIdの設定 -function setIframeIdHandler(event: MessageEvent) { - if (event.data?.type === 'misskey:embedParent:registerIframeId' && event.data.payload?.iframeId != null) { - setIframeId(event.data.payload.iframeId); - window.removeEventListener('message', setIframeIdHandler); - } -} - -window.addEventListener('message', setIframeIdHandler); - -try { - await fetchCustomEmojis(); -} catch (err) { /* empty */ } - -const app = createApp( - defineAsyncComponent(() => import('@/ui.vue')), -); - -app.provide(DI.mediaProxy, new MediaProxy(serverMetadata, url)); - -app.provide(DI.serverMetadata, serverMetadata); - -app.provide(DI.serverContext, serverContext); - -app.provide(DI.embedParams, embedParams); - -// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 -// なぜか2回実行されることがあるため、mountするdivを1つに制限する -const rootEl = ((): HTMLElement => { - const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; - - const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID); - - if (currentRoot) { - console.warn('multiple import detected'); - return currentRoot; - } - - const root = document.createElement('div'); - root.id = MISSKEY_MOUNT_DIV_ID; - document.body.appendChild(root); - return root; -})(); - -postMessageToParentWindow('misskey:embed:ready'); - -app.mount(rootEl); - -// boot.jsのやつを解除 -window.onerror = null; -window.onunhandledrejection = null; - -removeSplash(); - -//#region Self-XSS 対策メッセージ -console.log( - `%c${i18n.ts._selfXssPrevention.warning}`, - 'color: #f00; background-color: #ff0; font-size: 36px; padding: 4px;', -); -console.log( - `%c${i18n.ts._selfXssPrevention.title}`, - 'color: #f00; font-weight: 900; font-family: "Hiragino Sans W9", "Hiragino Kaku Gothic ProN", sans-serif; font-size: 24px;', -); -console.log( - `%c${i18n.ts._selfXssPrevention.description1}`, - 'font-size: 16px; font-weight: 700;', -); -console.log( - `%c${i18n.ts._selfXssPrevention.description2}`, - 'font-size: 16px;', - 'font-size: 20px; font-weight: 700; color: #f00;', -); -console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hub.net/docs/for-users/resources/self-xss/' })); -//#endregion - -function removeSplash() { - const splash = document.getElementById('splash'); - if (splash) { - splash.style.opacity = '0'; - splash.style.pointerEvents = 'none'; - - // transitionendイベントが発火しない場合があるため - window.setTimeout(() => { - splash.remove(); - }, 1000); - } -} diff --git a/packages/frontend-embed/src/components/EmA.vue b/packages/frontend-embed/src/components/EmA.vue deleted file mode 100644 index 1c236b9a35..0000000000 --- a/packages/frontend-embed/src/components/EmA.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/packages/frontend-embed/src/components/EmAcct.vue b/packages/frontend-embed/src/components/EmAcct.vue deleted file mode 100644 index ff794d9b6e..0000000000 --- a/packages/frontend-embed/src/components/EmAcct.vue +++ /dev/null @@ -1,24 +0,0 @@ - - - - - diff --git a/packages/frontend-embed/src/components/EmAvatar.vue b/packages/frontend-embed/src/components/EmAvatar.vue deleted file mode 100644 index 58c35c8ef0..0000000000 --- a/packages/frontend-embed/src/components/EmAvatar.vue +++ /dev/null @@ -1,250 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmCustomEmoji.vue b/packages/frontend-embed/src/components/EmCustomEmoji.vue deleted file mode 100644 index 59b670cdc6..0000000000 --- a/packages/frontend-embed/src/components/EmCustomEmoji.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmEmoji.vue b/packages/frontend-embed/src/components/EmEmoji.vue deleted file mode 100644 index 224979707b..0000000000 --- a/packages/frontend-embed/src/components/EmEmoji.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmError.vue b/packages/frontend-embed/src/components/EmError.vue deleted file mode 100644 index d376b29a7f..0000000000 --- a/packages/frontend-embed/src/components/EmError.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue deleted file mode 100644 index 0bff048ce4..0000000000 --- a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue +++ /dev/null @@ -1,238 +0,0 @@ - - - - - - - - - diff --git a/packages/frontend-embed/src/components/EmInstanceTicker.vue b/packages/frontend-embed/src/components/EmInstanceTicker.vue deleted file mode 100644 index 4a116e317a..0000000000 --- a/packages/frontend-embed/src/components/EmInstanceTicker.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmLink.vue b/packages/frontend-embed/src/components/EmLink.vue deleted file mode 100644 index aec9b33072..0000000000 --- a/packages/frontend-embed/src/components/EmLink.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmLoading.vue b/packages/frontend-embed/src/components/EmLoading.vue deleted file mode 100644 index 47d797606b..0000000000 --- a/packages/frontend-embed/src/components/EmLoading.vue +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmMediaBanner.vue b/packages/frontend-embed/src/components/EmMediaBanner.vue deleted file mode 100644 index cf4a4c53b5..0000000000 --- a/packages/frontend-embed/src/components/EmMediaBanner.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmMediaImage.vue b/packages/frontend-embed/src/components/EmMediaImage.vue deleted file mode 100644 index 94f0268da4..0000000000 --- a/packages/frontend-embed/src/components/EmMediaImage.vue +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmMediaList.vue b/packages/frontend-embed/src/components/EmMediaList.vue deleted file mode 100644 index 0b2d835abe..0000000000 --- a/packages/frontend-embed/src/components/EmMediaList.vue +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmMediaVideo.vue b/packages/frontend-embed/src/components/EmMediaVideo.vue deleted file mode 100644 index e2779bdee4..0000000000 --- a/packages/frontend-embed/src/components/EmMediaVideo.vue +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmMention.vue b/packages/frontend-embed/src/components/EmMention.vue deleted file mode 100644 index b5aaa95894..0000000000 --- a/packages/frontend-embed/src/components/EmMention.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmMfm.ts b/packages/frontend-embed/src/components/EmMfm.ts deleted file mode 100644 index 1f9ce9d4f4..0000000000 --- a/packages/frontend-embed/src/components/EmMfm.ts +++ /dev/null @@ -1,453 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { h, provide } from 'vue'; -import type { VNode, SetupContext } from 'vue'; -import * as mfm from 'mfm-js'; -import * as Misskey from 'misskey-js'; -import { host } from '@@/js/config.js'; -import EmUrl from '@/components/EmUrl.vue'; -import EmTime from '@/components/EmTime.vue'; -import EmLink from '@/components/EmLink.vue'; -import EmMention from '@/components/EmMention.vue'; -import EmEmoji from '@/components/EmEmoji.vue'; -import EmCustomEmoji from '@/components/EmCustomEmoji.vue'; -import EmA from '@/components/EmA.vue'; - -function safeParseFloat(str: unknown): number | null { - if (typeof str !== 'string' || str === '') return null; - const num = parseFloat(str); - if (isNaN(num)) return null; - return num; -} - -const QUOTE_STYLE = ` -display: block; -margin: 8px; -padding: 6px 0 6px 12px; -color: var(--MI_THEME-fg); -border-left: solid 3px var(--MI_THEME-fg); -opacity: 0.7; -`.split('\n').join(' '); - -type MfmProps = { - text: string; - plain?: boolean; - nowrap?: boolean; - author?: Misskey.entities.UserLite; - isNote?: boolean; - emojiUrls?: Record; - rootScale?: number; - nyaize?: boolean | 'respect'; - parsedNodes?: mfm.MfmNode[] | null; -}; - -type MfmEvents = { - clickEv(id: string): void; -}; - -// eslint-disable-next-line import/no-default-export -export default function (props: MfmProps, { emit }: { emit: SetupContext['emit'] }) { - const isNote = props.isNote ?? true; - const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false; - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (props.text == null || props.text === '') return; - - const rootAst = props.parsedNodes ?? (props.plain ? mfm.parseSimple : mfm.parse)(props.text); - - const validTime = (t: string | boolean | null | undefined) => { - if (t == null) return null; - if (typeof t === 'boolean') return null; - return t.match(/^\-?[0-9.]+s$/) ? t : null; - }; - - const validColor = (c: unknown): string | null => { - if (typeof c !== 'string') return null; - return c.match(/^[0-9a-f]{3,6}$/i) ? c : null; - }; - - const useAnim = true; - - /** - * Gen Vue Elements from MFM AST - * @param ast MFM AST - * @param scale How times large the text is - * @param disableNyaize Whether nyaize is disabled or not - */ - const genEl = (ast: mfm.MfmNode[], scale: number, disableNyaize = false) => ast.map((token): VNode | string | (VNode | string)[] => { - switch (token.type) { - case 'text': { - let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); - if (!disableNyaize && shouldNyaize) { - text = Misskey.nyaize(text); - } - - if (!props.plain) { - const res: (VNode | string)[] = []; - for (const t of text.split('\n')) { - res.push(h('br')); - res.push(t); - } - res.shift(); - return res; - } else { - return [text.replace(/\n/g, ' ')]; - } - } - - case 'bold': { - return [h('b', genEl(token.children, scale))]; - } - - case 'strike': { - return [h('del', genEl(token.children, scale))]; - } - - case 'italic': { - return h('i', { - style: 'font-style: oblique;', - }, genEl(token.children, scale)); - } - - case 'fn': { - // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる - let style: string | undefined; - switch (token.props.name) { - case 'tada': { - const speed = validTime(token.props.args.speed) ?? '1s'; - const delay = validTime(token.props.args.delay) ?? '0s'; - style = 'font-size: 150%;' + (useAnim ? `animation: global-tada ${speed} linear infinite both; animation-delay: ${delay};` : ''); - break; - } - case 'jelly': { - const speed = validTime(token.props.args.speed) ?? '1s'; - const delay = validTime(token.props.args.delay) ?? '0s'; - style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both; animation-delay: ${delay};` : ''); - break; - } - case 'twitch': { - const speed = validTime(token.props.args.speed) ?? '0.5s'; - const delay = validTime(token.props.args.delay) ?? '0s'; - style = useAnim ? `animation: mfm-twitch ${speed} ease infinite; animation-delay: ${delay};` : ''; - break; - } - case 'shake': { - const speed = validTime(token.props.args.speed) ?? '0.5s'; - const delay = validTime(token.props.args.delay) ?? '0s'; - style = useAnim ? `animation: mfm-shake ${speed} ease infinite; animation-delay: ${delay};` : ''; - break; - } - case 'spin': { - const direction = - token.props.args.left ? 'reverse' : - token.props.args.alternate ? 'alternate' : - 'normal'; - const anime = - token.props.args.x ? 'mfm-spinX' : - token.props.args.y ? 'mfm-spinY' : - 'mfm-spin'; - const speed = validTime(token.props.args.speed) ?? '1.5s'; - const delay = validTime(token.props.args.delay) ?? '0s'; - style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction}; animation-delay: ${delay};` : ''; - break; - } - case 'jump': { - const speed = validTime(token.props.args.speed) ?? '0.75s'; - const delay = validTime(token.props.args.delay) ?? '0s'; - style = useAnim ? `animation: mfm-jump ${speed} linear infinite; animation-delay: ${delay};` : ''; - break; - } - case 'bounce': { - const speed = validTime(token.props.args.speed) ?? '0.75s'; - const delay = validTime(token.props.args.delay) ?? '0s'; - style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom; animation-delay: ${delay};` : ''; - break; - } - case 'flip': { - const transform = - (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' : - token.props.args.v ? 'scaleY(-1)' : - 'scaleX(-1)'; - style = `transform: ${transform};`; - break; - } - case 'x2': { - return h('span', { - class: 'mfm-x2', - }, genEl(token.children, scale * 2)); - } - case 'x3': { - return h('span', { - class: 'mfm-x3', - }, genEl(token.children, scale * 3)); - } - case 'x4': { - return h('span', { - class: 'mfm-x4', - }, genEl(token.children, scale * 4)); - } - case 'font': { - const family = - token.props.args.serif ? 'serif' : - token.props.args.monospace ? 'monospace' : - token.props.args.cursive ? 'cursive' : - token.props.args.fantasy ? 'fantasy' : - token.props.args.emoji ? 'emoji' : - token.props.args.math ? 'math' : - null; - if (family) style = `font-family: ${family};`; - break; - } - case 'blur': { - return h('span', { - class: '_mfm_blur_', - }, genEl(token.children, scale)); - } - case 'rainbow': { - if (!useAnim) { - return h('span', { - class: '_mfm_rainbow_fallback_', - }, genEl(token.children, scale)); - } - const speed = validTime(token.props.args.speed) ?? '1s'; - const delay = validTime(token.props.args.delay) ?? '0s'; - style = `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};`; - break; - } - case 'sparkle': { - return genEl(token.children, scale); - } - case 'rotate': { - const degrees = safeParseFloat(token.props.args.deg) ?? 90; - style = `transform: rotate(${degrees}deg); transform-origin: center center;`; - break; - } - case 'position': { - const x = safeParseFloat(token.props.args.x) ?? 0; - const y = safeParseFloat(token.props.args.y) ?? 0; - style = `transform: translateX(${x}em) translateY(${y}em);`; - break; - } - case 'scale': { - const x = Math.min(safeParseFloat(token.props.args.x) ?? 1, 5); - const y = Math.min(safeParseFloat(token.props.args.y) ?? 1, 5); - style = `transform: scale(${x}, ${y});`; - scale = scale * Math.max(x, y); - break; - } - case 'fg': { - let color = validColor(token.props.args.color); - color = color ?? 'f00'; - style = `color: #${color}; overflow-wrap: anywhere;`; - break; - } - case 'bg': { - let color = validColor(token.props.args.color); - color = color ?? 'f00'; - style = `background-color: #${color}; overflow-wrap: anywhere;`; - break; - } - case 'border': { - let color = validColor(token.props.args.color); - color = color ? `#${color}` : 'var(--MI_THEME-accent)'; - let b_style = token.props.args.style; - if ( - typeof b_style !== 'string' || - !['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'] - .includes(b_style) - ) b_style = 'solid'; - const width = safeParseFloat(token.props.args.width) ?? 1; - const radius = safeParseFloat(token.props.args.radius) ?? 0; - style = `border: ${width}px ${b_style} ${color}; border-radius: ${radius}px;${token.props.args.noclip ? '' : ' overflow: clip;'}`; - break; - } - case 'ruby': { - if (token.children.length === 1) { - const child = token.children[0]; - let text = child.type === 'text' ? child.props.text : ''; - if (!disableNyaize && shouldNyaize) { - text = Misskey.nyaize(text); - } - return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]); - } else { - const rt = token.children.at(-1)!; - let text = rt.type === 'text' ? rt.props.text : ''; - if (!disableNyaize && shouldNyaize) { - text = Misskey.nyaize(text); - } - return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]); - } - } - case 'unixtime': { - const child = token.children[0]; - const unixtime = parseInt(child.type === 'text' ? child.props.text : ''); - return h('span', { - style: 'display: inline-block; font-size: 90%; border: solid 1px var(--MI_THEME-divider); border-radius: 999px; padding: 4px 10px 4px 6px;', - }, [ - h('i', { - class: 'ti ti-clock', - style: 'margin-right: 0.25em;', - }), - h(EmTime, { - key: Math.random(), - time: unixtime * 1000, - mode: 'detail', - }), - ]); - } - case 'clickable': { - return h('span', { onClick(ev: MouseEvent): void { - ev.stopPropagation(); - ev.preventDefault(); - const clickEv = typeof token.props.args.ev === 'string' ? token.props.args.ev : ''; - emit('clickEv', clickEv); - } }, genEl(token.children, scale)); - } - } - if (style === undefined) { - return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']); - } else { - return h('span', { - style: 'display: inline-block; ' + style, - }, genEl(token.children, scale)); - } - } - - case 'small': { - return [h('small', { - style: 'opacity: 0.7;', - }, genEl(token.children, scale))]; - } - - case 'center': { - return [h('div', { - style: 'text-align:center;', - }, genEl(token.children, scale))]; - } - - case 'url': { - return [h(EmUrl, { - key: Math.random(), - url: token.props.url, - rel: 'nofollow noopener', - })]; - } - - case 'link': { - return [h(EmLink, { - key: Math.random(), - url: token.props.url, - rel: 'nofollow noopener', - }, genEl(token.children, scale, true))]; - } - - case 'mention': { - return [h(EmMention, { - key: Math.random(), - host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host, - username: token.props.username, - })]; - } - - case 'hashtag': { - return [h(EmA, { - key: Math.random(), - to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`, - style: 'color:var(--MI_THEME-hashtag);', - }, `#${token.props.hashtag}`)]; - } - - case 'blockCode': { - return [h('code', { - key: Math.random(), - lang: token.props.lang ?? undefined, - }, token.props.code)]; - } - - case 'inlineCode': { - return [h('code', { - key: Math.random(), - }, token.props.code)]; - } - - case 'quote': { - if (!props.nowrap) { - return [h('div', { - style: QUOTE_STYLE, - }, genEl(token.children, scale, true))]; - } else { - return [h('span', { - style: QUOTE_STYLE, - }, genEl(token.children, scale, true))]; - } - } - - case 'emojiCode': { - if (props.author?.host == null) { - return [h(EmCustomEmoji, { - key: Math.random(), - name: token.props.name, - normal: props.plain, - host: null, - useOriginalSize: scale >= 2.5, - fallbackToImage: false, - })]; - } else { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) { - return [h('span', `:${token.props.name}:`)]; - } else { - return [h(EmCustomEmoji, { - key: Math.random(), - name: token.props.name, - url: props.emojiUrls && props.emojiUrls[token.props.name], - normal: props.plain, - host: props.author.host, - useOriginalSize: scale >= 2.5, - })]; - } - } - } - - case 'unicodeEmoji': { - return [h(EmEmoji, { - key: Math.random(), - emoji: token.props.emoji, - })]; - } - - case 'mathInline': { - return [h('code', token.props.formula)]; - } - - case 'mathBlock': { - return [h('code', token.props.formula)]; - } - - case 'search': { - return [h('div', { - key: Math.random(), - }, token.props.query)]; - } - - case 'plain': { - return [h('span', genEl(token.children, scale, true))]; - } - - default: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - console.error('unrecognized ast type:', (token as any).type); - - return []; - } - } - }).flat(Infinity) as (VNode | string)[]; - - return h('span', { - // https://codeday.me/jp/qa/20190424/690106.html - style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;', - }, genEl(rootAst, props.rootScale ?? 1)); -} diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue deleted file mode 100644 index f5b064c293..0000000000 --- a/packages/frontend-embed/src/components/EmNote.vue +++ /dev/null @@ -1,605 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue deleted file mode 100644 index b39b47c065..0000000000 --- a/packages/frontend-embed/src/components/EmNoteDetailed.vue +++ /dev/null @@ -1,490 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmNoteHeader.vue b/packages/frontend-embed/src/components/EmNoteHeader.vue deleted file mode 100644 index 85b4aac071..0000000000 --- a/packages/frontend-embed/src/components/EmNoteHeader.vue +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmNoteSimple.vue b/packages/frontend-embed/src/components/EmNoteSimple.vue deleted file mode 100644 index b9aaf3fa4a..0000000000 --- a/packages/frontend-embed/src/components/EmNoteSimple.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmNoteSub.vue b/packages/frontend-embed/src/components/EmNoteSub.vue deleted file mode 100644 index 59be8608e0..0000000000 --- a/packages/frontend-embed/src/components/EmNoteSub.vue +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue deleted file mode 100644 index 962a982fb7..0000000000 --- a/packages/frontend-embed/src/components/EmNotes.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue deleted file mode 100644 index 94a91305f4..0000000000 --- a/packages/frontend-embed/src/components/EmPagination.vue +++ /dev/null @@ -1,502 +0,0 @@ - - - - - - - - diff --git a/packages/frontend-embed/src/components/EmPoll.vue b/packages/frontend-embed/src/components/EmPoll.vue deleted file mode 100644 index d197e094c6..0000000000 --- a/packages/frontend-embed/src/components/EmPoll.vue +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmReactionIcon.vue b/packages/frontend-embed/src/components/EmReactionIcon.vue deleted file mode 100644 index 5c38ecb0ed..0000000000 --- a/packages/frontend-embed/src/components/EmReactionIcon.vue +++ /dev/null @@ -1,23 +0,0 @@ - - - - - diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue deleted file mode 100644 index 2ebff489fd..0000000000 --- a/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.vue b/packages/frontend-embed/src/components/EmReactionsViewer.vue deleted file mode 100644 index f5aa6bdc3f..0000000000 --- a/packages/frontend-embed/src/components/EmReactionsViewer.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmSubNoteContent.vue b/packages/frontend-embed/src/components/EmSubNoteContent.vue deleted file mode 100644 index 61815ddfd8..0000000000 --- a/packages/frontend-embed/src/components/EmSubNoteContent.vue +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmTime.vue b/packages/frontend-embed/src/components/EmTime.vue deleted file mode 100644 index 7902e18483..0000000000 --- a/packages/frontend-embed/src/components/EmTime.vue +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmTimelineContainer.vue b/packages/frontend-embed/src/components/EmTimelineContainer.vue deleted file mode 100644 index 60fd67ced9..0000000000 --- a/packages/frontend-embed/src/components/EmTimelineContainer.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmUrl.vue b/packages/frontend-embed/src/components/EmUrl.vue deleted file mode 100644 index 2dbbe90858..0000000000 --- a/packages/frontend-embed/src/components/EmUrl.vue +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/components/EmUserName.vue b/packages/frontend-embed/src/components/EmUserName.vue deleted file mode 100644 index c0c7c443ca..0000000000 --- a/packages/frontend-embed/src/components/EmUserName.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/packages/frontend-embed/src/components/I18n.vue b/packages/frontend-embed/src/components/I18n.vue deleted file mode 100644 index b621110ec9..0000000000 --- a/packages/frontend-embed/src/components/I18n.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - - - diff --git a/packages/frontend-embed/src/custom-emojis.ts b/packages/frontend-embed/src/custom-emojis.ts deleted file mode 100644 index d5b40885c1..0000000000 --- a/packages/frontend-embed/src/custom-emojis.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { shallowRef, watch } from 'vue'; -import * as Misskey from 'misskey-js'; -import { misskeyApi, misskeyApiGet } from '@/misskey-api.js'; - -function get(key: string) { - const value = localStorage.getItem(key); - if (value === null) return null; - return JSON.parse(value); -} - -function set(key: string, value: any) { - localStorage.setItem(key, JSON.stringify(value)); -} - -const storageCache = await get('emojis'); -export const customEmojis = shallowRef(Array.isArray(storageCache) ? storageCache : []); - -export const customEmojisMap = new Map(); -watch(customEmojis, emojis => { - customEmojisMap.clear(); - for (const emoji of emojis) { - customEmojisMap.set(emoji.name, emoji); - } -}, { immediate: true }); - -export async function fetchCustomEmojis(force = false) { - const now = Date.now(); - - let res; - if (force) { - res = await misskeyApi('emojis', {}); - } else { - const lastFetchedAt = await get('lastEmojisFetchedAt'); - if (lastFetchedAt && (now - lastFetchedAt) < 1000 * 60 * 60) return; - res = await misskeyApiGet('emojis', {}); - } - - customEmojis.value = res.emojis; - set('emojis', res.emojis); - set('lastEmojisFetchedAt', now); -} - -let cachedTags; -export function getCustomEmojiTags() { - if (cachedTags) return cachedTags; - - const tags = new Set(); - for (const emoji of customEmojis.value) { - for (const tag of emoji.aliases) { - tags.add(tag); - } - } - const res = Array.from(tags); - cachedTags = res; - return res; -} diff --git a/packages/frontend-embed/src/di.ts b/packages/frontend-embed/src/di.ts deleted file mode 100644 index 22f6276630..0000000000 --- a/packages/frontend-embed/src/di.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { InjectionKey } from 'vue'; -import * as Misskey from 'misskey-js'; -import { MediaProxy } from '@@/js/media-proxy.js'; -import type { ParsedEmbedParams } from '@@/js/embed-page.js'; -import type { ServerContext } from '@/server-context.js'; - -export const DI = { - serverMetadata: Symbol() as InjectionKey, - embedParams: Symbol() as InjectionKey, - serverContext: Symbol() as InjectionKey, - mediaProxy: Symbol() as InjectionKey, -}; diff --git a/packages/frontend-embed/src/i18n.ts b/packages/frontend-embed/src/i18n.ts deleted file mode 100644 index 6ad503b089..0000000000 --- a/packages/frontend-embed/src/i18n.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { markRaw } from 'vue'; -import { I18n } from '@@/js/i18n.js'; -import type { Locale } from '../../../locales/index.js'; -import { locale } from '@@/js/config.js'; - -export const i18n = markRaw(new I18n(locale, _DEV_)); - -export function updateI18n(newLocale: Locale) { - i18n.locale = newLocale; -} diff --git a/packages/frontend-embed/src/pages/clip.vue b/packages/frontend-embed/src/pages/clip.vue deleted file mode 100644 index f4d4e8cf6f..0000000000 --- a/packages/frontend-embed/src/pages/clip.vue +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/pages/not-found.vue b/packages/frontend-embed/src/pages/not-found.vue deleted file mode 100644 index 68897ca7e1..0000000000 --- a/packages/frontend-embed/src/pages/not-found.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/packages/frontend-embed/src/pages/note.vue b/packages/frontend-embed/src/pages/note.vue deleted file mode 100644 index e879430286..0000000000 --- a/packages/frontend-embed/src/pages/note.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/pages/tag.vue b/packages/frontend-embed/src/pages/tag.vue deleted file mode 100644 index 4b00ae7c2d..0000000000 --- a/packages/frontend-embed/src/pages/tag.vue +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/pages/user-timeline.vue b/packages/frontend-embed/src/pages/user-timeline.vue deleted file mode 100644 index 348b1a7622..0000000000 --- a/packages/frontend-embed/src/pages/user-timeline.vue +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/post-message.ts b/packages/frontend-embed/src/post-message.ts deleted file mode 100644 index 93b57c380b..0000000000 --- a/packages/frontend-embed/src/post-message.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const postMessageEventTypes = [ - 'misskey:embed:ready', - 'misskey:embed:changeHeight', -] as const; - -export type PostMessageEventType = typeof postMessageEventTypes[number]; - -export interface PostMessageEventPayload extends Record { - 'misskey:embed:ready': undefined; - 'misskey:embed:changeHeight': { - height: number; - }; -} - -export type MiPostMessageEvent = { - type: T; - iframeId?: string; - payload?: PostMessageEventPayload[T]; -}; - -let defaultIframeId: string | null = null; - -export function setIframeId(id: string): void { - if (defaultIframeId != null) return; - - if (_DEV_) console.log('setIframeId', id); - defaultIframeId = id; -} - -/** - * 親フレームにイベントを送信 - */ -export function postMessageToParentWindow(type: T, payload?: PostMessageEventPayload[T], iframeId: string | null = null): void { - let _iframeId = iframeId; - if (_iframeId == null) { - _iframeId = defaultIframeId; - } - if (_DEV_) console.log('postMessageToParentWindow', type, _iframeId, payload); - window.parent.postMessage({ - type, - iframeId: _iframeId, - payload, - }, '*'); -} diff --git a/packages/frontend-embed/src/server-context.ts b/packages/frontend-embed/src/server-context.ts deleted file mode 100644 index a84a1a726a..0000000000 --- a/packages/frontend-embed/src/server-context.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ -import * as Misskey from 'misskey-js'; - -const providedContextEl = document.getElementById('misskey_embedCtx'); - -export type ServerContext = { - clip?: Misskey.entities.Clip; - note?: Misskey.entities.Note; - user?: Misskey.entities.UserLite; -} | null; - -// NOTE: devモードのときしか embedCtx が null になることは無い -export const serverContext: ServerContext = (providedContextEl && providedContextEl.textContent) ? JSON.parse(providedContextEl.textContent) : null; - -export function assertServerContext>(ctx: ServerContext, entity: K): ctx is Required, K>> { - if (ctx == null) return false; - return entity in ctx; -} diff --git a/packages/frontend-embed/src/server-metadata.ts b/packages/frontend-embed/src/server-metadata.ts deleted file mode 100644 index 6c94aacd48..0000000000 --- a/packages/frontend-embed/src/server-metadata.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; -import { misskeyApi } from '@/misskey-api.js'; - -const providedMetaEl = document.getElementById('misskey_meta'); - -const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null; - -// NOTE: devモードのときしか _serverMetadata が null になることは無い -export const serverMetadata: Misskey.entities.MetaDetailed = _serverMetadata ?? await misskeyApi('meta', { - detail: true, -}); diff --git a/packages/frontend-embed/src/style.scss b/packages/frontend-embed/src/style.scss deleted file mode 100644 index 035d687ee4..0000000000 --- a/packages/frontend-embed/src/style.scss +++ /dev/null @@ -1,446 +0,0 @@ -@charset "utf-8"; - -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -:root { - --MI-radius: 12px; - --MI-marginFull: 14px; - --MI-marginHalf: 10px; - - --MI-margin: var(--MI-marginFull); -} - -html { - background-color: transparent; - color-scheme: light dark; - color: var(--MI_THEME-fg); - accent-color: var(--MI_THEME-accent); - overflow: clip; - overflow-wrap: break-word; - font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; - font-size: 14px; - line-height: 1.35; - text-size-adjust: 100%; - tab-size: 2; - -webkit-text-size-adjust: 100%; - - &, * { - scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent; - scrollbar-width: thin; - - &::-webkit-scrollbar { - width: 6px; - height: 6px; - } - - &::-webkit-scrollbar-track { - background: inherit; - } - - &::-webkit-scrollbar-thumb { - background: var(--MI_THEME-scrollbarHandle); - - &:hover { - background: var(--MI_THEME-scrollbarHandleHover); - } - - &:active { - background: var(--MI_THEME-accent); - } - } - } -} - -html, body { - height: 100%; - touch-action: manipulation; - margin: 0; - padding: 0; - scroll-behavior: smooth; -} - -#misskey_app { - height: 100%; -} - -a { - text-decoration: none; - cursor: pointer; - color: inherit; - tap-highlight-color: transparent; - -webkit-tap-highlight-color: transparent; - -webkit-touch-callout: none; - - &:focus-visible { - outline-offset: 2px; - } - - &:hover { - text-decoration: underline; - } - - &[target="_blank"] { - -webkit-touch-callout: default; - } -} - -rt { - white-space: initial; -} - -:focus-visible { - outline: var(--MI_THEME-focus) solid 2px; - outline-offset: -2px; - - &:hover { - text-decoration: none; - } -} - -.ti { - width: 1.28em; - vertical-align: -12%; - line-height: 1em; - - &::before { - font-size: 128%; - } -} - -.ti-fw { - display: inline-block; - text-align: center; -} - -._nowrap { - white-space: pre !important; - word-wrap: normal !important; // https://codeday.me/jp/qa/20190424/690106.html - overflow: hidden; - text-overflow: ellipsis; -} - -._button { - user-select: none; - -webkit-user-select: none; - -webkit-touch-callout: none; - appearance: none; - display: inline-block; - padding: 0; - margin: 0; // for Safari - background: none; - border: none; - cursor: pointer; - color: inherit; - touch-action: manipulation; - tap-highlight-color: transparent; - -webkit-tap-highlight-color: transparent; - font-size: 1em; - font-family: inherit; - line-height: inherit; - max-width: 100%; - - &:disabled { - opacity: 0.5; - cursor: default; - } -} - -._buttonGray { - @extend ._button; - background: var(--MI_THEME-buttonBg); - - &:not(:disabled):hover { - background: var(--MI_THEME-buttonHoverBg); - } -} - -._buttonPrimary { - @extend ._button; - color: var(--MI_THEME-fgOnAccent); - background: var(--MI_THEME-accent); - - &:not(:disabled):hover { - background: hsl(from var(--MI_THEME-accent) h s calc(l + 5)); - } - - &:not(:disabled):active { - background: hsl(from var(--MI_THEME-accent) h s calc(l - 5)); - } -} - -._buttonGradate { - @extend ._buttonPrimary; - color: var(--MI_THEME-fgOnAccent); - background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); - - &:not(:disabled):hover { - background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); - } - - &:not(:disabled):active { - background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); - } -} - -._buttonRounded { - font-size: 0.95em; - padding: 0.5em 1em; - min-width: 100px; - border-radius: 99rem; - - &._buttonPrimary, - &._buttonGradate { - font-weight: 700; - } -} - -._help { - color: var(--MI_THEME-accent); - cursor: help; -} - -._textButton { - @extend ._button; - color: var(--MI_THEME-accent); - - &:focus-visible { - outline-offset: 2px; - } - - &:not(:disabled):hover { - text-decoration: underline; - } -} - -._panel { - background: var(--MI_THEME-panel); - border-radius: var(--MI-radius); - overflow: clip; -} - -._margin { - margin: var(--MI-margin) 0; -} - -._gaps_m { - display: flex; - flex-direction: column; - gap: 1.5em; -} - -._gaps_s { - display: flex; - flex-direction: column; - gap: 0.75em; -} - -._gaps { - display: flex; - flex-direction: column; - gap: var(--MI-margin); -} - -._buttons { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -._buttonsCenter { - @extend ._buttons; - - justify-content: center; -} - -._borderButton { - @extend ._button; - display: block; - width: 100%; - padding: 10px; - box-sizing: border-box; - text-align: center; - border: solid 0.5px var(--MI_THEME-divider); - border-radius: var(--MI-radius); - - &:active { - border-color: var(--MI_THEME-accent); - } -} - -._popup { - background: var(--MI_THEME-popup); - border-radius: var(--MI-radius); - contain: content; -} - -._acrylic { - background: color(from var(--MI_THEME-panel) srgb r g b / 0.5); - -webkit-backdrop-filter: var(--MI-blur, blur(15px)); - backdrop-filter: var(--MI-blur, blur(15px)); -} - -._fullinfo { - padding: 64px 32px; - text-align: center; -} - -._link { - color: var(--MI_THEME-link); -} - -._caption { - font-size: 0.8em; - opacity: 0.7; -} - -._monospace { - font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important; -} - -// MFM ----------------------------- - -._mfm_blur_ { - filter: blur(6px); - transition: filter 0.3s; - - &:hover { - filter: blur(0px); - } -} - -.mfm-x2 { - --mfm-zoom-size: 200%; -} - -.mfm-x3 { - --mfm-zoom-size: 400%; -} - -.mfm-x4 { - --mfm-zoom-size: 600%; -} - -.mfm-x2, .mfm-x3, .mfm-x4 { - font-size: var(--mfm-zoom-size); - - .mfm-x2, .mfm-x3, .mfm-x4 { - /* only half effective */ - font-size: calc(var(--mfm-zoom-size) / 2 + 50%); - - .mfm-x2, .mfm-x3, .mfm-x4 { - /* disabled */ - font-size: 100%; - } - } -} - -._mfm_rainbow_fallback_ { - background-image: linear-gradient(to right, rgb(255, 0, 0) 0%, rgb(255, 165, 0) 17%, rgb(255, 255, 0) 33%, rgb(0, 255, 0) 50%, rgb(0, 255, 255) 67%, rgb(0, 0, 255) 83%, rgb(255, 0, 255) 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; -} - -@keyframes mfm-spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -@keyframes mfm-spinX { - 0% { transform: perspective(128px) rotateX(0deg); } - 100% { transform: perspective(128px) rotateX(360deg); } -} - -@keyframes mfm-spinY { - 0% { transform: perspective(128px) rotateY(0deg); } - 100% { transform: perspective(128px) rotateY(360deg); } -} - -@keyframes mfm-jump { - 0% { transform: translateY(0); } - 25% { transform: translateY(-16px); } - 50% { transform: translateY(0); } - 75% { transform: translateY(-8px); } - 100% { transform: translateY(0); } -} - -@keyframes mfm-bounce { - 0% { transform: translateY(0) scale(1, 1); } - 25% { transform: translateY(-16px) scale(1, 1); } - 50% { transform: translateY(0) scale(1, 1); } - 75% { transform: translateY(0) scale(1.5, 0.75); } - 100% { transform: translateY(0) scale(1, 1); } -} - -// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`; -// let css = ''; -// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; } -@keyframes mfm-twitch { - 0% { transform: translate(7px, -2px) } - 5% { transform: translate(-3px, 1px) } - 10% { transform: translate(-7px, -1px) } - 15% { transform: translate(0px, -1px) } - 20% { transform: translate(-8px, 6px) } - 25% { transform: translate(-4px, -3px) } - 30% { transform: translate(-4px, -6px) } - 35% { transform: translate(-8px, -8px) } - 40% { transform: translate(4px, 6px) } - 45% { transform: translate(-3px, 1px) } - 50% { transform: translate(2px, -10px) } - 55% { transform: translate(-7px, 0px) } - 60% { transform: translate(-2px, 4px) } - 65% { transform: translate(3px, -8px) } - 70% { transform: translate(6px, 7px) } - 75% { transform: translate(-7px, -2px) } - 80% { transform: translate(-7px, -8px) } - 85% { transform: translate(9px, 3px) } - 90% { transform: translate(-3px, -2px) } - 95% { transform: translate(-10px, 2px) } - 100% { transform: translate(-2px, -6px) } -} - -// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`; -// let css = ''; -// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; } -@keyframes mfm-shake { - 0% { transform: translate(-3px, -1px) rotate(-8deg) } - 5% { transform: translate(0px, -1px) rotate(-10deg) } - 10% { transform: translate(1px, -3px) rotate(0deg) } - 15% { transform: translate(1px, 1px) rotate(11deg) } - 20% { transform: translate(-2px, 1px) rotate(1deg) } - 25% { transform: translate(-1px, -2px) rotate(-2deg) } - 30% { transform: translate(-1px, 2px) rotate(-3deg) } - 35% { transform: translate(2px, 1px) rotate(6deg) } - 40% { transform: translate(-2px, -3px) rotate(-9deg) } - 45% { transform: translate(0px, -1px) rotate(-12deg) } - 50% { transform: translate(1px, 2px) rotate(10deg) } - 55% { transform: translate(0px, -3px) rotate(8deg) } - 60% { transform: translate(1px, -1px) rotate(8deg) } - 65% { transform: translate(0px, -1px) rotate(-7deg) } - 70% { transform: translate(-1px, -3px) rotate(6deg) } - 75% { transform: translate(0px, -2px) rotate(4deg) } - 80% { transform: translate(-2px, -1px) rotate(3deg) } - 85% { transform: translate(1px, -3px) rotate(-10deg) } - 90% { transform: translate(1px, 0px) rotate(3deg) } - 95% { transform: translate(-2px, 0px) rotate(-3deg) } - 100% { transform: translate(2px, 1px) rotate(2deg) } -} - -@keyframes mfm-rubberBand { - from { transform: scale3d(1, 1, 1); } - 30% { transform: scale3d(1.25, 0.75, 1); } - 40% { transform: scale3d(0.75, 1.25, 1); } - 50% { transform: scale3d(1.15, 0.85, 1); } - 65% { transform: scale3d(0.95, 1.05, 1); } - 75% { transform: scale3d(1.05, 0.95, 1); } - to { transform: scale3d(1, 1, 1); } -} - -@keyframes mfm-rainbow { - 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); } - 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); } -} diff --git a/packages/frontend-embed/src/theme.ts b/packages/frontend-embed/src/theme.ts deleted file mode 100644 index 680ab80167..0000000000 --- a/packages/frontend-embed/src/theme.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import tinycolor from 'tinycolor2'; -import lightTheme from '@@/themes/_light.json5'; -import darkTheme from '@@/themes/_dark.json5'; -import type { BundledTheme } from 'shiki/themes'; - -export type Theme = { - id: string; - name: string; - author: string; - desc?: string; - base?: 'dark' | 'light'; - props: Record; - codeHighlighter?: { - base: BundledTheme; - overrides?: Record; - } | { - base: '_none_'; - overrides: Record; - }; -}; - -let timeout: number | null = null; - -export function assertIsTheme(theme: Record): theme is Theme { - return typeof theme === 'object' && theme !== null && 'id' in theme && 'name' in theme && 'author' in theme && 'props' in theme; -} - -export function applyTheme(theme: Theme, persist = true) { - if (timeout) window.clearTimeout(timeout); - - document.documentElement.classList.add('_themeChanging_'); - - timeout = window.setTimeout(() => { - document.documentElement.classList.remove('_themeChanging_'); - }, 1000); - - const colorScheme = theme.base === 'dark' ? 'dark' : 'light'; - - document.documentElement.dataset.colorScheme = colorScheme; - - // Deep copy - const _theme = JSON.parse(JSON.stringify(theme)); - - if (_theme.base) { - const base = [lightTheme, darkTheme].find(x => x.id === _theme.base); - if (base) _theme.props = Object.assign({}, base.props, _theme.props); - } - - const props = compile(_theme); - - for (const tag of document.head.children) { - if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { - tag.setAttribute('content', props['htmlThemeColor']); - break; - } - } - - for (const [k, v] of Object.entries(props)) { - document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); - } - - // iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照 -} - -function compile(theme: Theme): Record { - function getColor(val: string): tinycolor.Instance { - if (val[0] === '@') { // ref (prop) - return getColor(theme.props[val.substring(1)]); - } else if (val[0] === '$') { // ref (const) - return getColor(theme.props[val]); - } else if (val[0] === ':') { // func - const parts = val.split('<'); - const funcTxt = parts.shift(); - const argTxt = parts.shift(); - - if (funcTxt && argTxt) { - const func = funcTxt.substring(1); - const arg = parseFloat(argTxt); - const color = getColor(parts.join('<')); - - switch (func) { - case 'darken': return color.darken(arg); - case 'lighten': return color.lighten(arg); - case 'alpha': return color.setAlpha(arg); - case 'hue': return color.spin(arg); - case 'saturate': return color.saturate(arg); - } - } - } - - // other case - return tinycolor(val); - } - - const props = {}; - - for (const [k, v] of Object.entries(theme.props)) { - if (k.startsWith('$')) continue; // ignore const - - props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v)); - } - - return props; -} - -function genValue(c: tinycolor.Instance): string { - return c.toRgbString(); -} diff --git a/packages/frontend-embed/src/ui.vue b/packages/frontend-embed/src/ui.vue deleted file mode 100644 index 4ba5968a91..0000000000 --- a/packages/frontend-embed/src/ui.vue +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - diff --git a/packages/frontend-embed/src/utils.ts b/packages/frontend-embed/src/utils.ts deleted file mode 100644 index 939648aa38..0000000000 --- a/packages/frontend-embed/src/utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; -import { url } from '@@/js/config.js'; - -export const acct = (user: Misskey.Acct) => { - return Misskey.acct.toString(user); -}; - -export const userName = (user: Misskey.entities.User) => { - return user.name || user.username; -}; - -export const userPage = (user: Misskey.Acct, path?: string, absolute = false) => { - return `${absolute ? url : ''}/@${acct(user)}${(path ? `/${path}` : '')}`; -}; - -export const notePage = (note: Misskey.entities.Note) => { - return `/notes/${note.id}`; -}; diff --git a/packages/frontend-embed/src/workers/draw-blurhash.ts b/packages/frontend-embed/src/workers/draw-blurhash.ts deleted file mode 100644 index 22de6cd3a8..0000000000 --- a/packages/frontend-embed/src/workers/draw-blurhash.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { render } from 'buraha'; - -const canvas = new OffscreenCanvas(64, 64); - -onmessage = (event) => { - // console.log(event.data); - if (!('id' in event.data && typeof event.data.id === 'string')) { - return; - } - if (!('hash' in event.data && typeof event.data.hash === 'string')) { - return; - } - - render(event.data.hash, canvas); - const bitmap = canvas.transferToImageBitmap(); - postMessage({ id: event.data.id, bitmap }); -}; diff --git a/packages/frontend-embed/src/workers/test-webgl2.ts b/packages/frontend-embed/src/workers/test-webgl2.ts deleted file mode 100644 index b203ebe666..0000000000 --- a/packages/frontend-embed/src/workers/test-webgl2.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -const canvas = globalThis.OffscreenCanvas && new OffscreenCanvas(1, 1); -// 環境によってはOffscreenCanvasが存在しないため -// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -const gl = canvas?.getContext('webgl2'); -if (gl) { - postMessage({ result: true }); -} else { - postMessage({ result: false }); -} diff --git a/packages/frontend-embed/src/workers/tsconfig.json b/packages/frontend-embed/src/workers/tsconfig.json deleted file mode 100644 index 8ee8930465..0000000000 --- a/packages/frontend-embed/src/workers/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "compilerOptions": { - "lib": ["esnext", "webworker"], - } -} diff --git a/packages/frontend-embed/tsconfig.json b/packages/frontend-embed/tsconfig.json deleted file mode 100644 index 63e637c844..0000000000 --- a/packages/frontend-embed/tsconfig.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "noEmitOnError": false, - "noImplicitAny": false, - "noImplicitReturns": true, - "noUnusedParameters": false, - "noUnusedLocals": false, - "noFallthroughCasesInSwitch": true, - "declaration": false, - "sourceMap": false, - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "Bundler", - "removeComments": false, - "noLib": false, - "strict": true, - "strictNullChecks": true, - "experimentalDecorators": true, - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, - "isolatedModules": true, - "useDefineForClassFields": true, - "verbatimModuleSyntax": true, - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"], - "@@/*": ["../frontend-shared/*"] - }, - "typeRoots": [ - "./@types", - "./node_modules/@types", - "./node_modules/@vue-macros", - "./node_modules" - ], - "types": [ - "vite/client" - ], - "lib": [ - "esnext", - "dom", - "dom.iterable" - ], - "jsx": "preserve" - }, - "compileOnSave": false, - "include": [ - "./src/**/*.ts", - "./src/**/*.vue", - "./@types/**/*.ts" - ], - "exclude": [ - ".storybook/**/*" - ] -} diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts deleted file mode 100644 index a057581b3a..0000000000 --- a/packages/frontend-embed/vite.config.ts +++ /dev/null @@ -1,179 +0,0 @@ -import path from 'path'; -import pluginVue from '@vitejs/plugin-vue'; -import { type UserConfig, defineConfig } from 'vite'; -import * as yaml from 'js-yaml'; -import { promises as fsp } from 'fs'; - -import locales from '../../locales/index.js'; -import meta from '../../package.json'; -import packageInfo from './package.json' with { type: 'json' }; -import pluginJson5 from './vite.json5.js'; - -const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null; -const host = url ? (new URL(url)).hostname : undefined; - -const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue']; - -/** - * Misskeyのフロントエンドにバンドルせず、CDNなどから別途読み込むリソースを記述する。 - * CDNを使わずにバンドルしたい場合、以下の配列から該当要素を削除orコメントアウトすればOK - */ -const externalPackages = [ - // shiki(コードブロックのシンタックスハイライトで使用中)はテーマ・言語の定義の容量が大きいため、それらはCDNから読み込む - { - name: 'shiki', - match: /^shiki\/(?(langs|themes))$/, - path(id: string, pattern: RegExp): string { - const match = pattern.exec(id)?.groups; - return match - ? `https://esm.sh/shiki@${packageInfo.dependencies.shiki}/${match['subPkg']}` - : id; - }, - }, -]; - -const hash = (str: string, seed = 0): number => { - let h1 = 0xdeadbeef ^ seed, - h2 = 0x41c6ce57 ^ seed; - for (let i = 0, ch; i < str.length; i++) { - ch = str.charCodeAt(i); - h1 = Math.imul(h1 ^ ch, 2654435761); - h2 = Math.imul(h2 ^ ch, 1597334677); - } - - h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); - h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); - - return 4294967296 * (2097151 & h2) + (h1 >>> 0); -}; - -const BASE62_DIGITS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - -function toBase62(n: number): string { - if (n === 0) { - return '0'; - } - let result = ''; - while (n > 0) { - result = BASE62_DIGITS[n % BASE62_DIGITS.length] + result; - n = Math.floor(n / BASE62_DIGITS.length); - } - - return result; -} - -export function getConfig(): UserConfig { - return { - base: '/embed_vite/', - - // The console is shared with backend, so clearing the console will also clear the backend log. - clearScreen: false, - - server: { - // The backend allows access from any addresses, so vite also allows access from any addresses. - host: '0.0.0.0', - allowedHosts: host ? [host] : undefined, - port: 5174, - strictPort: true, - hmr: { - // バックエンド経由での起動時、Viteは5174経由でアセットを参照していると思い込んでいるが実際は3000から配信される - // そのため、バックエンドのWSサーバーにHMRのWSリクエストが吸収されてしまい、正しくHMRが機能しない - // クライアント側のWSポートをViteサーバーのポートに強制させることで、正しくHMRが機能するようになる - clientPort: 5174, - }, - }, - - plugins: [ - pluginVue(), - pluginJson5(), - ], - - resolve: { - extensions, - alias: { - '@/': __dirname + '/src/', - '@@/': __dirname + '/../frontend-shared/', - '/client-assets/': __dirname + '/assets/', - '/static-assets/': __dirname + '/../backend/assets/' - }, - }, - - css: { - modules: { - generateScopedName(name, filename, _css): string { - const id = (path.relative(__dirname, filename.split('?')[0]) + '-' + name).replace(/[\\\/\.\?&=]/g, '-').replace(/(src-|vue-)/g, ''); - if (process.env.NODE_ENV === 'production') { - return 'x' + toBase62(hash(id)).substring(0, 4); - } else { - return id; - } - }, - }, - preprocessorOptions: { - scss: { - api: 'modern-compiler', - }, - }, - }, - - define: { - _VERSION_: JSON.stringify(meta.version), - _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), - _ENV_: JSON.stringify(process.env.NODE_ENV), - _DEV_: process.env.NODE_ENV !== 'production', - _PERF_PREFIX_: JSON.stringify('Misskey:'), - __VUE_OPTIONS_API__: false, - __VUE_PROD_DEVTOOLS__: false, - }, - - build: { - target: [ - 'chrome116', - 'firefox116', - 'safari16', - ], - manifest: 'manifest.json', - rollupOptions: { - input: { - app: './src/boot.ts', - }, - external: externalPackages.map(p => p.match), - output: { - manualChunks: { - vue: ['vue'], - }, - chunkFileNames: process.env.NODE_ENV === 'production' ? '[hash:8].js' : '[name]-[hash:8].js', - assetFileNames: process.env.NODE_ENV === 'production' ? '[hash:8][extname]' : '[name]-[hash:8][extname]', - paths(id) { - for (const p of externalPackages) { - if (p.match.test(id)) { - return p.path(id, p.match); - } - } - - return id; - }, - }, - }, - cssCodeSplit: true, - outDir: __dirname + '/../../built/_frontend_embed_vite_', - assetsDir: '.', - emptyOutDir: false, - sourcemap: process.env.NODE_ENV === 'development', - reportCompressedSize: false, - - // https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies - commonjsOptions: { - include: [/misskey-js/, /node_modules/], - }, - }, - - worker: { - format: 'es', - }, - }; -} - -const config = defineConfig(({ command, mode }) => getConfig()); - -export default config; diff --git a/packages/frontend-embed/vite.json5.ts b/packages/frontend-embed/vite.json5.ts deleted file mode 100644 index 87b67c2142..0000000000 --- a/packages/frontend-embed/vite.json5.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Original: https://github.com/rollup/plugins/tree/8835dd2aed92f408d7dc72d7cc25a9728e16face/packages/json - -import JSON5 from 'json5'; -import { Plugin } from 'rollup'; -import { createFilter, dataToEsm } from '@rollup/pluginutils'; -import { RollupJsonOptions } from '@rollup/plugin-json'; - -// json5 extends SyntaxError with additional fields (without subclassing) -// https://github.com/json5/json5/blob/de344f0619bda1465a6e25c76f1c0c3dda8108d9/lib/parse.js#L1111-L1112 -interface Json5SyntaxError extends SyntaxError { - lineNumber: number; - columnNumber: number; -} - -export default function json5(options: RollupJsonOptions = {}): Plugin { - const filter = createFilter(options.include, options.exclude); - const indent = 'indent' in options ? options.indent : '\t'; - - return { - name: 'json5', - - // eslint-disable-next-line no-shadow - transform(json, id) { - if (id.slice(-6) !== '.json5' || !filter(id)) return null; - - try { - const parsed = JSON5.parse(json); - return { - code: dataToEsm(parsed, { - preferConst: options.preferConst, - compact: options.compact, - namedExports: options.namedExports, - indent, - }), - map: { mappings: '' }, - }; - } catch (err) { - if (!(err instanceof SyntaxError)) { - throw err; - } - const message = 'Could not parse JSON5 file'; - const { lineNumber, columnNumber } = err as Json5SyntaxError; - this.warn({ message, id, loc: { line: lineNumber, column: columnNumber } }); - return null; - } - }, - }; -} diff --git a/packages/frontend-embed/vue-shims.d.ts b/packages/frontend-embed/vue-shims.d.ts deleted file mode 100644 index eba994772d..0000000000 --- a/packages/frontend-embed/vue-shims.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* eslint-disable */ -declare module "*.vue" { - import { defineComponent } from "vue"; - const component: ReturnType; - export default component; -} diff --git a/packages/frontend-shared/.gitignore b/packages/frontend-shared/.gitignore deleted file mode 100644 index 5f6be09d7c..0000000000 --- a/packages/frontend-shared/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/storybook-static -js-built diff --git a/packages/frontend-shared/@types/global.d.ts b/packages/frontend-shared/@types/global.d.ts deleted file mode 100644 index 52081d07b3..0000000000 --- a/packages/frontend-shared/@types/global.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type FIXME = any; - -declare const _LANGS_: string[][]; -declare const _VERSION_: string; -declare const _ENV_: string; -declare const _DEV_: boolean; -declare const _PERF_PREFIX_: string; - -// for dev-mode -declare const _LANGS_FULL_: string[][]; - -// TagCanvas -interface Window { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - TagCanvas: any; -} diff --git a/packages/frontend-shared/build.js b/packages/frontend-shared/build.js deleted file mode 100644 index 9941114757..0000000000 --- a/packages/frontend-shared/build.js +++ /dev/null @@ -1,110 +0,0 @@ -import fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; -import * as esbuild from 'esbuild'; -import { build } from 'esbuild'; -import { globSync } from 'glob'; -import { execa } from 'execa'; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); -const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8')); - -const entryPoints = globSync('./js/**/**.{ts,tsx}'); - -/** @type {import('esbuild').BuildOptions} */ -const options = { - entryPoints, - minify: process.env.NODE_ENV === 'production', - outdir: './js-built', - target: 'es2022', - platform: 'browser', - format: 'esm', - sourcemap: 'linked', -}; - -const args = process.argv.slice(2).map(arg => arg.toLowerCase()); - -// js-built配下をすべて削除する -if (!args.includes('--no-clean')) { - fs.rmSync('./js-built', { recursive: true, force: true }); -} - -if (args.includes('--watch')) { - await watchSrc(); -} else { - await buildSrc(); -} - -async function buildSrc() { - console.log(`[${_package.name}] start building...`); - - await build(options) - .then(() => { - console.log(`[${_package.name}] build succeeded.`); - }) - .catch((err) => { - process.stderr.write(err.stderr); - process.exit(1); - }); - - if (process.env.NODE_ENV === 'production') { - console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`); - } else { - await buildDts(); - } - - fs.copyFileSync('./js/emojilist.json', './js-built/emojilist.json'); - - console.log(`[${_package.name}] finish building.`); -} - -function buildDts() { - return execa( - 'tsc', - [ - '--project', 'tsconfig.json', - '--outDir', 'js-built', - '--declaration', 'true', - '--emitDeclarationOnly', 'true', - ], - { - stdout: process.stdout, - stderr: process.stderr, - }, - ); -} - -async function watchSrc() { - const plugins = [{ - name: 'gen-dts', - setup(build) { - build.onStart(() => { - console.log(`[${_package.name}] detect changed...`); - }); - build.onEnd(async result => { - if (result.errors.length > 0) { - console.error(`[${_package.name}] watch build failed:`, result); - return; - } - await buildDts(); - }); - }, - }]; - - console.log(`[${_package.name}] start watching...`); - - const context = await esbuild.context({ ...options, plugins }); - await context.watch(); - - await new Promise((resolve, reject) => { - process.on('SIGHUP', resolve); - process.on('SIGINT', resolve); - process.on('SIGTERM', resolve); - process.on('uncaughtException', reject); - process.on('exit', resolve); - }).finally(async () => { - await context.dispose(); - console.log(`[${_package.name}] finish watching.`); - }); -} diff --git a/packages/frontend-shared/eslint.config.js b/packages/frontend-shared/eslint.config.js deleted file mode 100644 index f6fd64153c..0000000000 --- a/packages/frontend-shared/eslint.config.js +++ /dev/null @@ -1,106 +0,0 @@ -import globals from 'globals'; -import tsParser from '@typescript-eslint/parser'; -import parser from 'vue-eslint-parser'; -import pluginVue from 'eslint-plugin-vue'; -import pluginMisskey from '@misskey-dev/eslint-plugin'; -import sharedConfig from '../shared/eslint.config.js'; - -// eslint-disable-next-line import/no-default-export -export default [ - ...sharedConfig, - { - files: ['**/*.vue'], - ...pluginMisskey.configs.typescript, - }, - ...pluginVue.configs['flat/recommended'], - { - files: [ - '@types/**/*.ts', - 'js/**/*.ts', - '**/*.vue', - ], - languageOptions: { - globals: { - ...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])), - ...globals.browser, - - // Node.js - module: false, - require: false, - __dirname: false, - - // Misskey - _DEV_: false, - _LANGS_: false, - _VERSION_: false, - _ENV_: false, - _PERF_PREFIX_: false, - }, - parser, - parserOptions: { - extraFileExtensions: ['.vue'], - parser: tsParser, - project: ['./tsconfig.json'], - sourceType: 'module', - tsconfigRootDir: import.meta.dirname, - }, - }, - rules: { - '@typescript-eslint/no-empty-interface': ['error', { - allowSingleExtends: true, - }], - 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], - // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため - // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため - 'id-denylist': ['error', 'window', 'e'], - 'no-shadow': ['warn'], - 'vue/attributes-order': ['error', { - alphabetical: false, - }], - 'vue/no-use-v-if-with-v-for': ['error', { - allowUsingIterationVar: false, - }], - 'vue/no-ref-as-operand': 'error', - 'vue/no-multi-spaces': ['error', { - ignoreProperties: false, - }], - 'vue/no-v-html': 'warn', - 'vue/order-in-components': 'error', - 'vue/html-indent': ['warn', 'tab', { - attribute: 1, - baseIndent: 0, - closeBracket: 0, - alignAttributesVertically: true, - ignores: [], - }], - 'vue/html-closing-bracket-spacing': ['warn', { - startTag: 'never', - endTag: 'never', - selfClosingTag: 'never', - }], - 'vue/multi-word-component-names': 'warn', - 'vue/require-v-for-key': 'warn', - 'vue/no-unused-components': 'warn', - 'vue/no-unused-vars': 'warn', - 'vue/no-dupe-keys': 'warn', - 'vue/valid-v-for': 'warn', - 'vue/return-in-computed-property': 'warn', - 'vue/no-setup-props-reactivity-loss': 'warn', - 'vue/max-attributes-per-line': 'off', - 'vue/html-self-closing': 'off', - 'vue/singleline-html-element-content-newline': 'off', - 'vue/v-on-event-hyphenation': ['error', 'never', { - autofix: true, - }], - 'vue/attribute-hyphenation': ['error', 'never'], - }, - }, - { - ignores: [ - // TODO: Error while loading rule '@typescript-eslint/naming-convention': Cannot use 'in' operator to search for 'type' in undefined のため一時的に無効化 - // See https://github.com/misskey-dev/misskey/pull/15311 - 'js/i18n.ts', - 'js-built/', - ], - }, -]; diff --git a/packages/frontend-shared/js/collapsed.ts b/packages/frontend-shared/js/collapsed.ts deleted file mode 100644 index aa24c43bcb..0000000000 --- a/packages/frontend-shared/js/collapsed.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; - -export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean { - if (note.cw != null) { - return false; - } - - if (note.text != null) { - if ( - note.text.includes('$[x2') || - note.text.includes('$[x3') || - note.text.includes('$[x4') || - note.text.includes('$[scale') || - note.text.split('\n').length > 9 || - note.text.length > 500 - ) { - return true; - } - } - - if (urls.length >= 4) { - return true; - } - - if (note.files != null && note.files.length >= 5) { - return true; - } - - return false; -} diff --git a/packages/frontend-shared/js/config.ts b/packages/frontend-shared/js/config.ts deleted file mode 100644 index 26dd36d6c3..0000000000 --- a/packages/frontend-shared/js/config.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { Locale } from '../../../locales/index.js'; - -// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -const address = new URL(document.querySelector('meta[property="instance_url"]')?.content || location.href); -const siteName = document.querySelector('meta[property="og:site_name"]')?.content; - -export const host = address.host; -export const hostname = address.hostname; -export const url = address.origin; -export const port = address.port; -export const apiUrl = location.origin + '/api'; -export const wsOrigin = location.origin; -export const lang = localStorage.getItem('lang') ?? 'en-US'; -export const langs = _LANGS_; -const preParseLocale = localStorage.getItem('locale'); -export let locale: Locale = preParseLocale ? JSON.parse(preParseLocale) : null; -export const version = _VERSION_; -export const instanceName = (siteName === 'Misskey' || siteName == null) ? host : siteName; -export const ui = localStorage.getItem('ui'); -export const debug = localStorage.getItem('debug') === 'true'; - -export function updateLocale(newLocale: Locale): void { - locale = newLocale; -} diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts deleted file mode 100644 index c4c4a25d74..0000000000 --- a/packages/frontend-shared/js/const.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// ブラウザで直接表示することを許可するファイルの種類のリスト -// ここに含まれないものは application/octet-stream としてレスポンスされる -// SVGはXSSを生むので許可しない -export const FILE_TYPE_BROWSERSAFE = [ - // Images - 'image/png', - 'image/gif', - 'image/jpeg', - 'image/webp', - 'image/avif', - 'image/apng', - 'image/bmp', - 'image/tiff', - 'image/x-icon', - - // OggS - 'audio/opus', - 'video/ogg', - 'audio/ogg', - 'application/ogg', - - // ISO/IEC base media file format - 'video/quicktime', - 'video/mp4', - 'audio/mp4', - 'video/x-m4v', - 'audio/x-m4a', - 'video/3gpp', - 'video/3gpp2', - - 'video/mpeg', - 'audio/mpeg', - - 'video/webm', - 'audio/webm', - - 'audio/aac', - - // see https://github.com/misskey-dev/misskey/pull/10686 - 'audio/flac', - 'audio/wav', - // backward compatibility - 'audio/x-flac', - 'audio/vnd.wave', -]; -/* -https://github.com/sindresorhus/file-type/blob/main/supported.js -https://github.com/sindresorhus/file-type/blob/main/core.js -https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers -*/ - -export const notificationTypes = [ - 'note', - 'follow', - 'mention', - 'reply', - 'renote', - 'quote', - 'reaction', - 'pollEnded', - 'receiveFollowRequest', - 'followRequestAccepted', - 'roleAssigned', - 'chatRoomInvitationReceived', - 'achievementEarned', - 'exportCompleted', - 'login', - 'createToken', - 'test', - 'app', -] as const; -export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; - -export const ROLE_POLICIES = [ - 'gtlAvailable', - 'ltlAvailable', - 'canPublicNote', - 'mentionLimit', - 'canInvite', - 'inviteLimit', - 'inviteLimitCycle', - 'inviteExpirationTime', - 'canManageCustomEmojis', - 'canManageAvatarDecorations', - 'canSearchNotes', - 'canUseTranslator', - 'canHideAds', - 'driveCapacityMb', - 'maxFileSizeMb', - 'alwaysMarkNsfw', - 'canUpdateBioMedia', - 'pinLimit', - 'antennaLimit', - 'wordMuteLimit', - 'webhookLimit', - 'clipLimit', - 'noteEachClipsLimit', - 'userListLimit', - 'userEachUserListsLimit', - 'rateLimitFactor', - 'avatarDecorationLimit', - 'canImportAntennas', - 'canImportBlocking', - 'canImportFollowing', - 'canImportMuting', - 'canImportUserLists', - 'chatAvailability', - 'uploadableFileTypes', -] as const; - -export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime']; -export const MFM_PARAMS: Record = { - tada: ['speed=', 'delay='], - jelly: ['speed=', 'delay='], - twitch: ['speed=', 'delay='], - shake: ['speed=', 'delay='], - spin: ['speed=', 'delay=', 'left', 'alternate', 'x', 'y'], - jump: ['speed=', 'delay='], - bounce: ['speed=', 'delay='], - flip: ['h', 'v'], - x2: [], - x3: [], - x4: [], - scale: ['x=', 'y='], - position: ['x=', 'y='], - fg: ['color='], - bg: ['color='], - border: ['width=', 'style=', 'color=', 'radius=', 'noclip'], - font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'], - blur: [], - rainbow: ['speed=', 'delay='], - rotate: ['deg='], - ruby: [], - unixtime: [], -}; diff --git a/packages/frontend-shared/js/embed-page.ts b/packages/frontend-shared/js/embed-page.ts deleted file mode 100644 index 2a9b7eb478..0000000000 --- a/packages/frontend-shared/js/embed-page.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -//#region Embed関連の定義 - -/** 埋め込みの対象となるエンティティ(/embed/xxx の xxx の部分と対応させる) */ -export const embeddableEntities = [ - 'notes', - 'user-timeline', - 'clips', - 'tags', -] as const; - -/** 埋め込みの対象となるエンティティ */ -export type EmbeddableEntity = typeof embeddableEntities[number]; - -/** 内部でスクロールがあるページ */ -export const embedRouteWithScrollbar: EmbeddableEntity[] = [ - 'clips', - 'tags', - 'user-timeline', -]; - -/** 埋め込みコードのパラメータ */ -export type EmbedParams = { - maxHeight?: number; - colorMode?: 'light' | 'dark'; - rounded?: boolean; - border?: boolean; - autoload?: boolean; - header?: boolean; -}; - -/** 正規化されたパラメータ */ -export type ParsedEmbedParams = Required> & Pick; - -/** パラメータのデフォルトの値 */ -export const defaultEmbedParams = { - maxHeight: undefined, - colorMode: undefined, - rounded: true, - border: true, - autoload: false, - header: true, -} as const satisfies EmbedParams; - -//#endregion - -/** - * パラメータを正規化する(埋め込みページ初期化用) - * @param searchParams URLSearchParamsもしくはクエリ文字列 - * @returns 正規化されたパラメータ - */ -export function parseEmbedParams(searchParams: URLSearchParams | string): ParsedEmbedParams { - let _searchParams: URLSearchParams; - if (typeof searchParams === 'string') { - _searchParams = new URLSearchParams(searchParams); - } else if (searchParams instanceof URLSearchParams) { - _searchParams = searchParams; - } else { - throw new Error('searchParams must be URLSearchParams or string'); - } - - function convertBoolean(value: string | null): boolean | undefined { - if (value === 'true') { - return true; - } else if (value === 'false') { - return false; - } - return undefined; - } - - function convertNumber(value: string | null): number | undefined { - if (value != null && !isNaN(Number(value))) { - return Number(value); - } - return undefined; - } - - function convertColorMode(value: string | null): 'light' | 'dark' | undefined { - if (value != null && ['light', 'dark'].includes(value)) { - return value as 'light' | 'dark'; - } - return undefined; - } - - return { - maxHeight: convertNumber(_searchParams.get('maxHeight')) ?? defaultEmbedParams.maxHeight, - colorMode: convertColorMode(_searchParams.get('colorMode')) ?? defaultEmbedParams.colorMode, - rounded: convertBoolean(_searchParams.get('rounded')) ?? defaultEmbedParams.rounded, - border: convertBoolean(_searchParams.get('border')) ?? defaultEmbedParams.border, - autoload: convertBoolean(_searchParams.get('autoload')) ?? defaultEmbedParams.autoload, - header: convertBoolean(_searchParams.get('header')) ?? defaultEmbedParams.header, - }; -} diff --git a/packages/frontend-shared/js/emojilist.ts b/packages/frontend-shared/js/emojilist.ts deleted file mode 100644 index f8bbf39177..0000000000 --- a/packages/frontend-shared/js/emojilist.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const; - -export type UnicodeEmojiDef = { - name: string; - char: string; - category: typeof unicodeEmojiCategories[number]; -}; - -// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb -import _emojilist from './emojilist.json' with { type: 'json' }; - -export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({ - name: x[1] as string, - char: x[0] as string, - category: unicodeEmojiCategories[x[2] as number], -})); - -const unicodeEmojisMap = new Map( - emojilist.map(x => [x.char, x]), -); - -const _indexByChar = new Map(); -const _charGroupByCategory = new Map(); -for (let i = 0; i < emojilist.length; i++) { - const emo = emojilist[i]; - _indexByChar.set(emo.char, i); - - if (_charGroupByCategory.has(emo.category)) { - _charGroupByCategory.get(emo.category)?.push(emo.char); - } else { - _charGroupByCategory.set(emo.category, [emo.char]); - } -} - -export const emojiCharByCategory = _charGroupByCategory; - -export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string { - // Colorize it because emojilist.json assumes that - return unicodeEmojisMap.get(colorizeEmoji(char)) - // カラースタイル絵文字がjsonに無い場合はテキストスタイル絵文字にフォールバックする - ?? unicodeEmojisMap.get(char) - // それでも見つからない場合はそのまま返す(絵文字情報がjsonに無い場合、このフォールバックが無いとレンダリングに失敗する) - ?? char; -} - -export function getEmojiName(char: string): string { - // Colorize it because emojilist.json assumes that - const idx = _indexByChar.get(colorizeEmoji(char)) ?? _indexByChar.get(char); - if (idx === undefined) { - // 絵文字情報がjsonに無い場合は名前の取得が出来ないのでそのまま返すしか無い - return char; - } else { - return emojilist[idx].name; - } -} - -/** - * テキストスタイル絵文字(U+260Eなどの1文字で表現される絵文字)をカラースタイル絵文字に変換します(VS16:U+FE0Fを付与)。 - */ -export function colorizeEmoji(char: string) { - return char.length === 1 ? `${char}\uFE0F` : char; -} - -export interface CustomEmojiFolderTree { - value: string; - category: string; - children: CustomEmojiFolderTree[]; -} diff --git a/packages/frontend-shared/js/extract-avg-color-from-blurhash.ts b/packages/frontend-shared/js/extract-avg-color-from-blurhash.ts deleted file mode 100644 index 992f6e9a16..0000000000 --- a/packages/frontend-shared/js/extract-avg-color-from-blurhash.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function extractAvgColorFromBlurhash(hash: string) { - return typeof hash === 'string' - ? '#' + [...hash.slice(2, 6)] - .map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x)) - .reduce((a, c) => a * 83 + c, 0) - .toString(16) - .padStart(6, '0') - : undefined; -} diff --git a/packages/frontend-shared/js/i18n.ts b/packages/frontend-shared/js/i18n.ts deleted file mode 100644 index 480cfcd642..0000000000 --- a/packages/frontend-shared/js/i18n.ts +++ /dev/null @@ -1,252 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { ILocale, ParameterizedString } from '../../../locales/index.js'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type TODO = any; - -type FlattenKeys = keyof { - [K in keyof T as T[K] extends ILocale - ? FlattenKeys extends infer C extends string - ? `${K & string}.${C}` - : never - : T[K] extends TPrediction - ? K - : never]: T[K]; -}; - -type ParametersOf> = TKey extends `${infer K}.${infer C}` - // @ts-expect-error -- C は明らかに FlattenKeys になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。 - ? ParametersOf - : TKey extends keyof T - ? T[TKey] extends ParameterizedString - ? P - : never - : never; - -type Tsx = { - readonly [K in keyof T as T[K] extends string ? never : K]: T[K] extends ParameterizedString - ? (arg: { readonly [_ in P]: string | number }) => string - // @ts-expect-error -- 証明省略 - : Tsx; -}; - -export class I18n { - private tsxCache?: Tsx; - private devMode: boolean; - - constructor(public locale: T, devMode = false) { - this.devMode = devMode; - - //#region BIND - this.t = this.t.bind(this); - //#endregion - } - - public get ts(): T { - if (this.devMode) { - class Handler implements ProxyHandler { - get(target: TTarget, p: string | symbol): unknown { - const value = target[p as keyof TTarget]; - - if (typeof value === 'object') { - return new Proxy(value, new Handler()); - } - - if (typeof value === 'string') { - const parameters = Array.from(value.matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter); - - if (parameters.length) { - console.error(`Missing locale parameters: ${parameters.join(', ')} at ${String(p)}`); - } - - return value; - } - - console.error(`Unexpected locale key: ${String(p)}`); - - return p; - } - } - - return new Proxy(this.locale, new Handler()); - } - - return this.locale; - } - - public get tsx(): Tsx { - if (this.devMode) { - if (this.tsxCache) { - return this.tsxCache; - } - - class Handler implements ProxyHandler { - get(target: TTarget, p: string | symbol): unknown { - const value = target[p as keyof TTarget]; - - if (typeof value === 'object') { - return new Proxy(value, new Handler()); - } - - if (typeof value === 'string') { - const quasis: string[] = []; - const expressions: string[] = []; - let cursor = 0; - - while (~cursor) { - const start = value.indexOf('{', cursor); - - if (!~start) { - quasis.push(value.slice(cursor)); - break; - } - - quasis.push(value.slice(cursor, start)); - - const end = value.indexOf('}', start); - - expressions.push(value.slice(start + 1, end)); - - cursor = end + 1; - } - - if (!expressions.length) { - console.error(`Unexpected locale key: ${String(p)}`); - - return () => value; - } - - return (arg: TODO) => { - let str = quasis[0]; - - for (let i = 0; i < expressions.length; i++) { - if (!Object.hasOwn(arg, expressions[i])) { - console.error(`Missing locale parameters: ${expressions[i]} at ${String(p)}`); - } - - str += arg[expressions[i]] + quasis[i + 1]; - } - - return str; - }; - } - - console.error(`Unexpected locale key: ${String(p)}`); - - return p; - } - } - - return this.tsxCache = new Proxy(this.locale, new Handler()) as unknown as Tsx; - } - - if (this.tsxCache) { - return this.tsxCache; - } - - function build(target: ILocale): Tsx { - const result = {} as Tsx; - - for (const k in target) { - if (!Object.hasOwn(target, k)) { - continue; - } - - const value = target[k as keyof typeof target]; - - if (typeof value === 'object') { - (result as TODO)[k] = build(value as ILocale); - } else if (typeof value === 'string') { - const quasis: string[] = []; - const expressions: string[] = []; - let cursor = 0; - - while (~cursor) { - const start = value.indexOf('{', cursor); - - if (!~start) { - quasis.push(value.slice(cursor)); - break; - } - - quasis.push(value.slice(cursor, start)); - - const end = value.indexOf('}', start); - - expressions.push(value.slice(start + 1, end)); - - cursor = end + 1; - } - - if (!expressions.length) { - continue; - } - - (result as TODO)[k] = (arg: TODO) => { - let str = quasis[0]; - - for (let i = 0; i < expressions.length; i++) { - str += arg[expressions[i]] + quasis[i + 1]; - } - - return str; - }; - } - } - return result; - } - - return this.tsxCache = build(this.locale); - } - - /** - * @deprecated なるべくこのメソッド使うよりも ts 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも - */ - public t>(key: TKey): string; - /** - * @deprecated なるべくこのメソッド使うよりも tsx 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも - */ - public t>(key: TKey, args: { readonly [_ in ParametersOf]: string | number }): string; - public t(key: string, args?: { readonly [_: string]: string | number }) { - let str: string | ParameterizedString | ILocale = this.locale; - - for (const k of key.split('.')) { - str = (str as TODO)[k]; - - if (this.devMode) { - if (typeof str === 'undefined') { - console.error(`Unexpected locale key: ${key}`); - return key; - } - } - } - - if (args) { - if (this.devMode) { - const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter)); - - if (missing.length) { - console.error(`Missing locale parameters: ${missing.join(', ')} at ${key}`); - } - } - - for (const [k, v] of Object.entries(args)) { - const search = `{${k}}`; - - if (this.devMode) { - if (!(str as string).includes(search)) { - console.error(`Unexpected locale parameter: ${k} at ${key}`); - } - } - - str = (str as string).replace(search, v.toString()); - } - } - - return str; - } -} diff --git a/packages/frontend-shared/js/intl-const.ts b/packages/frontend-shared/js/intl-const.ts deleted file mode 100644 index 33b65b6e9b..0000000000 --- a/packages/frontend-shared/js/intl-const.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { lang } from '@@/js/config.js'; - -// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP'); - -let _dateTimeFormat: Intl.DateTimeFormat; -try { - _dateTimeFormat = new Intl.DateTimeFormat(versatileLang, { - year: 'numeric', - month: 'numeric', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }); -} catch (err) { - console.warn(err); - if (_DEV_) console.log('[Intl] Fallback to en-US'); - - // Fallback to en-US - _dateTimeFormat = new Intl.DateTimeFormat('en-US', { - year: 'numeric', - month: 'numeric', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }); -} -export const dateTimeFormat = _dateTimeFormat; - -export const timeZone = dateTimeFormat.resolvedOptions().timeZone; - -export const hemisphere = /^(australia|pacific|antarctica|indian)\//i.test(timeZone) ? 'S' : 'N'; - -let _numberFormat: Intl.NumberFormat; -try { - _numberFormat = new Intl.NumberFormat(versatileLang); -} catch (err) { - console.warn(err); - if (_DEV_) console.log('[Intl] Fallback to en-US'); - - // Fallback to en-US - _numberFormat = new Intl.NumberFormat('en-US'); -} -export const numberFormat = _numberFormat; diff --git a/packages/frontend-shared/js/is-link.ts b/packages/frontend-shared/js/is-link.ts deleted file mode 100644 index 946f86400e..0000000000 --- a/packages/frontend-shared/js/is-link.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function isLink(el: HTMLElement) { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - return false; -} diff --git a/packages/frontend-shared/js/media-proxy.ts b/packages/frontend-shared/js/media-proxy.ts deleted file mode 100644 index 2837870c9a..0000000000 --- a/packages/frontend-shared/js/media-proxy.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; -import { query } from './url.js'; - -export class MediaProxy { - private serverMetadata: Misskey.entities.MetaDetailed; - private url: string; - - constructor(serverMetadata: Misskey.entities.MetaDetailed, url: string) { - this.serverMetadata = serverMetadata; - this.url = url; - } - - public getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string { - const localProxy = `${this.url}/proxy`; - let _imageUrl = imageUrl; - - if (imageUrl.startsWith(this.serverMetadata.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { - // もう既にproxyっぽそうだったらurlを取り出す - _imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; - } - - return `${mustOrigin ? localProxy : this.serverMetadata.mediaProxy}/${ - type === 'preview' ? 'preview.webp' - : 'image.webp' - }?${query({ - url: _imageUrl, - ...(!noFallback ? { 'fallback': '1' } : {}), - ...(type ? { [type]: '1' } : {}), - ...(mustOrigin ? { origin: '1' } : {}), - })}`; - } - - public getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { - if (imageUrl == null) return null; - return this.getProxiedImageUrl(imageUrl, type); - } - - public getStaticImageUrl(baseUrl: string): string { - const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, this.url); - - if (u.href.startsWith(`${this.url}/emoji/`)) { - // もう既にemojiっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; - } - - if (u.href.startsWith(this.serverMetadata.mediaProxy + '/')) { - // もう既にproxyっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; - } - - return `${this.serverMetadata.mediaProxy}/static.webp?${query({ - url: u.href, - static: '1', - })}`; - } -} diff --git a/packages/frontend-shared/js/url.ts b/packages/frontend-shared/js/url.ts deleted file mode 100644 index 1f3550d951..0000000000 --- a/packages/frontend-shared/js/url.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* objを検査して - * 1. 配列に何も入っていない時はクエリを付けない - * 2. プロパティがundefinedの時はクエリを付けない - * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) - */ -export function query(obj: Record): string { - const params = Object.entries(obj) - .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) // eslint-disable-line @typescript-eslint/no-unnecessary-condition - .reduce>((a, [k, v]) => (a[k] = v, a), {}); - - return Object.entries(params) - .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`) - .join('&'); -} - -export function appendQuery(url: string, queryString: string): string { - return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${queryString}`; -} - -export function extractDomain(url: string) { - const match = url.match(/^(?:https?:)?(?:\/\/)?(?:[^@\n]+@)?([^:\/\n]+)/im); - return match ? match[1] : null; -} - -export function maybeMakeRelative(urlStr: string, baseStr: string): string { - try { - const baseObj = new URL(baseStr); - const urlObj = new URL(urlStr); - /* in all places where maybeMakeRelative is used, baseStr is the - * instance's public URL, which can't have path components, so the - * relative URL will always have the whole path from the urlStr - */ - if (urlObj.origin === baseObj.origin) { - return urlObj.pathname + urlObj.search + urlObj.hash; - } - return urlStr; - } catch { - return ''; - } -} diff --git a/packages/frontend-shared/js/worker-multi-dispatch.ts b/packages/frontend-shared/js/worker-multi-dispatch.ts deleted file mode 100644 index 5d393ed1ed..0000000000 --- a/packages/frontend-shared/js/worker-multi-dispatch.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -function defaultUseWorkerNumber(prev: number) { - return prev + 1; -} - -type WorkerNumberGetter = (prev: number, totalWorkers: number) => number; - -export class WorkerMultiDispatch { - private symbol = Symbol('WorkerMultiDispatch'); - private workers: Worker[] = []; - private terminated = false; - private prevWorkerNumber = 0; - private getUseWorkerNumber: WorkerNumberGetter; - private finalizationRegistry: FinalizationRegistry; - - constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) { - this.getUseWorkerNumber = getUseWorkerNumber; - for (let i = 0; i < concurrency; i++) { - this.workers.push(workerConstructor()); - } - - this.finalizationRegistry = new FinalizationRegistry(() => { - this.terminate(); - }); - this.finalizationRegistry.register(this, this.symbol); - - if (_DEV_) console.log('WorkerMultiDispatch: Created', this); - } - - public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: WorkerNumberGetter = this.getUseWorkerNumber) { - let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length); - workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length; - if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber); - this.prevWorkerNumber = workerNumber; - - // 不毛だがunionをoverloadに突っ込めない - // https://stackoverflow.com/questions/66507585/overload-signatures-union-types-and-no-overload-matches-this-call-error - // https://github.com/microsoft/TypeScript/issues/14107 - if (Array.isArray(options)) { - this.workers[workerNumber].postMessage(message, options); - } else { - this.workers[workerNumber].postMessage(message, options); - } - return workerNumber; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public addListener(callback: (this: Worker, ev: MessageEvent) => any, options?: boolean | AddEventListenerOptions) { - this.workers.forEach(worker => { - worker.addEventListener('message', callback, options); - }); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public removeListener(callback: (this: Worker, ev: MessageEvent) => any, options?: boolean | AddEventListenerOptions) { - this.workers.forEach(worker => { - worker.removeEventListener('message', callback, options); - }); - } - - public terminate() { - this.terminated = true; - if (_DEV_) console.log('WorkerMultiDispatch: Terminating', this); - this.workers.forEach(worker => { - worker.terminate(); - }); - this.workers = []; - this.finalizationRegistry.unregister(this); - } - - public isTerminated() { - return this.terminated; - } - - public getWorkers() { - return this.workers; - } - - public getSymbol() { - return this.symbol; - } -} diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json deleted file mode 100644 index 63aef63beb..0000000000 --- a/packages/frontend-shared/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "frontend-shared", - "type": "module", - "main": "./js-built/index.js", - "types": "./js-built/index.d.ts", - "exports": { - ".": { - "import": "./js-built/index.js", - "types": "./js-built/index.d.ts" - }, - "./*": { - "import": "./js-built/*", - "types": "./js-built/*" - } - }, - "scripts": { - "build": "node ./build.js", - "watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"", - "eslint": "eslint './**/*.{js,jsx,ts,tsx}'", - "typecheck": "tsc --noEmit", - "lint": "pnpm typecheck && pnpm eslint" - }, - "devDependencies": { - "@types/node": "22.15.28", - "@typescript-eslint/eslint-plugin": "8.33.0", - "@typescript-eslint/parser": "8.33.0", - "esbuild": "0.25.5", - "eslint-plugin-vue": "10.1.0", - "nodemon": "3.1.10", - "typescript": "5.8.3", - "vue-eslint-parser": "10.1.3" - }, - "files": [ - "js-built" - ], - "dependencies": { - "misskey-js": "workspace:*", - "vue": "3.5.16" - } -} diff --git a/packages/frontend-shared/tsconfig.json b/packages/frontend-shared/tsconfig.json deleted file mode 100644 index 12f00eb503..0000000000 --- a/packages/frontend-shared/tsconfig.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "compilerOptions": { - "target": "ES2022", - "module": "nodenext", - "moduleResolution": "nodenext", - "declaration": true, - "declarationMap": true, - "sourceMap": false, - "outDir": "./js-built/", - "removeComments": true, - "resolveJsonModule": true, - "strict": true, - "strictFunctionTypes": true, - "strictNullChecks": true, - "experimentalDecorators": true, - "noImplicitReturns": true, - "esModuleInterop": true, - "verbatimModuleSyntax": true, - "baseUrl": ".", - "paths": { - "@/*": ["./*"], - "@@/*": ["./*"] - }, - "typeRoots": [ - "./@types", - "./node_modules/@types" - ], - "lib": [ - "esnext", - "dom" - ] - }, - "include": [ - "@types/**/*.ts", - "js/**/*" - ], - "exclude": [ - "node_modules", - "test/**/*" - ] -} diff --git a/packages/frontend/.eslintrc.js b/packages/frontend/.eslintrc.js new file mode 100644 index 0000000000..24c3ad4b83 --- /dev/null +++ b/packages/frontend/.eslintrc.js @@ -0,0 +1,91 @@ +module.exports = { + root: true, + env: { + 'node': false, + }, + parser: 'vue-eslint-parser', + parserOptions: { + 'parser': '@typescript-eslint/parser', + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + extraFileExtensions: ['.vue'], + }, + extends: [ + '../shared/.eslintrc.js', + 'plugin:vue/vue3-recommended', + ], + rules: { + '@typescript-eslint/no-empty-interface': [ + 'error', + { + 'allowSingleExtends': true, + }, + ], + '@typescript-eslint/prefer-nullish-coalescing': [ + 'error', + ], + // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため + // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため + 'id-denylist': ['error', 'window', 'e'], + 'no-shadow': ['warn'], + 'vue/attributes-order': ['error', { + 'alphabetical': false, + }], + 'vue/no-use-v-if-with-v-for': ['error', { + 'allowUsingIterationVar': false, + }], + 'vue/no-ref-as-operand': 'error', + 'vue/no-multi-spaces': ['error', { + 'ignoreProperties': false, + }], + 'vue/no-v-html': 'warn', + 'vue/order-in-components': 'error', + 'vue/html-indent': ['warn', 'tab', { + 'attribute': 1, + 'baseIndent': 0, + 'closeBracket': 0, + 'alignAttributesVertically': true, + 'ignores': [], + }], + 'vue/html-closing-bracket-spacing': ['warn', { + 'startTag': 'never', + 'endTag': 'never', + 'selfClosingTag': 'never', + }], + 'vue/multi-word-component-names': 'warn', + 'vue/require-v-for-key': 'warn', + 'vue/no-unused-components': 'warn', + 'vue/no-unused-vars': 'warn', + 'vue/no-dupe-keys': 'warn', + 'vue/valid-v-for': 'warn', + 'vue/return-in-computed-property': 'warn', + 'vue/no-setup-props-destructure': 'warn', + 'vue/max-attributes-per-line': 'off', + 'vue/html-self-closing': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/v-on-event-hyphenation': ['error', 'never', { autofix: true }], + 'vue/attribute-hyphenation': ['error', 'never'], + }, + globals: { + // Node.js + 'module': false, + 'require': false, + '__dirname': false, + + // Vue + '$$': false, + '$ref': false, + '$shallowRef': false, + '$computed': false, + + // Misskey + '_DEV_': false, + '_LANGS_': false, + '_VERSION_': false, + '_ENV_': false, + '_PERF_PREFIX_': false, + '_DATA_TRANSFER_DRIVE_FILE_': false, + '_DATA_TRANSFER_DRIVE_FOLDER_': false, + '_DATA_TRANSFER_DECK_COLUMN_': false, + }, +}; diff --git a/packages/frontend/.storybook/changes.ts b/packages/frontend/.storybook/changes.ts index c7e0048818..fc0f0c286b 100644 --- a/packages/frontend/.storybook/changes.ts +++ b/packages/frontend/.storybook/changes.ts @@ -1,15 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import fs from 'node:fs/promises'; -import { fileURLToPath } from 'node:url'; import path from 'node:path'; import micromatch from 'micromatch'; -import main from './main.js'; - -const __dirname = fileURLToPath(new URL('.', import.meta.url)); +import main from './main'; interface Stats { readonly modules: readonly { @@ -21,8 +13,8 @@ interface Stats { }[]; } -await fs.readFile( - new URL('../storybook-static/preview-stats.json', import.meta.url) +fs.readFile( + path.resolve(__dirname, '../storybook-static/preview-stats.json') ).then((buffer) => { const stats: Stats = JSON.parse(buffer.toString()); const keys = new Set(stats.modules.map((stat) => stat.id)); @@ -47,15 +39,17 @@ await fs.readFile( ) ) .map((path) => path.replace(/(?:(?<=\.stories)\.(?:impl|meta)|\.msw)(?=\.ts$)/g, '')) + .map((path) => (path.startsWith('.') ? path : `./${path}`)) ); if ( micromatch(Array.from(modules), [ '../../assets/**', '../../fluent-emojis/**', '../../locales/ja-JP.yml', + '../../misskey-assets/**', 'assets/**', 'public/**', - 'package.json', + '../../pnpm-lock.yaml', ]).length ) { return; diff --git a/packages/frontend/.storybook/charts.ts b/packages/frontend/.storybook/charts.ts deleted file mode 100644 index 31bb9e51c5..0000000000 --- a/packages/frontend/.storybook/charts.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { HttpResponse, http } from 'msw'; -import type { DefaultBodyType, HttpResponseResolver, JsonBodyType, PathParams } from 'msw'; -import seedrandom from 'seedrandom'; -import { action } from '@storybook/addon-actions'; - -function getChartArray(seed: string, limit: number, option?: { accumulate?: boolean, mul?: number }): number[] { - const rng = seedrandom(seed); - const max = Math.floor(option?.mul ?? 250 * rng()); - let accumulation = 0; - const array: number[] = []; - for (let i = 0; i < limit; i++) { - const num = Math.floor((max + 1) * rng()); - if (option?.accumulate) { - accumulation += num; - array.unshift(accumulation); - } else { - array.push(num); - } - } - return array; -} - -export function getChartResolver(fields: string[], option?: { accumulate?: boolean, mulMap?: Record }): HttpResponseResolver { - return ({ request }) => { - action(`GET ${request.url}`)(); - const limitParam = new URL(request.url).searchParams.get('limit'); - const limit = limitParam ? parseInt(limitParam) : 30; - const res = {}; - for (const field of fields) { - const layers = field.split('.'); - let current = res; - while (layers.length > 1) { - const currentKey = layers.shift()!; - if (current[currentKey] == null) current[currentKey] = {}; - current = current[currentKey]; - } - current[layers[0]] = getChartArray(field, limit, { - accumulate: option?.accumulate, - mul: option?.mulMap != null && field in option.mulMap ? option.mulMap[field] : undefined, - }); - } - return HttpResponse.json(res); - }; -} diff --git a/packages/frontend/.storybook/fake-utils.ts b/packages/frontend/.storybook/fake-utils.ts deleted file mode 100644 index 44e2263ca0..0000000000 --- a/packages/frontend/.storybook/fake-utils.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import seedrandom from 'seedrandom'; - -/** - * AIで生成した無作為なファーストネーム - */ -export const firstNameDict = [ - 'Ethan', 'Olivia', 'Jackson', 'Emma', 'Liam', 'Ava', 'Aiden', 'Sophia', 'Mason', 'Isabella', - 'Noah', 'Mia', 'Lucas', 'Harper', 'Caleb', 'Abigail', 'Samuel', 'Emily', 'Logan', - 'Madison', 'Benjamin', 'Chloe', 'Elijah', 'Grace', 'Alexander', 'Scarlett', 'William', 'Zoey', 'James', 'Lily', -] - -/** - * AIで生成した無作為なラストネーム - */ -export const lastNameDict = [ - 'Anderson', 'Johnson', 'Thompson', 'Davis', 'Rodriguez', 'Smith', 'Patel', 'Williams', 'Lee', 'Brown', - 'Garcia', 'Jackson', 'Martinez', 'Taylor', 'Harris', 'Nguyen', 'Miller', 'Jones', 'Wilson', - 'White', 'Thomas', 'Garcia', 'Martinez', 'Robinson', 'Turner', 'Lewis', 'Hall', 'King', 'Baker', 'Cooper', -] - -/** - * AIで生成した無作為な国名 - */ -export const countryDict = [ - 'Japan', 'Canada', 'Brazil', 'Australia', 'Italy', 'SouthAfrica', 'Mexico', 'Sweden', 'Russia', 'India', - 'Germany', 'Argentina', 'South Korea', 'France', 'Nigeria', 'Turkey', 'Spain', 'Egypt', 'Thailand', - 'Vietnam', 'Kenya', 'Saudi Arabia', 'Netherlands', 'Colombia', 'Poland', 'Chile', 'Malaysia', 'Ukraine', 'New Zealand', 'Peru', -] - -export function text(length: number = 10, seed?: string): string { - let result = ""; - - // シード値を使う場合、同じ数値が羅列されるが、ランダム文字列という意味では満たせていると思うのでこのまま使っておく - const rand = seed ? seedrandom(seed)() : Math.random(); - while (result.length < length) { - result += rand.toString(36).substring(2); - } - - return result.substring(0, length); -} - -export function integer(min: number = 0, max: number = 9999, seed?: string): number { - const rand = seed ? seedrandom(seed)() : Math.random(); - return Math.floor(rand * (max - min)) + min; -} - -export function date(params?: { - yearMin?: number, - yearMax?: number, - monthMin?: number, - monthMax?: number, - dayMin?: number, - dayMax?: number, - hourMin?: number, - hourMax?: number, - minuteMin?: number, - minuteMax?: number, - secondMin?: number, - secondMax?: number, - millisecondMin?: number, - millisecondMax?: number, -}, seed?: string): Date { - const year = integer(params?.yearMin ?? 1970, params?.yearMax ?? (new Date()).getFullYear(), seed); - const month = integer(params?.monthMin ?? 1, params?.monthMax ?? 12, seed); - let day = integer(params?.dayMin ?? 1, params?.dayMax ?? 31, seed); - if (month === 2) { - day = Math.min(day, 28); - } else if ([4, 6, 9, 11].includes(month)) { - day = Math.min(day, 30); - } else { - day = Math.min(day, 31); - } - - const hour = integer(params?.hourMin ?? 0, params?.hourMax ?? 23, seed); - const minute = integer(params?.minuteMin ?? 0, params?.minuteMax ?? 59, seed); - const second = integer(params?.secondMin ?? 0, params?.secondMax ?? 59, seed); - const millisecond = integer(params?.millisecondMin ?? 0, params?.millisecondMax ?? 999, seed); - - return new Date(year, month - 1, day, hour, minute, second, millisecond); -} - -export function boolean(seed?: string): boolean { - const rand = seed ? seedrandom(seed)() : Math.random(); - return rand < 0.5; -} - -export function choose(array: T[], seed?: string): T { - const rand = seed ? seedrandom(seed)() : Math.random(); - return array[Math.floor(rand * array.length)]; -} - -export function firstName(seed?: string): string { - return choose(firstNameDict, seed); -} - -export function lastName(seed?: string): string { - return choose(lastNameDict, seed); -} - -export function country(seed?: string): string { - return choose(countryDict, seed); -} - -const TIME2000 = 946684800000; -export function fakeId(seed?: string): string { - let time = new Date().getTime(); - - time = time - TIME2000; - if (time < 0) time = 0; - - const timeStr = time.toString(36).padStart(8, '0'); - const noiseStr = text(2, seed); - - return timeStr + noiseStr; -} - -export function imageDataUrl(options?: { - size?: { - width?: number, - height?: number, - }, - color?: { - red?: number, - green?: number, - blue?: number, - alpha?: number, - } -}, seed?: string): string { - const canvas = window.document.createElement('canvas'); - canvas.width = options?.size?.width ?? 100; - canvas.height = options?.size?.height ?? 100; - - const ctx = canvas.getContext('2d'); - if (!ctx) { - throw new Error('Failed to get 2d context'); - } - - ctx.beginPath() - - const red = options?.color?.red ?? integer(0, 255, seed); - const green = options?.color?.green ?? integer(0, 255, seed); - const blue = options?.color?.blue ?? integer(0, 255, seed); - const alpha = options?.color?.alpha ?? 1; - ctx.arc(canvas.width / 2, canvas.height / 2, canvas.width / 2, 0, Math.PI * 2, true); - ctx.fillStyle = `rgba(${red}, ${green}, ${blue}, ${alpha})`; - ctx.fill(); - - return canvas.toDataURL('image/png', 1.0); -} diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index 91ef41eedf..5fd21cdf0a 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -1,11 +1,4 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { AISCRIPT_VERSION } from '@syuilo/aiscript'; import type { entities } from 'misskey-js' -import { date, imageDataUrl, text } from "./fake-utils.js"; export function abuseUserReport() { return { @@ -24,96 +17,12 @@ export function abuseUserReport() { }; } -export function channel(id = 'somechannelid', name = 'Some Channel', bannerUrl: string | null = 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true'): entities.Channel { - return { - id, - createdAt: '2016-12-28T22:49:51.000Z', - lastNotedAt: '2016-12-28T22:49:51.000Z', - name, - description: null, - userId: null, - bannerUrl, - pinnedNoteIds: [], - color: '#000', - isArchived: false, - usersCount: 1, - notesCount: 1, - isSensitive: false, - allowRenoteToExternal: false, - }; -} - -export function chatMessage(room = false, id = 'somechatmessageid', text = 'Hello!'): entities.ChatMessage { - const fromUser = userLite(); - const toRoom = chatRoom(); - const toUser = userLite('touserid'); - return { - id, - createdAt: '2016-12-28T22:49:51.000Z', - fromUserId: fromUser.id, - fromUser, - text, - isRead: false, - reactions: [], - ...room ? { - toRoomId: toRoom.id, - toRoom, - } : { - toUserId: toUser.id, - toUser, - }, - }; -} - -export function chatRoom(id = 'somechatroomid', name = 'Some Chat Room'): entities.ChatRoom { - const owner = userLite('someownerid'); - return { - id, - createdAt: '2016-12-28T22:49:51.000Z', - ownerId: owner.id, - owner, - name, - description: 'A chat room for testing', - isMuted: false, - }; -} - -export function clip(id = 'someclipid', name = 'Some Clip'): entities.Clip { - return { - id, - createdAt: '2016-12-28T22:49:51.000Z', - lastClippedAt: null, - userId: 'someuserid', - user: userLite(), - notesCount: undefined, - name, - description: 'Some clip description', - isPublic: false, - favoritedCount: 0, - }; -} - -export function emojiDetailed(id = 'someemojiid', name = 'some_emoji'): entities.EmojiDetailed { - return { - id, - aliases: ['alias1', 'alias2'], - name, - category: 'emojiCategory', - host: null, - url: '/client-assets/about-icon.png', - license: null, - isSensitive: false, - localOnly: false, - roleIdsThatCanBeUsedThisEmojiAsReaction: ['roleId1', 'roleId2'], - }; -} - export function galleryPost(isSensitive = false) { return { id: 'somepostid', createdAt: '2016-12-28T22:49:51.000Z', updatedAt: '2016-12-28T22:49:51.000Z', - userId: 'someuserid', + userid: 'someuserid', user: userDetailed(), title: 'Some post title', description: 'Some post description', @@ -151,99 +60,7 @@ export function file(isSensitive = false) { }; } -const script = `/// @ ${AISCRIPT_VERSION} - -var name = "" - -Ui:render([ - Ui:C:textInput({ - label: "Your name" - onInput: @(v) { name = v } - }) - Ui:C:button({ - text: "Hello" - onClick: @() { - Mk:dialog(null, \`Hello, {name}!\`) - } - }) -]) -`; - -export function flash(): entities.Flash { - return { - id: 'someflashid', - createdAt: '2016-12-28T22:49:51.000Z', - updatedAt: '2016-12-28T22:49:51.000Z', - userId: 'someuserid', - user: userLite(), - title: 'Some Play title', - summary: 'Some Play summary', - script, - visibility: 'public', - likedCount: 0, - isLiked: false, - }; -} - -export function folder(id = 'somefolderid', name = 'Some Folder', parentId: string | null = null): entities.DriveFolder { - return { - id, - createdAt: '2016-12-28T22:49:51.000Z', - name, - parentId, - }; -} - -export function federationInstance(): entities.FederationInstance { - return { - id: 'someinstanceid', - firstRetrievedAt: '2021-01-01T00:00:00.000Z', - host: 'misskey-hub.net', - usersCount: 10, - notesCount: 20, - followingCount: 5, - followersCount: 15, - isNotResponding: false, - isSuspended: false, - suspensionState: 'none', - isBlocked: false, - softwareName: 'misskey', - softwareVersion: '2024.5.0', - openRegistrations: false, - name: 'Misskey Hub', - description: '', - maintainerName: '', - maintainerEmail: '', - isSilenced: false, - iconUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true', - faviconUrl: '', - themeColor: '', - infoUpdatedAt: '', - latestRequestReceivedAt: '', - }; -} - -export function note(id = 'somenoteid'): entities.Note { - return { - id, - createdAt: '2016-12-28T22:49:51.000Z', - deletedAt: null, - text: 'some note', - cw: null, - userId: 'someuserid', - user: userLite(), - visibility: 'public', - reactionAcceptance: 'nonSensitiveOnly', - reactionEmojis: {}, - reactions: {}, - myReaction: null, - reactionCount: 0, - renoteCount: 0, - repliesCount: 0, - }; -} - -export function userLite(id = 'someuserid', username = 'miskist', host: entities.UserDetailed['host'] = 'misskey-hub.net', name: entities.UserDetailed['name'] = 'Misskey User'): entities.UserLite { +export function userDetailed(id = 'someuserid', username = 'miskist', host = 'misskey-hub.net', name = 'Misskey User'): entities.UserDetailed { return { id, username, @@ -252,21 +69,14 @@ export function userLite(id = 'someuserid', username = 'miskist', host: entities onlineStatus: 'unknown', avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true', avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay', - avatarDecorations: [], - emojis: {}, - }; -} - -export function userDetailed(id = 'someuserid', username = 'miskist', host: entities.UserDetailed['host'] = 'misskey-hub.net', name: entities.UserDetailed['name'] = 'Misskey User'): entities.UserDetailed { - return { - ...userLite(id, username, host, name), + emojis: [], bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog', + bannerColor: '#000000', bannerUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true', birthday: '2014-06-20', createdAt: '2016-12-28T22:49:51.000Z', description: 'I am a cool user!', - followingVisibility: 'public', - followersVisibility: 'public', + ffVisibility: 'public', roles: [], fields: [ { @@ -274,7 +84,6 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host: enti value: 'https://misskey-hub.net', }, ], - verifiedLinks: [], followersCount: 1024, followingCount: 16, hasPendingFollowRequestFromYou: false, @@ -301,129 +110,8 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host: enti publicReactions: false, securityKeys: false, twoFactorEnabled: false, - usePasswordLessLogin: false, - twoFactorBackupCodesStock: 'none', updatedAt: null, - lastFetchedAt: null, uri: null, url: null, - movedTo: null, - alsoKnownAs: null, - notify: 'none', - memo: null, }; } - -export function inviteCode(isUsed = false, hasExpiration = false, isExpired = false, isCreatedBySystem = false) { - const date = new Date(); - const createdAt = new Date(); - createdAt.setDate(date.getDate() - 1) - const expiresAt = new Date(); - - if (isExpired) { - expiresAt.setHours(date.getHours() - 1) - } else { - expiresAt.setHours(date.getHours() + 1) - } - - return { - id: "9gyqzizw77", - code: "SLF3JKF7UV2H9", - expiresAt: hasExpiration ? expiresAt.toISOString() : null, - createdAt: createdAt.toISOString(), - createdBy: isCreatedBySystem ? null : userDetailed('8i3rvznx32'), - usedBy: isUsed ? userDetailed('3i3r2znx1v') : null, - usedAt: isUsed ? date.toISOString() : null, - used: isUsed, - } -} - -export function role(params: { - id?: string, - name?: string, - color?: string | null, - iconUrl?: string | null, - description?: string, - isModerator?: boolean, - isAdministrator?: boolean, - displayOrder?: number, - createdAt?: string, - updatedAt?: string, - target?: 'manual' | 'conditional', - isPublic?: boolean, - isExplorable?: boolean, - asBadge?: boolean, - canEditMembersByModerator?: boolean, - usersCount?: number, -}, seed?: string): entities.Role { - const prefix = params.displayOrder ? params.displayOrder.toString().padStart(3, '0') + '-' : ''; - const genId = text(36, seed); - const createdAt = params.createdAt ?? date({}, seed).toISOString(); - const updatedAt = params.updatedAt ?? date({}, seed).toISOString(); - - return { - id: params.id ?? genId, - name: params.name ?? `${prefix}TestRole-${genId}`, - color: params.color ?? '#445566', - iconUrl: params.iconUrl ?? null, - description: params.description ?? '', - isModerator: params.isModerator ?? false, - isAdministrator: params.isAdministrator ?? false, - displayOrder: params.displayOrder ?? 0, - createdAt: createdAt, - updatedAt: updatedAt, - target: params.target ?? 'manual', - isPublic: params.isPublic ?? true, - isExplorable: params.isExplorable ?? true, - asBadge: params.asBadge ?? true, - canEditMembersByModerator: params.canEditMembersByModerator ?? false, - usersCount: params.usersCount ?? 10, - condFormula: { - id: '', - type: 'or', - values: [] - }, - policies: {}, - } -} - -export function emoji(params?: { - id?: string, - name?: string, - host?: string, - uri?: string, - publicUrl?: string, - originalUrl?: string, - type?: string, - aliases?: string[], - category?: string, - license?: string, - isSensitive?: boolean, - localOnly?: boolean, - roleIdsThatCanBeUsedThisEmojiAsReaction?: {id:string, name:string}[], - updatedAt?: string, -}, seed?: string): entities.EmojiDetailedAdmin { - const _seed = seed ?? (params?.id ?? "DEFAULT_SEED"); - const id = params?.id ?? text(32, _seed); - const name = params?.name ?? text(8, _seed); - const updatedAt = params?.updatedAt ?? date({}, _seed).toISOString(); - - const image = imageDataUrl({}, _seed) - - return { - id: id, - name: name, - host: params?.host ?? null, - uri: params?.uri ?? null, - publicUrl: params?.publicUrl ?? image, - originalUrl: params?.originalUrl ?? image, - type: params?.type ?? 'image/png', - aliases: params?.aliases ?? [`alias1-${name}`, `alias2-${name}`], - category: params?.category ?? null, - license: params?.license ?? null, - isSensitive: params?.isSensitive ?? false, - localOnly: params?.localOnly ?? false, - roleIdsThatCanBeUsedThisEmojiAsReaction: params?.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], - updatedAt: updatedAt, - } -} diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index 89d4214141..f442422109 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { existsSync, readFileSync } from 'node:fs'; import { writeFile } from 'node:fs/promises'; import { basename, dirname } from 'node:path/posix'; @@ -17,52 +12,8 @@ interface SatisfiesExpression extends estree.BaseExpression { reference: estree.Identifier; } -interface ImportDeclaration extends estree.ImportDeclaration { - kind?: 'type'; -} - const generator = { ...GENERATOR, - ImportDeclaration(node: ImportDeclaration, state: State) { - state.write('import '); - if (node.kind === 'type') state.write('type '); - const { specifiers } = node; - if (specifiers.length > 0) { - let i = 0; - for (; i < specifiers.length; i++) { - if (i > 0) { - state.write(', '); - } - const specifier = specifiers[i]!; - if (specifier.type === 'ImportDefaultSpecifier') { - state.write(specifier.local.name, specifier); - } else if (specifier.type === 'ImportNamespaceSpecifier') { - state.write(`* as ${specifier.local.name}`, specifier); - } else { - break; - } - } - if (i < specifiers.length) { - state.write('{'); - for (; i < specifiers.length; i++) { - const specifier = specifiers[i]! as estree.ImportSpecifier; - const { name } = specifier.imported as estree.Identifier; - state.write(name, specifier); - if (name !== specifier.local.name) { - state.write(` as ${specifier.local.name}`); - } - if (i < specifiers.length - 1) { - state.write(', '); - } - } - state.write('}'); - } - state.write(' from '); - } - this.Literal(node.source, state); - - state.write(';'); - }, SatisfiesExpression(node: SatisfiesExpression, state: State) { switch (node.expression.type) { case 'ArrowFunctionExpression': { @@ -106,7 +57,7 @@ type ToKebab = T extends readonly [ : T extends readonly [ infer XH extends string, ...infer XR extends readonly string[] - ] + ] ? `${XH}${XR extends readonly string[] ? `-${ToKebab}` : ''}` : ''; @@ -126,19 +77,26 @@ function h( return Object.assign(props || {}, { type }) as T; } -declare namespace h.JSX { - type Element = estree.Node; - type IntrinsicElements = { - [T in keyof typeof generator as ToKebab>>]: { - [K in keyof Omit< - Parameters<(typeof generator)[T]>[0], - 'type' - >]?: Parameters<(typeof generator)[T]>[0][K]; +declare global { + namespace JSX { + type Element = estree.Node; + type ElementClass = never; + type ElementAttributesProperty = never; + type ElementChildrenAttribute = never; + type IntrinsicAttributes = never; + type IntrinsicClassAttributes = never; + type IntrinsicElements = { + [T in keyof typeof generator as ToKebab>>]: { + [K in keyof Omit< + Parameters<(typeof generator)[T]>[0], + 'type' + >]?: Parameters<(typeof generator)[T]>[0][K]; + }; }; - }; + } } -function toStories(component: string): Promise { +function toStories(component: string): string { const msw = `${component.slice(0, -'.vue'.length)}.msw`; const implStories = `${component.slice(0, -'.vue'.length)}.stories.impl`; const metaStories = `${component.slice(0, -'.vue'.length)}.stories.meta`; @@ -176,7 +134,7 @@ function toStories(component: string): Promise { kind={'init' as const} shorthand /> as estree.Property, - ] + ] : []), ]} /> as estree.ObjectExpression; @@ -199,8 +157,7 @@ function toStories(component: string): Promise { /> as estree.ImportSpecifier, ]), ]} - kind={'type'} - /> as ImportDeclaration, + /> as estree.ImportDeclaration, ...(hasMsw ? [ { local={ as estree.Identifier} /> as estree.ImportNamespaceSpecifier, ]} - /> as ImportDeclaration, - ] + /> as estree.ImportDeclaration, + ] : []), ...(hasImplStories ? [] @@ -221,8 +178,8 @@ function toStories(component: string): Promise { specifiers={[ as estree.ImportDefaultSpecifier, ]} - /> as ImportDeclaration, - ]), + /> as estree.ImportDeclaration, + ]), ...(hasMetaStories ? [ { local={ as estree.Identifier} /> as estree.ImportNamespaceSpecifier, ]} - /> as ImportDeclaration, + /> as estree.ImportDeclaration, ] : []), { '/* eslint-disable @typescript-eslint/explicit-function-return-type */\n' + '/* eslint-disable import/no-default-export */\n' + '/* eslint-disable import/no-duplicates */\n' + - '/* eslint-disable import/order */\n' + generate(program, { generator }) + (hasImplStories ? readFileSync(`${implStories}.ts`, 'utf-8') : ''), { @@ -438,41 +394,18 @@ function toStories(component: string): Promise { } // glob('src/{components,pages,ui,widgets}/**/*.vue') -(async () => { - const globs = await Promise.all([ - glob('src/components/global/Mk*.vue'), - glob('src/components/global/RouterView.vue'), - glob('src/components/MkAbuseReportWindow.vue'), - glob('src/components/MkAccountMoved.vue'), - glob('src/components/MkAchievements.vue'), - glob('src/components/MkAnalogClock.vue'), - glob('src/components/MkAnimBg.vue'), - glob('src/components/MkAnnouncementDialog.vue'), - glob('src/components/MkAntennaEditor.vue'), - glob('src/components/MkAntennaEditorDialog.vue'), - glob('src/components/MkAsUi.vue'), - glob('src/components/MkAutocomplete.vue'), - glob('src/components/MkAvatars.vue'), - glob('src/components/Mk[B-E]*.vue'), - glob('src/components/MkFlashPreview.vue'), - glob('src/components/MkGalleryPostPreview.vue'), - glob('src/components/MkSignupServerRules.vue'), - glob('src/components/MkUserSetupDialog.vue'), - glob('src/components/MkUserSetupDialog.*.vue'), - glob('src/components/MkImgPreviewDialog.vue'), - glob('src/components/MkInstanceCardMini.vue'), - glob('src/components/MkInviteCode.vue'), - glob('src/components/MkTagItem.vue'), - glob('src/components/MkRoleSelectDialog.vue'), - glob('src/components/grid/MkGrid.vue'), - glob('src/pages/admin/custom-emojis-manager2.vue'), - glob('src/pages/admin/overview.ap-requests.vue'), - glob('src/pages/user/home.vue'), - glob('src/pages/search.vue'), - ]); - const components = globs.flat(); - await Promise.all(components.map(async (component) => { +Promise.all([ + glob('src/components/global/*.vue'), + glob('src/components/Mk{A,B}*.vue'), + glob('src/components/MkDigitalClock.vue'), + glob('src/components/MkGalleryPostPreview.vue'), + glob('src/components/MkSignupServerRules.vue'), + glob('src/components/MkUserSetupDialog.vue'), + glob('src/components/MkUserSetupDialog.*.vue'), + glob('src/pages/user/home.vue'), +]) + .then((globs) => globs.flat()) + .then((components) => Promise.all(components.map((component) => { const stories = component.replace(/\.vue$/, '.stories.ts'); - await writeFile(stories, await toStories(component)); - })) -})(); + return writeFile(stories, toStories(component)); + }))); diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts index c1119c2523..1d0ce5ab63 100644 --- a/packages/frontend/.storybook/main.ts +++ b/packages/frontend/.storybook/main.ts @@ -1,31 +1,18 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { createRequire } from 'node:module'; -import { dirname, join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { resolve } from 'node:path'; import type { StorybookConfig } from '@storybook/vue3-vite'; import { type Plugin, mergeConfig } from 'vite'; import turbosnap from 'vite-plugin-turbosnap'; - -const require = createRequire(import.meta.url); -const _dirname = fileURLToPath(new URL('.', import.meta.url)); - const config = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], - staticDirs: [{ from: '../assets', to: '/client-assets' }], addons: [ - getAbsolutePath('@storybook/addon-essentials'), - getAbsolutePath('@storybook/addon-interactions'), - getAbsolutePath('@storybook/addon-links'), - getAbsolutePath('@storybook/addon-storysource'), - getAbsolutePath('@storybook/addon-mdx-gfm'), - resolve(_dirname, '../node_modules/storybook-addon-misskey-theme'), + '@storybook/addon-essentials', + '@storybook/addon-interactions', + '@storybook/addon-links', + '@storybook/addon-storysource', + resolve(__dirname, '../node_modules/storybook-addon-misskey-theme'), ], framework: { - name: getAbsolutePath('@storybook/vue3-vite') as '@storybook/vue3-vite', + name: '@storybook/vue3-vite', options: {}, }, docs: { @@ -35,23 +22,15 @@ const config = { disableTelemetry: true, }, async viteFinal(config) { - const replacePluginForIsChromatic = config.plugins?.findIndex((plugin: Plugin) => plugin && plugin.name === 'replace') ?? -1; + const replacePluginForIsChromatic = config.plugins?.findIndex((plugin) => plugin && (plugin as Partial)?.name === 'replace') ?? -1; if (~replacePluginForIsChromatic) { config.plugins?.splice(replacePluginForIsChromatic, 1); } - - //pluginsからcreateSearchIndexを削除、複数あるかもしれないので全て削除 - config.plugins = config.plugins?.filter((plugin: Plugin) => plugin && plugin.name !== 'createSearchIndex') ?? []; - return mergeConfig(config, { plugins: [ - { - // XXX: https://github.com/IanVS/vite-plugin-turbosnap/issues/8 - ...(turbosnap as any as typeof turbosnap['default'])({ - rootDir: config.root ?? process.cwd(), - }), - name: 'fake-turbosnap', - }, + turbosnap({ + rootDir: config.root ?? process.cwd(), + }), ], build: { target: [ @@ -64,7 +43,3 @@ const config = { }, } satisfies StorybookConfig; export default config; - -function getAbsolutePath(value: string): string { - return dirname(require.resolve(join(value, 'package.json'))); -} diff --git a/packages/frontend/.storybook/manager.ts b/packages/frontend/.storybook/manager.ts index 7375a1f2a9..5653deee84 100644 --- a/packages/frontend/.storybook/manager.ts +++ b/packages/frontend/.storybook/manager.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { addons } from '@storybook/manager-api'; import { create } from '@storybook/theming/create'; diff --git a/packages/frontend/.storybook/mocks.ts b/packages/frontend/.storybook/mocks.ts index 29cb112ccb..4091e39686 100644 --- a/packages/frontend/.storybook/mocks.ts +++ b/packages/frontend/.storybook/mocks.ts @@ -1,44 +1,26 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { type SharedOptions, http, HttpResponse } from 'msw'; +import { type SharedOptions, rest } from 'msw'; export const onUnhandledRequest = ((req, print) => { - const url = new URL(req.url); - if (url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(url.pathname)) { + if (req.url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(req.url.pathname)) { return } print.warning() }) satisfies SharedOptions['onUnhandledRequest']; export const commonHandlers = [ - http.get('/fluent-emoji/:codepoints.png', async ({ params }) => { - const { codepoints } = params; + rest.get('/fluent-emoji/:codepoints.png', async (req, res, ctx) => { + const { codepoints } = req.params; const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob()); - return new HttpResponse(value, { - headers: { - 'Content-Type': 'image/png', - }, - }); + return res(ctx.set('Content-Type', 'image/png'), ctx.body(value)); }), - http.get('/fluent-emojis/:codepoints.png', async ({ params }) => { - const { codepoints } = params; + rest.get('/fluent-emojis/:codepoints.png', async (req, res, ctx) => { + const { codepoints } = req.params; const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob()); - return new HttpResponse(value, { - headers: { - 'Content-Type': 'image/png', - }, - }); + return res(ctx.set('Content-Type', 'image/png'), ctx.body(value)); }), - http.get('/twemoji/:codepoints.svg', async ({ params }) => { - const { codepoints } = params; - const value = await fetch(`https://unpkg.com/@discordapp/twemoji@15.0.2/dist/svg/${codepoints}.svg`).then((response) => response.blob()); - return new HttpResponse(value, { - headers: { - 'Content-Type': 'image/svg+xml', - }, - }); + rest.get('/twemoji/:codepoints.svg', async (req, res, ctx) => { + const { codepoints } = req.params; + const value = await fetch(`https://unpkg.com/@discordapp/twemoji@14.1.2/dist/svg/${codepoints}.svg`).then((response) => response.blob()); + return res(ctx.set('Content-Type', 'image/svg+xml'), ctx.body(value)); }), ]; diff --git a/packages/frontend/.storybook/package.json b/packages/frontend/.storybook/package.json deleted file mode 100644 index bedb411a91..0000000000 --- a/packages/frontend/.storybook/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/packages/frontend/.storybook/preload-locale.ts b/packages/frontend/.storybook/preload-locale.ts index c823ff9bee..a54164742a 100644 --- a/packages/frontend/.storybook/preload-locale.ts +++ b/packages/frontend/.storybook/preload-locale.ts @@ -1,13 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { writeFile } from 'node:fs/promises'; -import locales from '../../../locales/index.js'; +import { resolve } from 'node:path'; +import * as locales from '../../../locales'; -await writeFile( - new URL('locale.ts', import.meta.url), +writeFile( + resolve(__dirname, 'locale.ts'), `export default ${JSON.stringify(locales['ja-JP'], undefined, 2)} as const;`, 'utf8', ) diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts index e5573f2ac3..1ff8f71ecd 100644 --- a/packages/frontend/.storybook/preload-theme.ts +++ b/packages/frontend/.storybook/preload-theme.ts @@ -1,10 +1,6 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { readFile, writeFile } from 'node:fs/promises'; -import JSON5 from 'json5'; +import { resolve } from 'node:path'; +import * as JSON5 from 'json5'; const keys = [ '_dark', @@ -30,9 +26,9 @@ const keys = [ 'd-u0', ] -await Promise.all(keys.map((key) => readFile(new URL(`../../frontend-shared/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { +Promise.all(keys.map((key) => readFile(resolve(__dirname, `../src/themes/${key}.json5`), 'utf8'))).then((sources) => { writeFile( - new URL('./themes.ts', import.meta.url), + resolve(__dirname, './themes.ts'), `export default ${JSON.stringify( Object.fromEntries(sources.map((source, i) => [keys[i], JSON5.parse(source)])), undefined, diff --git a/packages/frontend/.storybook/preview-head.html b/packages/frontend/.storybook/preview-head.html index 2431a71ddc..f6a9a4875d 100644 --- a/packages/frontend/.storybook/preview-head.html +++ b/packages/frontend/.storybook/preview-head.html @@ -1,11 +1,6 @@ - - - + diff --git a/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts b/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts index b62096bbe9..d0877ffd3b 100644 --- a/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts +++ b/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts @@ -1,14 +1,9 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { action } from '@storybook/addon-actions'; -import type { StoryObj } from '@storybook/vue3'; -import { HttpResponse, http } from 'msw'; -import { userDetailed } from '../../.storybook/fakes.js'; -import { commonHandlers } from '../../.storybook/mocks.js'; +import { StoryObj } from '@storybook/vue3'; +import { rest } from 'msw'; +import { userDetailed } from '../../.storybook/fakes'; +import { commonHandlers } from '../../.storybook/mocks'; import MkAbuseReportWindow from './MkAbuseReportWindow.vue'; export const Default = { render(args) { @@ -44,9 +39,9 @@ export const Default = { msw: { handlers: [ ...commonHandlers, - http.post('/api/users/report-abuse', async ({ request }) => { - action('POST /api/users/report-abuse')(await request.json()); - return HttpResponse.json({}); + rest.post('/api/users/report-abuse', async (req, res, ctx) => { + action('POST /api/users/report-abuse')(await req.json()); + return res(ctx.json({})); }), ], }, diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue index 61297fdc76..48236782d9 100644 --- a/packages/frontend/src/components/MkAbuseReportWindow.vue +++ b/packages/frontend/src/components/MkAbuseReportWindow.vue @@ -1,8 +1,3 @@ - - -
+
@@ -25,21 +20,21 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.send }}
-
+ diff --git a/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts b/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts deleted file mode 100644 index 4d921a4c48..0000000000 --- a/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { action } from '@storybook/addon-actions'; -import type { StoryObj } from '@storybook/vue3'; -import { HttpResponse, http } from 'msw'; -import { commonHandlers } from '../../.storybook/mocks.js'; -import MkAntennaEditor from './MkAntennaEditor.vue'; -export const Default = { - render(args) { - return { - components: { - MkAntennaEditor, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - created: action('created'), - updated: action('updated'), - deleted: action('deleted'), - }; - }, - }, - template: '', - }; - }, - args: { - }, - parameters: { - layout: 'fullscreen', - msw: { - handlers: [ - ...commonHandlers, - http.post('/api/antennas/create', async ({ request }) => { - action('POST /api/antennas/create')(await request.json()); - return HttpResponse.json({}); - }), - http.post('/api/antennas/update', async ({ request }) => { - action('POST /api/antennas/update')(await request.json()); - return HttpResponse.json({}); - }), - http.post('/api/antennas/delete', async ({ request }) => { - action('POST /api/antennas/delete')(await request.json()); - return HttpResponse.json(); - }), - ], - }, - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue deleted file mode 100644 index e2febf7225..0000000000 --- a/packages/frontend/src/components/MkAntennaEditor.vue +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts b/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts deleted file mode 100644 index 5878b52fb9..0000000000 --- a/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { action } from '@storybook/addon-actions'; -import type { StoryObj } from '@storybook/vue3'; -import { HttpResponse, http } from 'msw'; -import { commonHandlers } from '../../.storybook/mocks.js'; -import MkAntennaEditorDialog from './MkAntennaEditorDialog.vue'; -export const Default = { - render(args) { - return { - components: { - MkAntennaEditorDialog, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - created: action('created'), - updated: action('updated'), - deleted: action('deleted'), - closed: action('closed'), - }; - }, - }, - template: '', - }; - }, - args: { - }, - parameters: { - layout: 'centered', - msw: { - handlers: [ - ...commonHandlers, - http.post('/api/antennas/create', async ({ request }) => { - action('POST /api/antennas/create')(await request.json()); - return HttpResponse.json({}); - }), - http.post('/api/antennas/update', async ({ request }) => { - action('POST /api/antennas/update')(await request.json()); - return HttpResponse.json({}); - }), - http.post('/api/antennas/delete', async ({ request }) => { - action('POST /api/antennas/delete')(await request.json()); - return HttpResponse.json(); - }), - ], - }, - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkAntennaEditorDialog.vue b/packages/frontend/src/components/MkAntennaEditorDialog.vue deleted file mode 100644 index 0ebf5abf4c..0000000000 --- a/packages/frontend/src/components/MkAntennaEditorDialog.vue +++ /dev/null @@ -1,63 +0,0 @@ - - - - - diff --git a/packages/frontend/src/components/MkAsUi.stories.impl.ts b/packages/frontend/src/components/MkAsUi.stories.impl.ts index cf8d5483b9..b67c0e679d 100644 --- a/packages/frontend/src/components/MkAsUi.stories.impl.ts +++ b/packages/frontend/src/components/MkAsUi.stories.impl.ts @@ -1,7 +1,2 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import MkAsUi from './MkAsUi.vue'; void MkAsUi; diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index 20a953c72c..8bfcfa6aa6 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -1,8 +1,3 @@ - -
- {{ c.text }} - + {{ c.text }} + {{ c.text }}
{{ button.text }} @@ -20,41 +15,31 @@ SPDX-License-Identifier: AGPL-3.0-only - + - + - + - + {{ c.text }} -
- -
-
+
@@ -63,23 +48,21 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts index 64ccb708aa..075904d6a3 100644 --- a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts +++ b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts @@ -1,18 +1,14 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { action } from '@storybook/addon-actions'; -import { expect, userEvent, waitFor, within } from '@storybook/test'; -import type { StoryObj } from '@storybook/vue3'; -import { HttpResponse, http } from 'msw'; -import { userDetailed } from '../../.storybook/fakes.js'; -import { commonHandlers } from '../../.storybook/mocks.js'; +import { expect } from '@storybook/jest'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; +import { StoryObj } from '@storybook/vue3'; +import { rest } from 'msw'; +import { userDetailed } from '../../.storybook/fakes'; +import { commonHandlers } from '../../.storybook/mocks'; import MkAutocomplete from './MkAutocomplete.vue'; import MkInput from './MkInput.vue'; -import { tick } from '@/utility/test-utils.js'; +import { tick } from '@/scripts/test-utils'; const common = { render(args) { return { @@ -98,11 +94,11 @@ export const User = { msw: { handlers: [ ...commonHandlers, - http.post('/api/users/search-by-username-and-host', () => { - return HttpResponse.json([ + rest.post('/api/users/search-by-username-and-host', (req, res, ctx) => { + return res(ctx.json([ userDetailed('44', 'mizuki', 'misskey-hub.net', 'Mizuki'), userDetailed('49', 'momoko', 'misskey-hub.net', 'Momoko'), - ]); + ])); }), ], }, @@ -131,12 +127,12 @@ export const Hashtag = { msw: { handlers: [ ...commonHandlers, - http.post('/api/hashtags/search', () => { - return HttpResponse.json([ + rest.post('/api/hashtags/search', (req, res, ctx) => { + return res(ctx.json([ '気象警報注意報', '気象警報', '気象情報', - ]); + ])); }), ], }, diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index e5b9533cd7..fd892d8174 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -1,8 +1,3 @@ - - - @@ -456,16 +412,16 @@ onBeforeUnmount(() => { text-overflow: ellipsis; &:hover { - background: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05)); + background: var(--X3); } &[data-selected='true'] { - background: var(--MI_THEME-accent); + background: var(--accent); color: #fff !important; } &:active { - background: hsl(from var(--MI_THEME-accent) h s calc(l - 10)); + background: var(--accentDarken); color: #fff !important; } } diff --git a/packages/frontend/src/components/MkAvatars.stories.impl.ts b/packages/frontend/src/components/MkAvatars.stories.impl.ts index 6e20294438..14052c7343 100644 --- a/packages/frontend/src/components/MkAvatars.stories.impl.ts +++ b/packages/frontend/src/components/MkAvatars.stories.impl.ts @@ -1,13 +1,8 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import type { StoryObj } from '@storybook/vue3'; -import { HttpResponse, http } from 'msw'; -import { userDetailed } from '../../.storybook/fakes.js'; -import { commonHandlers } from '../../.storybook/mocks.js'; +import { StoryObj } from '@storybook/vue3'; +import { rest } from 'msw'; +import { userDetailed } from '../../.storybook/fakes'; +import { commonHandlers } from '../../.storybook/mocks'; import MkAvatars from './MkAvatars.vue'; export const Default = { render(args) { @@ -38,12 +33,12 @@ export const Default = { msw: { handlers: [ ...commonHandlers, - http.post('/api/users/show', () => { - return HttpResponse.json([ + rest.post('/api/users/show', (req, res, ctx) => { + return res(ctx.json([ userDetailed('17'), userDetailed('20'), userDetailed('18'), - ]); + ])); }), ], }, diff --git a/packages/frontend/src/components/MkAvatars.vue b/packages/frontend/src/components/MkAvatars.vue index 1c44ed60d8..630620fc08 100644 --- a/packages/frontend/src/components/MkAvatars.vue +++ b/packages/frontend/src/components/MkAvatars.vue @@ -1,34 +1,24 @@ - - diff --git a/packages/frontend/src/components/MkButton.stories.impl.ts b/packages/frontend/src/components/MkButton.stories.impl.ts index 0a569b3beb..982a8b3be1 100644 --- a/packages/frontend/src/components/MkButton.stories.impl.ts +++ b/packages/frontend/src/components/MkButton.stories.impl.ts @@ -1,12 +1,7 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - /* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable import/no-default-export */ import { action } from '@storybook/addon-actions'; -import type { StoryObj } from '@storybook/vue3'; +import { StoryObj } from '@storybook/vue3'; import MkButton from './MkButton.vue'; export const Default = { render(args) { diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index 891af7f696..38c79e89d0 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -1,17 +1,11 @@ - - @@ -130,27 +120,18 @@ function onMousedown(evt: MouseEvent): void { font-size: 95%; box-shadow: none; text-decoration: none; - background: var(--MI_THEME-buttonBg); + background: var(--buttonBg); border-radius: 5px; overflow: clip; box-sizing: border-box; transition: background 0.1s ease; - &:hover { - text-decoration: none; - } - &:not(:disabled):hover { - background: var(--MI_THEME-buttonHoverBg); + background: var(--buttonHoverBg); } &:not(:disabled):active { - background: var(--MI_THEME-buttonHoverBg); - } - - &.iconOnly { - padding: 7px; - min-width: auto; + background: var(--buttonHoverBg); } &.small { @@ -173,15 +154,15 @@ function onMousedown(evt: MouseEvent): void { &.primary { font-weight: bold; - color: var(--MI_THEME-fgOnAccent) !important; - background: var(--MI_THEME-accent); + color: var(--fgOnAccent) !important; + background: var(--accent); &:not(:disabled):hover { - background: hsl(from var(--MI_THEME-accent) h s calc(l + 5)); + background: var(--X8); } &:not(:disabled):active { - background: hsl(from var(--MI_THEME-accent) h s calc(l + 5)); + background: var(--X8); } } @@ -222,45 +203,41 @@ function onMousedown(evt: MouseEvent): void { &.gradate { font-weight: bold; - color: var(--MI_THEME-fgOnAccent) !important; - background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); + color: var(--fgOnAccent) !important; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); &:not(:disabled):hover { - background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5))); + background: linear-gradient(90deg, var(--X8), var(--X8)); } &:not(:disabled):active { - background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5))); + background: linear-gradient(90deg, var(--X8), var(--X8)); } } &.danger { - font-weight: bold; - color: var(--MI_THEME-error); + color: #ff2a2a; &.primary { color: #fff; - background: var(--MI_THEME-error); + background: #ff2a2a; &:not(:disabled):hover { - background: hsl(from var(--MI_THEME-error) h s calc(l + 10)); + background: #ff4242; } &:not(:disabled):active { - background: hsl(from var(--MI_THEME-error) h s calc(l - 10)); + background: #d42e2e; } } } &:disabled { - opacity: 0.5; - } - - &.wait { - cursor: wait !important; + opacity: 0.7; } &:focus-visible { + outline: solid 2px var(--focus); outline-offset: 2px; } diff --git a/packages/frontend/src/components/MkCaptcha.stories.impl.ts b/packages/frontend/src/components/MkCaptcha.stories.impl.ts index 475257cc45..6ac437a277 100644 --- a/packages/frontend/src/components/MkCaptcha.stories.impl.ts +++ b/packages/frontend/src/components/MkCaptcha.stories.impl.ts @@ -1,7 +1,2 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import MkCaptcha from './MkCaptcha.vue'; void MkCaptcha; diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index 30940a34a9..1875b507ca 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -1,38 +1,16 @@ - - - diff --git a/packages/frontend/src/components/MkClickerGame.stories.impl.ts b/packages/frontend/src/components/MkClickerGame.stories.impl.ts deleted file mode 100644 index 6e1eb13d61..0000000000 --- a/packages/frontend/src/components/MkClickerGame.stories.impl.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { HttpResponse, http } from 'msw'; -import { action } from '@storybook/addon-actions'; -import { expect, userEvent, within } from '@storybook/test'; -import { commonHandlers } from '../../.storybook/mocks.js'; -import MkClickerGame from './MkClickerGame.vue'; -import type { StoryObj } from '@storybook/vue3'; - -function sleep(ms: number) { - return new Promise(resolve => window.setTimeout(resolve, ms)); -} - -export const Default = { - render(args) { - return { - components: { - MkClickerGame, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - }, - template: '', - }; - }, - async play({ canvasElement }) { - await sleep(1000); - const canvas = within(canvasElement); - const count = canvas.getByTestId('count'); - await expect(count).toHaveTextContent('0'); - const buttonElement = canvas.getByRole('button'); - await userEvent.click(buttonElement); - await expect(count).toHaveTextContent('1'); - }, - parameters: { - layout: 'centered', - msw: { - handlers: [ - ...commonHandlers, - http.post('/api/i/registry/get', async ({ request }) => { - action('POST /api/i/registry/get')(await request.json()); - return HttpResponse.json({ - error: { - message: 'No such key.', - code: 'NO_SUCH_KEY', - id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a', - }, - }, { - status: 400, - }); - }), - http.post('/api/i/registry/set', async ({ request }) => { - action('POST /api/i/registry/set')(await request.json()); - return HttpResponse.json(undefined, { status: 204 }); - }), - http.post('/api/i/claim-achievement', async ({ request }) => { - action('POST /api/i/claim-achievement')(await request.json()); - return HttpResponse.json(undefined, { status: 204 }); - }), - ], - }, - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index 775964af50..a6ab5aded4 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -1,13 +1,8 @@ - - diff --git a/packages/frontend/src/components/MkCode.stories.impl.ts b/packages/frontend/src/components/MkCode.stories.impl.ts deleted file mode 100644 index fae9d459fb..0000000000 --- a/packages/frontend/src/components/MkCode.stories.impl.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import type { StoryObj } from '@storybook/vue3'; -import MkCode from './MkCode.vue'; -const code = `for (let i, 100) { - <: if (i % 15 == 0) "FizzBuzz" - elif (i % 3 == 0) "Fizz" - elif (i % 5 == 0) "Buzz" - else i -}`; -export const Default = { - render(args) { - return { - components: { - MkCode, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - }, - template: '', - }; - }, - args: { - code, - lang: 'is', - }, - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue index f41cb0d00b..1640258d5b 100644 --- a/packages/frontend/src/components/MkCode.vue +++ b/packages/frontend/src/components/MkCode.vue @@ -1,102 +1,15 @@ - - - - diff --git a/packages/frontend/src/components/MkCodeEditor.stories.impl.ts b/packages/frontend/src/components/MkCodeEditor.stories.impl.ts deleted file mode 100644 index c76b6fd08e..0000000000 --- a/packages/frontend/src/components/MkCodeEditor.stories.impl.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import type { StoryObj } from '@storybook/vue3'; -import { action } from '@storybook/addon-actions'; -import MkCodeEditor from './MkCodeEditor.vue'; -const code = `for (let i, 100) { - <: if (i % 15 == 0) "FizzBuzz" - elif (i % 3 == 0) "Fizz" - elif (i % 5 == 0) "Buzz" - else i -}`; -export const Default = { - render(args) { - return { - components: { - MkCodeEditor, - }, - data() { - return { - code, - }; - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - 'change': action('change'), - 'keydown': action('keydown'), - 'enter': action('enter'), - 'update:modelValue': action('update:modelValue'), - }; - }, - }, - template: '', - }; - }, - args: { - lang: 'aiscript', - }, - parameters: { - layout: 'fullscreen', - }, - decorators: [ - () => ({ - template: '
', - }), - ], -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue deleted file mode 100644 index bdb2ba6a44..0000000000 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ /dev/null @@ -1,216 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/MkCodeInline.stories.impl.ts b/packages/frontend/src/components/MkCodeInline.stories.impl.ts deleted file mode 100644 index c17be177cb..0000000000 --- a/packages/frontend/src/components/MkCodeInline.stories.impl.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import type { StoryObj } from '@storybook/vue3'; -import MkCodeInline from './MkCodeInline.vue'; -export const Default = { - render(args) { - return { - components: { - MkCodeInline, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - }, - template: '', - }; - }, - args: { - code: '<: "Hello, world!"', - }, - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCodeInline.vue b/packages/frontend/src/components/MkCodeInline.vue deleted file mode 100644 index 04b6e54108..0000000000 --- a/packages/frontend/src/components/MkCodeInline.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/MkColorInput.stories.impl.ts b/packages/frontend/src/components/MkColorInput.stories.impl.ts deleted file mode 100644 index 3df92ca858..0000000000 --- a/packages/frontend/src/components/MkColorInput.stories.impl.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import type { StoryObj } from '@storybook/vue3'; -import { action } from '@storybook/addon-actions'; -import MkColorInput from './MkColorInput.vue'; -export const Default = { - render(args) { - return { - components: { - MkColorInput, - }, - data() { - return { - color: '#cccccc', - }; - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - 'update:modelValue': action('update:modelValue'), - }; - }, - }, - template: '', - }; - }, - parameters: { - layout: 'fullscreen', - }, - decorators: [ - () => ({ - template: '
', - }), - ], -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue index 80618ebfe4..2471aa958d 100644 --- a/packages/frontend/src/components/MkColorInput.vue +++ b/packages/frontend/src/components/MkColorInput.vue @@ -1,8 +1,3 @@ - - @@ -60,7 +56,7 @@ const onInput = () => { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); + color: var(--fgTransparentWeak); &:empty { display: none; @@ -72,8 +68,8 @@ const onInput = () => { &.focused { > .inputCore { - border-color: var(--MI_THEME-accent) !important; - //box-shadow: 0 0 0 4px var(--MI_THEME-focus); + border-color: var(--accent) !important; + //box-shadow: 0 0 0 4px var(--focus); } } @@ -98,9 +94,9 @@ const onInput = () => { font: inherit; font-weight: normal; font-size: 1em; - color: var(--MI_THEME-fg); - background: var(--MI_THEME-panel); - border: solid 1px var(--MI_THEME-panel); + color: var(--fg); + background: var(--panel); + border: solid 1px var(--panel); border-radius: 6px; outline: none; box-shadow: none; @@ -108,7 +104,7 @@ const onInput = () => { transition: border-color 0.1s ease-out; &:hover { - border-color: var(--MI_THEME-inputBorderHover) !important; + border-color: var(--inputBorderHover) !important; } } diff --git a/packages/frontend/src/components/MkContainer.stories.impl.ts b/packages/frontend/src/components/MkContainer.stories.impl.ts deleted file mode 100644 index 72a7659521..0000000000 --- a/packages/frontend/src/components/MkContainer.stories.impl.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import MkContainer from './MkContainer.vue'; -void MkContainer; diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index d4f338e4c8..af1c57b349 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -1,8 +1,3 @@ - - diff --git a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts deleted file mode 100644 index 78cb4120de..0000000000 --- a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { HttpResponse, http } from 'msw'; -import { action } from '@storybook/addon-actions'; -import { file } from '../../.storybook/fakes.js'; -import { commonHandlers } from '../../.storybook/mocks.js'; -import MkCropperDialog from './MkCropperDialog.vue'; -import type { StoryObj } from '@storybook/vue3'; -export const Default = { - render(args) { - return { - components: { - MkCropperDialog, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - 'ok': action('ok'), - 'cancel': action('cancel'), - 'closed': action('closed'), - }; - }, - }, - template: '', - }; - }, - args: { - file: file(), - aspectRatio: NaN, - }, - parameters: { - chromatic: { - // NOTE: ロードが終わるまで待つ - delay: 3000, - }, - layout: 'centered', - msw: { - handlers: [ - ...commonHandlers, - http.get('/proxy/image.webp', async ({ request }) => { - const url = new URL(request.url).searchParams.get('url'); - if (url === 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true') { - const image = await (await window.fetch('client-assets/fedi.jpg')).blob(); - return new HttpResponse(image, { - headers: { - 'Content-Type': 'image/jpeg', - }, - }); - } else { - return new HttpResponse(null, { status: 404 }); - } - }), - http.post('/api/drive/files/create', async ({ request }) => { - action('POST /api/drive/files/create')(await request.formData()); - return HttpResponse.json(file()); - }), - ], - }, - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 7f592fba79..82363499b7 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -1,8 +1,3 @@ - - - - diff --git a/packages/frontend/src/components/MkCwButton.stories.impl.ts b/packages/frontend/src/components/MkCwButton.stories.impl.ts deleted file mode 100644 index bbe5f4eddb..0000000000 --- a/packages/frontend/src/components/MkCwButton.stories.impl.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import type { StoryObj } from '@storybook/vue3'; -import { action } from '@storybook/addon-actions'; -import { expect, userEvent, within } from '@storybook/test'; -import { file } from '../../.storybook/fakes.js'; -import MkCwButton from './MkCwButton.vue'; -import { i18n } from '@/i18n.js'; - -export const Default = { - render(args) { - return { - components: { - MkCwButton, - }, - data() { - return { - showContent: false, - }; - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - 'update:modelValue': action('update:modelValue'), - }; - }, - }, - template: '', - }; - }, - args: { - text: 'Some CW content', - }, - async play({ canvasElement }) { - const canvas = within(canvasElement); - const buttonElement = canvas.getByRole('button'); - await expect(buttonElement).toHaveTextContent(i18n.ts._cw.show); - await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.chars({ count: 15 })); - await userEvent.click(buttonElement); - await expect(buttonElement).toHaveTextContent(i18n.ts._cw.hide); - await userEvent.click(buttonElement); - }, - parameters: { - chromatic: { - // NOTE: テストが終わるまで待つ - delay: 5000, - }, - layout: 'centered', - }, -} satisfies StoryObj; -export const IncludesTextAndDriveFile = { - ...Default, - args: { - text: 'Some CW content', - files: [file()], - }, - async play({ canvasElement }) { - const canvas = within(canvasElement); - const buttonElement = canvas.getByRole('button'); - await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.chars({ count: 15 })); - await expect(buttonElement).toHaveTextContent(' / '); - await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.files({ count: 1 })); - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue index cc8bbf1104..7d5579040a 100644 --- a/packages/frontend/src/components/MkCwButton.vue +++ b/packages/frontend/src/components/MkCwButton.vue @@ -1,26 +1,19 @@ - - diff --git a/packages/frontend/src/components/MkDialog.stories.impl.ts b/packages/frontend/src/components/MkDialog.stories.impl.ts deleted file mode 100644 index 57c7916049..0000000000 --- a/packages/frontend/src/components/MkDialog.stories.impl.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { action } from '@storybook/addon-actions'; -import { expect, userEvent, waitFor, within } from '@storybook/test'; -import type { StoryObj } from '@storybook/vue3'; -import { i18n } from '@/i18n.js'; -import MkDialog from './MkDialog.vue'; -const Base = { - render(args) { - return { - components: { - MkDialog, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - done: action('done'), - closed: action('closed'), - }; - }, - }, - template: '', - }; - }, - args: { - text: 'Hello, world!', - }, - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; -export const Success = { - ...Base, - args: { - ...Base.args, - type: 'success', - }, -} satisfies StoryObj; -export const Error = { - ...Base, - args: { - ...Base.args, - type: 'error', - }, -} satisfies StoryObj; -export const Warning = { - ...Base, - args: { - ...Base.args, - type: 'warning', - }, -} satisfies StoryObj; -export const Info = { - ...Base, - args: { - ...Base.args, - type: 'info', - }, -} satisfies StoryObj; -export const Question = { - ...Base, - args: { - ...Base.args, - type: 'question', - }, -} satisfies StoryObj; -export const Waiting = { - ...Base, - args: { - ...Base.args, - type: 'waiting', - }, -} satisfies StoryObj; -export const DialogWithActions = { - ...Question, - args: { - ...Question.args, - text: i18n.ts.areYouSure, - actions: [ - { - text: i18n.ts.yes, - primary: true, - callback() { - action('YES')(); - }, - }, - { - text: i18n.ts.no, - callback() { - action('NO')(); - }, - }, - ], - }, -} satisfies StoryObj; -export const DialogWithDangerActions = { - ...Warning, - args: { - ...Warning.args, - text: i18n.ts.resetAreYouSure, - actions: [ - { - text: i18n.ts.yes, - danger: true, - primary: true, - callback() { - action('YES')(); - }, - }, - { - text: i18n.ts.no, - callback() { - action('NO')(); - }, - }, - ], - }, -} satisfies StoryObj; -export const DialogWithInput = { - ...Question, - args: { - ...Question.args, - title: 'Hello, world!', - text: undefined, - input: { - placeholder: i18n.ts.inputMessageHere, - type: 'text', - default: null, - minLength: 2, - maxLength: 3, - }, - }, - async play({ canvasElement }) { - const canvas = within(canvasElement); - await expect(canvasElement).toHaveTextContent(i18n.tsx._dialog.charactersBelow({ current: 0, min: 2 })); - const okButton = canvas.getByRole('button', { name: i18n.ts.ok }); - await expect(okButton).toBeDisabled(); - const input = canvas.getByRole('combobox'); - await waitFor(() => userEvent.hover(input)); - await waitFor(() => userEvent.click(input)); - await waitFor(() => userEvent.type(input, 'M')); - await expect(canvasElement).toHaveTextContent(i18n.tsx._dialog.charactersBelow({ current: 1, min: 2 })); - await waitFor(() => userEvent.type(input, 'i')); - await expect(okButton).toBeEnabled(); - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 3f7519a43f..4d5df0bba4 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -1,46 +1,46 @@ - - diff --git a/packages/frontend/src/components/MkDivider.stories.impl.ts b/packages/frontend/src/components/MkDivider.stories.impl.ts deleted file mode 100644 index a593111987..0000000000 --- a/packages/frontend/src/components/MkDivider.stories.impl.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import MkDivider from './MkDivider.vue'; -void MkDivider; diff --git a/packages/frontend/src/components/MkDivider.vue b/packages/frontend/src/components/MkDivider.vue deleted file mode 100644 index f72f091383..0000000000 --- a/packages/frontend/src/components/MkDivider.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/MkDonation.stories.impl.ts b/packages/frontend/src/components/MkDonation.stories.impl.ts deleted file mode 100644 index 71d0c20c63..0000000000 --- a/packages/frontend/src/components/MkDonation.stories.impl.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { action } from '@storybook/addon-actions'; -import type { StoryObj } from '@storybook/vue3'; -import { onBeforeUnmount } from 'vue'; -import MkDonation from './MkDonation.vue'; -import { instance } from '@/instance.js'; -export const Default = { - render(args) { - return { - components: { - MkDonation, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - closed: action('closed'), - }; - }, - }, - template: '', - }; - }, - args: { - // @ts-expect-error name is used for mocking instance - name: 'Misskey Hub', - }, - decorators: [ - (_, { args }) => ({ - setup() { - // @ts-expect-error name is used for mocking instance - instance.name = args.name; - onBeforeUnmount(() => instance.name = null); - }, - template: '', - }), - ], - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue index 0e0da64750..b5ae4c6c48 100644 --- a/packages/frontend/src/components/MkDonation.vue +++ b/packages/frontend/src/components/MkDonation.vue @@ -1,8 +1,3 @@ - -
- {{ i18n.ts.learnMore }} + {{ i18n.ts.learnMore }}
@@ -38,11 +33,11 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkDrive.navFolder.stories.impl.ts b/packages/frontend/src/components/MkDrive.navFolder.stories.impl.ts deleted file mode 100644 index 9d49f24fa4..0000000000 --- a/packages/frontend/src/components/MkDrive.navFolder.stories.impl.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import MkDrive_navFolder from './MkDrive.navFolder.vue'; -void MkDrive_navFolder; diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue index 224aa2dca7..3349603d3b 100644 --- a/packages/frontend/src/components/MkDrive.navFolder.vue +++ b/packages/frontend/src/components/MkDrive.navFolder.vue @@ -1,11 +1,7 @@ - -